...

четверг, 9 января 2014 г.

Еще раз об архитектуре Android приложения или джентльменский набор библиотек

Вот надумал написать обзор библиотек с помощью которых легко и удобно писать приложения под Android.

Список вырисовывается такой:

Если заинтересованны прошу под кат.



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

Естественно, все продиктовано архитектурой приложения. Исходя из моего опыта, почти всегда, надо иметь следующую структуру приложения:



Как видно, приложение делится на 3 слоя — «мордочка», хранилище данных и сервис для асинхронных команд, где почти всегда скрыта логика.

Я не хочу разводить холивар и спорить об архитектуре, я просто описываю то как делаю это я и как делают мои коллеги.


Пару слов почему так




Контент провайдер очень мощная штука с уведомлениями об изменениях данных и это все из коробки. Юзаем в основном как обвертку над базой. Способа как туда правильно впихнуть загрузку данных из инета не нашел.

Сервис асинхронных команд — из названия все ясно, выполняет действия асинхронно(в другом потоке). Практически все действия должны быть асинхронны — запись в базу, походы в инет, да и подсчеты.

Почему «асинхронных команд»? Тут тоже все просто — каждое действие — законченная команда. Которая знает с какими параметрами запуститься, что с ними делать и оповещением о своем завершении.

Вот тут evilduck уже все детально описал.


«Мордочка» — набор активити/фрагментов для отображения данных. Хочу заметить, что вся загрузка данных из хранилища должна быть асинхронная(ни каких походов в базу из UI даже за одним числом). И тут нам на помощь приходит лоадер менеджер — тоже фича из коробки. Стоит смотреть в сторону CursorLoader


Велосипед




Наверное, каждый программист делал свой велосипед. Польза от них тоже есть — вы начинаете понимаете все подводные камни, что и зачем нужно. Но писать каждый раз велосипед — не хорошо. А прикатить его из другого проекта нельзя т.к. код продан и является собственностью заказчика.

Конечно ваш код можно оформить в либу, опубликовать ее, в договоре с заказчиком написать, что юзаем либу. Но написать либу на все случаи жизни не так просто, тем более зачем делать еще один велосипед если уже есть, а баги уже найдены и пофикшены. Так в конце концов произошло и со мной.

Я не то что противник рефлекшена, но предпочитаю либы которые генерят код. Его удобно дебажить и всегда можно посмотреть, что там творится.

Ко всему прочему мне нравятся аннотации. Вся моя подборка библиотек практически соответствует этому принципу. Начнем…


Groundy




Вот эта библиотечка реализует command service.


  • Команды можно кенселить

  • Любители AsyncTask'ов не заметят перехода

  • Есть поддержка калбеков и они очень просты в использовании




Я предпочитаю писать статический метод запуска команды и типизировать калбек.

Хотя калбеком может быть любой object я предпочитаю типизировать его. Это поможет компилятору помогать нам.

И оградит вас от соблазна навесить калбеки на паблик методы активити, тем самым нарушив один из столпов ООП — инкапсуляцию :)

По тем самым соображениям и нужен статический метод запуска.

public class LoginCommand extends GroundyTask{
private static final String ARG_PASSWORD = "arg_password";
private static final String ARG_USER = "arg_username";

@Override
protected TaskResult doInBackground() {
String userName = getStringArg(ARG_USER);
String password = getStringArg(ARG_PASSWORD);
//do something
return succeeded();
}

public static void start(Context context, BaseLoginCommandCallback callback, String login, String password) {
Groundy.create(LoginCommand.class)
.arg(ARG_USER, login)
.arg(ARG_PASSWORD, password)
.callback(callback)
.queueUsing(context);
}

public static abstract class BaseLoginCommandCallback{

@OnSuccess(LoginCommand.class)
public void handleSuccess(){
onLoginSuccess();
}

@OnFailure(LoginCommand.class)
public void handleFailure(){
onLoginError();
}

protected abstract void onLoginSuccess();

protected abstract void onLoginError();
}
}


Retrofit




Очень простой инструмент для вызова REST сервисов, как хорошо написанных так и не очень. Код правда не генерит, но очень прост.

Я смотрел в сторону Spring Android, но как-то тяжеловат он.

public interface ServicesFootballua {

String API_URL = "http://services.football.ua/api";

@GET("/News/GetArchive")
NewsArchive getNewsArchive(@Query("pageId") long pageId,
@Query("count") long count,
@Query("datePublish") String date);
}




ну и вот так юзаем

private static ServicesFootballua API = new RestAdapter.Builder()
.setServer(ServicesFootballua.API_URL)
.build()
.create(ServicesFootballua.class);
...................................
archive = API.getNewsArchive(PAGE_ID, COUNT, dateFormat.format(getTodayTime()));


можно подставить свой конвертер, http клиент и кучу всего прочего.


AnnotatedSQL




Генерит базу и контент провайдер по аннотациям.

Моя поделка, меня устраивает полностью. Пару статей есть на хабре — тут и тут.

Недавно с пинка evilduck опубликовался в maven central

Смотрел на ORMLite, но мне кажется не подходит оно для андроид. Обычно нам не нужно вытягивать прям все и вся. Обычный sql и вьшки решают почти все.


