Приклад програми з використанням бібліотеки AQuery

Нас постійно запитують, чому ми використовуємо бібліотеку 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, перевіряємо що всі підключилося:

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.ua/rss/","http://siliconrus.com/feed/","http://habrahabr.ua/rss/","http://megamozg.ua/rss/","http://geektimes.ua/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

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.