Пример приложения с использованием библиотеки AQuery

в 6:55, , рубрики: android, aquery, Блог компании Surfingbird, Разработка под android

Нас постоянно спрашивают, почему мы используем библиотеку AQuery в своих проектах. В конце концов нам надоело отвечать и мы решили показать, на что способна AQuery в бою.

Но писать какой-то странный псевдокод в духе hello world скучно и неинтересно и поэтому мы решили сделать какое-нибудь небольшое, но полезное приложение. Недавно от Хабра отделился проект Мегамозг и в комментариях к новости высказывали предложение объединить RSS поток со всех ресурсов. Этим мы и займемся.

В конце получится такой прототип приложения IT News (rss с хабра, гиктаймс, мегамозга и с силиконруса/роем упорядоченные по дате)
image

Ссылки для торопыжек:
github: github.com/recoilme/itnews
google play: play.google.com/store/apps/details?id=org.freemp.itnews

Сначала пара слов о самой библиотеке.

Библиотека предназначена в первую очередь для:
— манипулирования UI элементами
— работы с сетью
— работы с изображениями

Это только то, что на поверхности.

Крохотная и без внешних зависимостей. Не навязывает свое использование, не конфликтует с другими библиотеками и не навязывает какого-то стиля при программировании. Вы просто закидываете jar файл и всё.
Итак, по порядку:

Манипулирование UI элементами: пишите меньше, пишите быстрее
Код без AQuery

public void renderContent(Content content, View view) {
        
        
        ImageView tbView = (ImageView) view.findViewById(R.id.icon); 
        if(tbView != null){
                
                tbView.setImageBitmap(R.drawable.icon);
                tbView.setVisibility(View.VISIBLE);
                
                tbView.setOnClickListener(new OnClickListener() {
                                
                                @Override
                                public void onClick(View v) {
                                        someMethod(v);
                                }
                        });
                
        }
        
        TextView nameView = (TextView) view.findViewById(R.id.name);    
        if(nameView != null){
                nameView.setText(content.getPname());
        }
        
        TextView timeView = (TextView) view.findViewById(R.id.time);  
        
        if(timeView != null){
                long now = System.currentTimeMillis();
                timeView.setText(FormatUtility.relativeTime(now, content.getCreate()));
                timeView.setVisibility(View.VISIBLE);
        }
        
        TextView descView = (TextView) view.findViewById(R.id.desc);    
        
        if(descView != null){
                descView.setText(content.getDesc());
                descView.setVisibility(View.VISIBLE);
        }
}

Код с AQuery

public void renderContent(Content content, View view) {
        
        AQuery aq = new AQuery(view);
        
        aq.id(R.id.icon).image(R.drawable.icon).visible().clicked(this, "someMethod");  
        aq.id(R.id.name).text(content.getPname());
        aq.id(R.id.time).text(FormatUtility.relativeTime(System.currentTimeMillis(), content.getCreate())).visible();
        aq.id(R.id.desc).text(content.getDesc()).visible();             
        
        
}

Причем никто не запрещает тут же рядом писать findviewbyid — миксуйте как Вам нравится. Код становится более лаконичным и легко читаемым как будто пишешь не на Яве, а на каком-то Groovy или Kotlin.

Работа с сетью. Гет, пост, мультипарт запросы. Динамическое связывание с активити. Гибкая система кеширования из коробки

AsyncAPI
(далее я буду давать ссылки на wiki, чтобы не плодить энтропию)

Загрузка изображений. Кеширование, анимация, downsampling, манипулирование соотношением сторон — просто забудьте о проблемах с памятью и займитесь делом

ImageLoading

А также аутентификация через кучу ресурсов от фейсбука до твитера. Работа с локейшенами. Куча утилит для дебага, парсинг XML и так далее

великолепная документация с кучей примеров

Но это все слова, давайте попробуем в деле. Создаем пустой проект со следующими зависимостями:
Пример приложения с использованием библиотеки AQuery - 2

Начнем с объявления AQuery, проверяем что все подключилось:

public class ActivityMain extends Activity {

    private AQuery aq;
    private Activity activity;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        activity = this;
        aq = new AQuery(activity);
        AQUtility.setDebug(true);


    }
}

Теперь добавим карточки, и проверим как все работает:

public class ActivityMain extends Activity {

    private AQuery aq;
    private Activity activity;
    private RecyclerView gridView;
    private StaggeredGridLayoutManager mLayoutManager;
    private AdapterMain adapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        activity = this;
        aq = new AQuery(activity);
        AQUtility.setDebug(true);

        gridView = new RecyclerView(activity);
        gridView.setHasFixedSize(true);
        mLayoutManager = new StaggeredGridLayoutManager(1,StaggeredGridLayoutManager.VERTICAL);
        gridView.setLayoutManager(mLayoutManager);
        gridView.setItemAnimator(new DefaultItemAnimator());
        getWindow().setContentView(gridView);

        adapter = new AdapterMain(activity,new String[]{"123","456"});
        gridView.setAdapter(adapter);
    }
}

Адаптер

/**
 * Created by recoilme on 23/01/15.
 */
public class AdapterMain extends RecyclerView.Adapter<AdapterMain.ViewHolder> {

    private String[] data;
    private AQuery aq;
    private Activity activity;

    public AdapterMain(Activity activity,String[] data) {
        this.activity = activity;
        this.data = data;
        aq = new AQuery(activity);
    }
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ViewHolder(View v) {
            super(v);

            mTextView = (TextView) v.findViewById(R.id.articleTitle);
        }
    }

    @Override
    public AdapterMain.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.card, parent, false);

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        aq.id(viewHolder.mTextView).text(data[i]);
    }

    @Override
    public int getItemCount() {
        return data.length;
    }
}

Лэйаут карточки:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/card_view"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        card_view:contentPadding="8dp"
        card_view:cardBackgroundColor="@color/primary_bgr"
        card_view:cardUseCompatPadding="true"
        card_view:cardCornerRadius="4dp">
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    android:id="@+id/articleLayout"
                    android:background="@color/primary_bgr"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

        <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:id="@+id/stgvImageView"
                />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignLeft="@+id/stgvImageView"
                android:layout_alignRight="@+id/stgvImageView"
                android:gravity="center"
                android:layout_alignBottom="@+id/stgvImageView"
                android:layout_alignTop="@+id/stgvImageView"
                android:textColor="@color/white"

                android:textSize="20dp"
                android:id="@+id/siteurl"
                android:visibility="gone"/>
        <View android:layout_width="match_parent" android:layout_height="68dp"
              android:background="@drawable/main_adapter_tagbgr"
              android:layout_alignRight="@+id/stgvImageView"
              android:layout_alignTop="@+id/stgvImageView"
              android:layout_alignLeft="@+id/stgvImageView"
                />
        <LinearLayout android:layout_width="match_parent"
                      android:layout_height="wrap_content"
                      android:layout_below="@+id/stgvImageView"
                      android:id="@+id/footer"
                      android:orientation="vertical"
                      android:paddingTop="16dp"
                      android:paddingBottom="16dp"
                      android:paddingLeft="6dp"
                >
            <TextView android:layout_width="match_parent" android:layout_height="wrap_content"
                      android:id="@+id/articleTitle"
                      android:textAppearance="@android:style/TextAppearance.Medium"
                      android:textColor="@drawable/main_adapter_textselector"
                      android:textStyle="bold"
                      android:layout_marginBottom="16dp"
                      android:paddingRight="8dp"
                      android:paddingLeft="0dp"/>
            <RelativeLayout android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                    >

                <LinearLayout
                        android:id="@+id/authorLayout"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal"
                        android:layout_alignParentLeft="true"
                        android:layout_centerVertical="true"
                        android:layout_alignParentStart="false"
                        android:clickable="true"
                        android:paddingTop="10dp"
                        android:paddingBottom="10dp"
                        android:baselineAligned="false"
                        android:paddingRight="4dp"
                        android:paddingLeft="0dp">
                    <ImageView
                            android:id="@+id/userAva"
                            android:layout_width="20dp"
                            android:layout_height="20dp"
                            android:layout_gravity="center_vertical"
                            android:background="#f8f8f8" />

                    <TextView
                            android:id="@+id/userFullname"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_gravity="center_vertical"
                            android:textColor="@color/gray_text"
                            android:paddingLeft="8dp"
                            android:ellipsize="end"
                            android:maxWidth="160dp"
                            android:singleLine="true"
                            android:textAppearance="@android:style/TextAppearance.Small"/>
                </LinearLayout>

            </RelativeLayout>

        </LinearLayout>

    </RelativeLayout>
</android.support.v7.widget.CardView>

Проверим, что получилось, должно быть примерно так:

Пример приложения с использованием библиотеки AQuery - 3

Теперь начинаем магию с AQuery — дергаем rss и парсим его:

просто пишем aq.ajax(url, XmlDom.class, this, «onRequest») — AQuery сделает все остальное


    public void request(String url) {
        aq.ajax(url, XmlDom.class,this,"onRequest");
    }

    public void onRequest(String url,XmlDom xml, AjaxStatus status) {
        if (status.getCode()==200) {
            String logo = "";
            try {
                logo = xml.tags("url").get(0).text();
            }
            catch (Exception e) {
                e.printStackTrace();
            }

            List<XmlDom> xmlItems = xml.tags("item");

            for(XmlDom xmlItem: xmlItems){
                ClassItem item = new ClassItem();
                String description = xmlItem.tag("description").text();
                item.setLogo(logo);
                item.setAuthor(xmlItem.tag("author").text());
                item.setTitle(xmlItem.tag("title").text());
                item.setDescription(description);
                item.setLink(xmlItem.tag("link").text());
                String pubDate = xmlItem.tag("pubDate").text();
                Date date = new Date();
                try {
                    date = formatter.parse(pubDate);
                }
                catch (Exception e) {
                    AQUtility.debug("errorParsingDate",e.toString());
                }
                item.setDate(date);
                String src = "";
                try {
                    src = new XmlDom("<xml>"+description+"</xml>").tag("img").attr("src");
                    if (src.startsWith("//") ) {
                        src = "http:"+src;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                item.setImg(src);
                items.add(item);
            }
            adapter.notifyDataSetChanged();
        }

    }

В класс AjaxStatus приходит детальная информация о результатах выполнения запроса + в AQuery встроен простенький парсер XML. Нет необходимости беспокоиться о наличии активити на момент завершения запроса, AQuery сделает это за нас. Плюс модуль HTTP запросов гораздо гибче чем в примере выше, можно кастомизировать все, от хидеров до метода выполнения запроса. А если вам необходимо, например, закешировать запрос — просто добавляете параметр fileCache=true и время, на которое запрос должен быть закеширован. Есть функционал для инвалидации кеша в случае ошибки, например, и так далее.

Мы же пока вернемся к адаптеру, и обогатим rss поток функционалом отображения картинок. Тем более что с AQuery это не просто, а очень просто:

public class AdapterMain extends RecyclerView.Adapter<AdapterMain.ViewHolder> {

    private ArrayList<ClassItem> data;
    private AQuery aq;
    private Activity activity;
    private DateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm", Locale.getDefault());

    public AdapterMain(Activity activity,ArrayList<ClassItem> data) {
        this.activity = activity;
        this.data = data;
        aq = new AQuery(activity);
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView stgvImageView;
        private ImageView userAva;
        private TextView siteurl;
        private TextView userFullname;
        private TextView articleTitle;
        public ViewHolder(View holderView) {
            super(holderView);
            stgvImageView = (ImageView) holderView.findViewById(R.id.stgvImageView);
            siteurl = (TextView) holderView.findViewById(R.id.siteurl);
            userAva = (ImageView) holderView.findViewById(R.id.userAva);

            userFullname = (TextView) holderView.findViewById(R.id.userFullname);
            articleTitle = (TextView) holderView.findViewById(R.id.articleTitle);
        }
    }

    @Override
    public AdapterMain.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.card, parent, false);

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int i) {
        ClassItem item = data.get(i);
        aq.id(viewHolder.articleTitle).text(item.getTitle());
        aq.id(viewHolder.siteurl).text(item.getLink());
        aq.id(viewHolder.userAva).image(item.getLogo());
        aq.id(viewHolder.userFullname).text(item.getAuthor() + " " + formatter.format(item.getDate()));
        if (TextUtils.equals(item.getImg(),""))
            aq.id(viewHolder.stgvImageView).gone();
        else {
            aq.id(viewHolder.stgvImageView).visible().image(item.getImg(), true, false, 640, 0, null, AQuery.FADE_IN, AQuery.RATIO_PRESERVE);
        }
    }

    @Override
    public int getItemCount() {
        return data.size();
    }
}

