Pull to refresh

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

Reading time 6 min
Views 57K
Вот надумал написать обзор библиотек с помощью которых легко и удобно писать приложения под 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
		]
	}
}


Всем спасибо за внимание
Tags:
Hubs:
+55
Comments 82
Comments Comments 82

Articles