Нас постоянно спрашивают, почему мы используем библиотеку AQuery в своих проектах. В конце концов нам надоело отвечать и мы решили показать, на что способна AQuery в бою.
Но писать какой-то странный псевдокод в духе hello world скучно и неинтересно и поэтому мы решили сделать какое-нибудь небольшое, но полезное приложение. Недавно от Хабра отделился проект Мегамозг и в комментариях к новости высказывали предложение объединить RSS поток со всех ресурсов. Этим мы и займемся.
В конце получится такой прототип приложения IT News (rss с хабра, гиктаймс, мегамозга и с силиконруса/роем упорядоченные по дате):

Ссылки для торопыжек:
github: github.com/recoilme/itnews
google play: play.google.com/store/apps/details?id=org.freemp.itnews
Сначала пара слов о самой библиотеке.
Библиотека предназначена в первую очередь для:
— манипулирования UI элементами
— работы с сетью
— работы с изображениями
Это только то, что на поверхности.
Крохотная и без внешних зависимостей. Не навязывает свое использование, не конфликтует с другими библиотеками и не навязывает какого-то стиля при программировании. Вы просто закидываете jar файл и всё.
Итак, по порядку:
Манипулирование UI элементами: пишите меньше, пишите быстрее
Код без AQuery
Код с AQuery
Причем никто не запрещает тут же рядом писать findviewbyid — миксуйте как Вам нравится. Код становится более лаконичным и легко читаемым как будто пишешь не на Яве, а на каком-то Groovy или Kotlin.
Работа с сетью. Гет, пост, мультипарт запросы. Динамическое связывание с активити. Гибкая система кеширования из коробки
AsyncAPI
(далее я буду давать ссылки на wiki, чтобы не плодить энтропию)
Загрузка изображений. Кеширование, анимация, downsampling, манипулирование соотношением сторон — просто забудьте о проблемах с памятью и займитесь делом
ImageLoading
А также аутентификация через кучу ресурсов от фейсбука до твитера. Работа с локейшенами. Куча утилит для дебага, парсинг XML и так далее
великолепная документация с кучей примеров
Но это все слова, давайте попробуем в деле. Создаем пустой проект со следующими зависимостями:

Начнем с объявления AQuery, проверяем что все подключилось:
Теперь добавим карточки, и проверим как все работает:
Адаптер
Теперь начинаем магию с AQuery — дергаем rss и парсим его:
просто пишем aq.ajax(url, XmlDom.class, this, «onRequest») — AQuery сделает все остальное
В класс AjaxStatus приходит детальная информация о результатах выполнения запроса + в AQuery встроен простенький парсер XML. Нет необходимости беспокоиться о наличии активити на момент завершения запроса, AQuery сделает это за нас. Плюс модуль HTTP запросов гораздо гибче чем в примере выше, можно кастомизировать все, от хидеров до метода выполнения запроса. А если вам необходимо, например, закешировать запрос — просто добавляете параметр fileCache=true и время, на которое запрос должен быть закеширован. Есть функционал для инвалидации кеша в случае ошибки, например, и так далее.
Мы же пока вернемся к адаптеру, и обогатим rss поток функционалом отображения картинок. Тем более что с AQuery это не просто, а очень просто:
Буквально одной строкой мы отдаунскейлили картинку, включили кеширование её в памяти и отскейлили до нужного соотношения сторон, попутно включив анимацию отображения.
Если заглянуть в код библиотеки, то можно увидеть что вся логика работы с изображениями построена на weakReference, применяется вытесняющий LRU кэш, скейлинг производится с использованием оптимизированных методов inSampleSize и так далее. Более того, можно вручную управлять параметрами кеширования от размеров кеша под различные тип�� картинок (маленькие, большие, средние) до методики кеширования и количества картинок, одновременно хранящихся в кеше.
Пример конфига с отключенным кешированием на файловой системе (фрагмент application)
В данном случае картинки не будут скачиваться во временный файл, что может быть удобно при единовременной загрузке сотен изображений.
А вот так выглядит принудительная очистка кэша, например, при перезагрузке страницы:
А в нашем примере, собственно, осталось добавить обновление страницы:
Ну и расширим список фидов:
В результате получилось приложение для чтения 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
Но писать какой-то странный псевдокод в духе hello world скучно и неинтересно и поэтому мы решили сделать какое-нибудь небольшое, но полезное приложение. Недавно от Хабра отделился проект Мегамозг и в комментариях к новости высказывали предложение объединить RSS поток со всех ресурсов. Этим мы и займемся.
В конце получится такой прототип приложения IT News (rss с хабра, гиктаймс, мегамозга и с силиконруса/роем упорядоченные по дате):

Ссылки для торопыжек:
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, проверяем что все подключилось:
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 — дергаем 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