Буквально одной строкой мы отдаунскейлили картинку, включили кеширование её в памяти и отскейлили до нужного соотношения сторон, попутно включив анимацию отображения.

        aq.id(viewHolder.stgvImageView).visible().image(item.getImg(), true, false, 640, 0, null, AQuery.FADE_IN, AQuery.RATIO_PRESERVE);

Если заглянуть в код библиотеки, то можно увидеть что вся логика работы с изображениями построена на weakReference, применяется вытесняющий LRU кэш, скейлинг производится с использованием оптимизированных методов inSampleSize и так далее. Более того, можно вручную управлять параметрами кеширования от размеров кеша под различные типы картинок (маленькие, большие, средние) до методики кеширования и количества картинок, одновременно хранящихся в кеше.

Пример конфига с отключенным кешированием на файловой системе (фрагмент application)


    @Override
    public void onLowMemory(){

        //clear all memory cached images when system is in low memory
        //note that you can configure the max image cache count, see CONFIGURATION
        BitmapAjaxCallback.clearCache();
    }

    @Override
    public void onCreate() {

        //Config cache
        BitmapAjaxCallback.setDelayWrite(true);
        BitmapAjaxCallback.setPixelLimit(640*800);
        BitmapAjaxCallback.setMaxPixelLimit(4096000);
    }

В данном случае картинки не будут скачиваться во временный файл, что может быть удобно при единовременной загрузке сотен изображений.

А вот так выглядит принудительная очистка кэша, например, при перезагрузке страницы:

BitmapAjaxCallback.clearCache();

А в нашем примере, собственно, осталось добавить обновление страницы:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ...
        swipeRefreshLayout = new SwipeRefreshLayout(activity);

        swipeRefreshLayout.setOnRefreshListener(this);
        swipeRefreshLayout.setColorScheme(android.R.color.holo_blue_bright,
                android.R.color.holo_green_light,
                android.R.color.holo_orange_light,
                android.R.color.holo_red_light);

       ....

    @Override
    public void onRefresh() {
        getFeeds();
    }

Ну и расширим список фидов:

      private final String[] FEEDS = new String[]{"http://roem.ru/rss/","http://siliconrus.com/feed/","http://habrahabr.ru/rss/","http://megamozg.ru/rss/","http://geektimes.ru/rss/"};

В результате получилось приложение для чтения rss лент с пула основных it ресурсов, которое благодаря AQuery удалось написать буквально за 5 часов, без танцев с подключением кучи библиотек, сосредоточившись, собственно, на коде, а не на процессе. За что мы и любим AQuery &)

Ложка дёгтя — библиотека довольно редко обновляется и практически не развивается. Что с одной стороны говорит о её зрелости, а с другой — о том, что разработчик хотел собрать денег на её развитие до фреймворка, но разочаровался в модели пожертвований и забил. Впрочем за годы её использования ни с одной ошибкой в её коде мне столкнуться не посчастливилось, чего и другим библиотекам искренне желаю.

P.S.: Я выложил на github получившийся rss reader, конечно это только прототип, но вполне рабочий:
github.com/recoilme/itnews
google play: play.google.com/store/apps/details?id=org.freemp.itnews

Автор: recompileme

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js