Еще раз об архитектуре 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 		] 	} } 

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

ссылка на оригинал статьи http://habrahabr.ru/post/208504/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *