Нас постоянно спрашивают, почему мы используем библиотеку 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
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, манипулирование соотношением сторон — просто забудьте о проблемах с памятью и займитесь делом
А также аутентификация через кучу ресурсов от фейсбука до твитера. Работа с локейшенами. Куча утилит для дебага, парсинг 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
Автор: recompileme