Android Annotations




Очень долго присматривался к этой либе и недавно решился заюзать ее в продакшене — понравилось, несмотря на то, что надо юзать нагенеренные классы.

Мощнейший инструмент, главное не юзать Background ну или включать голову.

Хороший плюс — можно почти безболезненно выпилить.

Смотрел на AQuery и Dagger, но имхо Android Annotations — уже имеет все это.

Единственной минус — иногда тяжело искать ошибку «почему не компилируется?».


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


Например фрагмент AlertDialogFragment будет иметь метод



public static void show(FragmentActivity activity,
DialogType type, int titleId, String msg, int positiveTitleId,
OnDialogClickListener positiveListener) {
DialogUtil.show(activity, DIALOG_NAME,
AlertDialogFragment_.builder()
.titleId(titleId)
.errorMsg(msg)
.positiveButtonTitleId(positiveTitleId)
.dialogType(type).build()
).setOnClickListener(positiveListener);
}




а везде в коде вызов будет типа

AlertDialogFragment.show(BaseActivity.this,
DialogType.CONFIRM,
R.string.some_title,
getString(R.string.some_message),
R.string.btn_edit,
new OnDialogClickListener() {...............}


и никто не знает о существовании AlertDialogFragment_


Android db-commons




Совсем недавно нашел эту замечательную либу. Т.к. мы юзаем лоадеры везде и всюду, обычно результат это Cursor. Всегда можно заюзать CursorAdapter и отобразить то, что надо.

Но вот эта либка предлагает нам юзать список(List), но над курсором, а со списком всеми любимый ArrayAdapter.

Вот такой симбиоз — вы как бы видите List и юзаете ArrayAdapter, но по факту это курсор и курсор адаптер. Настоящая «уличная магия» :)

Ребята не поленились и написали такой себе LazyList с небольшим кешем внутри, все как и положено — внутри LruCache.


Для того что бы получить List вместо Cursor надо написать функцию(transform) конвертации строки курсора в объект и вы получите лоадер который вернет не Cursor, а List



return CursorLoaderBuilder.forUri(URI_ITEMS)
.projection(ItemConverter.PROJECTION)
.where(ItemTable.ACTIVE_STATUS + " = ?", 1)
.where(ItemTable.DESCRIPTION + " like ?", "%" + searchText + "%")
.transform(new ItemConverter()).build(getActivity());


Но как мы знаем иногда надо прочитать курсор в какой-то объект, например надо посчитать что-то, для этого у ребят есть метод wrap



public Loader<Integer> onCreateLoader(int i, Bundle bundle) {
return CursorLoaderBuilder
.forUri(ITEMS_URI)
.projection("count(" + ItemTable.GUID + ")")
.where(ItemTable.ACTIVE_STATUS + " = ?", 1)
.where(ItemTable.STOCK_TRACKING + " = ?", 1)
.where(ItemTable.TMP_AVAILABLE_QTY + " <= " + ItemTable.RECOMMENDED_QTY)
.wrap(new Function<Cursor, Integer>() {
@Override
public Integer apply(Cursor c) {
if (c.moveToFirst()) {
return c.getInt(0);
}
return 0;
}
}).build(DashboardActivity.this);
}




вот такой нехитрый способ.

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

При этом в метод wrap все еще выполняется в другом потоке. так что вы можете еще сходить в БД за дополнительными данными.

Как я и сказал это иногда надо.

Окончание




Понятно, что для самого UI используется еще кучка разных либ(в основном компоненты), но это уже зависит от фантазии дизайнера :)

Ну и вот такой кусочек билд скрипта для грейдл, что бы apt завелся(да-да уже есть спец плагин, но его еще не пробовал).

Ах, да — качаем android-db-commons-0.1.6.jar в папку libs



ext.androidAnnotationsVersion = '2.7.1';

configurations {
apt
}

dependencies {
compile fileTree(dir: 'libs', include: '*.jar')

compile 'com.google.guava:guava:13.0.1'

compile 'com.telly:groundy:1.3'
apt 'com.telly:groundy-compiler:1.3'

apt "com.googlecode.androidannotations:androidannotations:${androidAnnotationsVersion}"
compile "com.googlecode.androidannotations:androidannotations-api:${androidAnnotationsVersion}"

compile 'com.github.hamsterksu:android-annotatedsql-api:1.7.8'
apt 'com.github.hamsterksu:android-annotatedsql-processor:1.7.8'
}

android.applicationVariants.all { variant ->
aptOutput = file("${project.buildDir}/source/apt_generated/${variant.dirName}")

variant.javaCompile.doFirst {
aptOutput.mkdirs()
variant.javaCompile.options.compilerArgs += [
'-processorpath', configurations.apt.getAsPath(),
'-processor', 'com.annotatedsql.processor.provider.ProviderProcessor,com.annotatedsql.processor.sql.SQLProcessor,com.googlecode.androidannotations.AndroidAnnotationProcessor,com.telly.groundy.GroundyCodeGen',
'-AandroidManifestFile=' + variant.processResources.manifestFile,
'-s', aptOutput
]
}
}


Всем спасибо за внимание


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий