- PVSM.RU - https://www.pvsm.ru -
Код проекта должен быть разделён на независимые модули, работающие друг с другом как хорошо смазанный механизм — фото Честера Альвареза [1].
Экосистема средств разработки под Android развивается очень быстро. Каждую неделю кто-то создаёт новые инструменты, обновляет существующие библиотеки, пишет новые статьи, или выступает с докладами. Если вы уедете в отпуск на месяц, то к моменту вашего возвращения уже будет опубликована свежая версия Support Library и/или Google Play Services.
Я занимаюсь разработкой Android-приложений в компании ribot [2] в течение последних трёх лет, и всё это время и архитектура наших приложений, и используемые нами технологии, постоянно развивались и улучшались. Эта статья проведёт вас путём, пройденным нами, показав вынесенные нами уроки, совершенные нами ошибки, и рассуждения, которые привели ко всем этим архитектурным изменениям.
В далёком 2012-м структура наших проектов выглядела очень просто. У нас не было никаких библиотек для работы с сетью, и AsyncTask
всё ещё был нашим другом. Приведённая ниже диаграмма показывает примерную архитектуру тех решений:
Код был разделён на два уровня: уровень данных (data layer), который отвечал за получение/сохранение данных, получаемых как через REST API, так и через различные локальные хранилища, и уровень представления (view layer), отвечающий за обработку и отображение данных.
APIProvider
предоставляет методы, позволяющие активити и фрагментам взаимодействовать с REST API. Эти методы используют URLConnection
и AsyncTask
, чтобы выполнить запрос в фоновом потоке, а потом доставляют результаты в активити через функции обратного вызова. Аналогично работает и CacheProvider
: есть методы, которые достают данные из SharedPreferences
или SQLite, и есть функции обратного вызова, которые возвращают результаты.
Главная проблема такого подхода состоит в том, что уровень представления имеет слишком ответственности. Давайте представим простой сценарий, в котором приложение должно загрузить список постов из блога, закешировать их в SQLite, а потом отобразить в ListView
. Activity
должна сделать следующее:
APIProvider#loadPosts(Callback)
.onSuccess()
в переданном Callback
'е, и потом вызвать CacheProvider#savePosts(Callback)
.onSuccess()
в переданном Callback
'е, и потом отобразить данные в ListView
.APIProvider
, так и в CacheProvider
.
И это ещё простой пример. В реальной жизни может случиться так, что API вернёт данные не в том виде, в котором их ожидает наш уровень представления, а значит Activity
должна будет как-то трансформировать и/или отфильтровать данные прежде, чем сможет с ними работать. Или, например, loadPosts()
будет принимать аргумент, который нужно откуда-то получить (например, адрес электронной почты, который мы запросим через Play Services SDK). Наверняка SDK будет возвращать адрес асинхронно, через функцию обратного вызова, а значит у нас теперь есть три уровня вложенности функций обратного вызова. Если мы продолжим наворачивать всё больше и больше сложности, то в итоге получим то, что называется callback hell.
Просуммируем:
Мы использовали описанный выше подход на протяжении двух лет. В течение этого времени мы внесли несколько изменений, смягчивших боль и страдания от описанных проблем. Например, мы добавили несколько вспомогательных классов, и вынесли в них часть логики, чтобы разгрузить активити и фрагменты, а также мы начали использовать Volley [3] в APIProvider
. Несмотря на эти изменения, код всё так же был трудно тестируемым, и callback-hell периодически прорывался то тут, то там.
Ситуация начала меняться в 2014-м году, когда мы прочли несколько статей по RxJava [4]. Мы попробовали её на нескольких пробных проектах, и осознали, что решение проблемы вложенных функций обратного вызова, похоже, найдено. Если вы не знакомы с реактивным программированием, то рекомендуем прочесть вот это [5] введение. Если коротко, RxJava позволяет вам управлять вашими данными через асинхронные потоки (прим. переводчика: в данном случае имеются в виду потоки как streams, не путать с threads — потоками выполнения), и предоставляет множество операторов [6], которые можно применять к потокам, чтобы трансформировать, фильтровать, или же комбинировать данные так, как вам нужно.
Приняв во внимание все шишки, которые мы набили за два прошедших года, мы начали продумывать архитектуру нового приложения, и пришли к следующему:
Код всё так же разделён на два уровня: уровень данных содержит DataManager
и набор классов-помощников, уровень представления состоит из классов Android SDK, таких как Activity
, Fragment
, ViewGroup
, и так далее.
Классы-помощники (третья колонка в диаграмме) имеют очень ограниченные области ответственности, и реализуют их в последовательной манере. Например, большинство проектов имеют классы для доступа к REST API, чтения данных из бд или взаимодействия с SDK от сторонних производителей. У разных приложений будет разный набор классов-помощников, но наиболее часто используемыми будут следующие:
PreferencesHelper
: работает с данными в SharedPreferences
.DatabaseHelper
: работает с SQLite.
Многие публичные методы классов-помощников возвращают RxJava Observables
.
DataManager
является центральной частью новой архитектуры. Он широко использует операторы RxJava для того, чтобы комбинировать, фильтровать и трансформировать данные, полученные от помощников. Задача DataManager
состоит в том, чтобы освободить активити и фрагменты от работы по «причёсыванию» данных — он будет производить все нужные трансформации внутри себя и отдавать наружу данные, готовые к отображению.
Приведённый ниже код показывает, как может выглядеть какой-нибудь метод из DataManager
. Работает он следующим образом:
DatabaseHelper
.public Observable<Post> loadTodayPosts() {
return mRetrofitService.loadPosts()
.concatMap(new Func1<List<Post>, Observable<Post>>() {
@Override
public Observable<Post> call(List<Post> apiPosts) {
return mDatabaseHelper.savePosts(apiPosts);
}
})
.filter(new Func1<Post, Boolean>() {
@Override
public Boolean call(Post post) {
return isToday(post.date);
}
});
}
Компоненты уровня представления будут просто вызывать этот метод и подписываться на возвращенный им Observable
. Как только подписка завершится, посты, возвращённые полученным Observable
могут быть добавлены в Adapter
, чтобы отобразить их в RecyclerView
или чём-то подобном.
Последний элемент этой архитектуры это event bus. Event bus позволяет нам запускать сообщения о неких событиях, происходящих на уровне данных, а компоненты, находящиеся на уровне представления, могут подписываться на эти сообщения. Например, метод signOut()
в DataManager
может запустить сообщение, оповещающее о том, что соответствующий Observable
завершил свою работу, и тогда активити, подписанные на это событие, могут перерисовать свой интерфейс, чтобы показать, что пользователь вышел из системы.
Observables
и операторы из RxJava избавляют нас от вложенных функций обратного вызова.
DataManager
берёт на себя работу, которая ранее выполнялась на уровне представления, разгружая таким образом активити и фрагменты.DataManager
и классы-помощники делает юнит-тестирование активити и фрагментов более простым.DataManager
как единственной точки взаимодействия с уровнем данных делает всю архитектуру более дружественной к тестированию. Классы-помощники, или DataManager
, могут быть легко подменены на специальные заглушки.DataManager
может стать слишком раздутым, и поддержка его существенно затруднится.В течение прошлого года в Android-сообществе начали набирать популярность отдельные архитектурные шаблоны, так как MVP [8], или MVVM [9]. После исследования этих шаблонов в тестовом проекте [10], а также отдельной статье [11], мы обнаружили, что MVP может привнести значимые изменения в архитектуру наших проектов. Так как мы уже разделили код на два уровня (данных и представления), введение MVP выглядело натурально. Нам просто нужно было добавить новый уровень presenter'ов, и перенести в него часть кода из представлений.
Уровень данных остаётся неизменным, но теперь он называется моделью, чтобы соответствовать имени соответствующего уровня из MVP.
Presenter'ы отвечают за загрузку данных из модели и вызов соответствующих методов на уровне представления, когда данные загружены. Presenter'ы подписываются на Observables
, возвращаемые DataManager
. Следовательно, они должны работать с такими сущностями как подписки [12] и планировщики [13]. Более того, они могут анализировать возникающие ошибки, или применять дополнительные операторы к потокам данных, если необходимо. Например, если нам нужно отфильтровать некоторые данные, и этот фильтр скорее всего нигде больше использоваться не будет, есть смысл вынести этот фильтр на уровень presenter'а, а не DataManager
.
Ниже представлен один из методов, которые могут находиться на уровне presenter'а. Тут происходит подписка на Observable
, возвращаемый методом dataManager.loadTodayPosts()
, который мы определили в предыдущем разделе.
public void loadTodayPosts() {
mMvpView.showProgressIndicator(true);
mSubscription = mDataManager.loadTodayPosts().toList()
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Subscriber<List<Post>>() {
@Override
public void onCompleted() {
mMvpView.showProgressIndicator(false);
}
@Override
public void onError(Throwable e) {
mMvpView.showProgressIndicator(false);
mMvpView.showError();
}
@Override
public void onNext(List<Post> postsList) {
mMvpView.showPosts(postsList);
}
});
}
mMvpView
— это компонент уровня представления, с которым работает presenter. Обычно это будет Activity
, Fragment
или ViewGroup
.
Как и в предыдущей архитектуре, уровень представления содержит стандартные компоненты из Android SDK. Разница в том, что теперь эти компоненты не подписываются напрямую на Observables
. Вместо этого они имплементируют интерфейс MvpView
, и предоставляют список внятных и понятных методов, таких как showError()
или showProgressIndicator()
. Компоненты уровня представления отвечают также за обработку взаимодействия с пользователем (например, события нажатия), и вызов соответствующих методов в presenter'е. Например, если у нас есть кнопка, которая загружает список постов, наша Activity
должна будет вызвать в OnClickListener
'е метод presenter.loadTodayPosts()
.
Если вы хотите взглянуть на работающий пример, то можно заглянуть в наш репозиторий на Github [14]. Ну а если захотелось большего, то можете посмотреть наши рекомендации по построению архитектуры [15].
DataManager
становится слишком раздутым, мы всегда можем перенести часть кода в presenter'ы.DataManager
всё так же может стать слишком раздутым. Пока что это не произошло, но мы не зарекаемся от подобного развития событий.Я надеюсь, вам понравилась моя статья, и вы нашли её полезной. Если так, не забудьте нажать на кнопку Recommend (прим. переводчика: перейдите на оригинальную статью, и нажмите на кнопку-сердечко в конце статьи). Также, я хотел бы выслушать ваши мысли по поводу нашего текущего подхода.
Автор: artemgapchenko
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/115009
Ссылки в тексте:
[1] Честера Альвареза: https://unsplash.com/chesteralvarez
[2] ribot: http://ribot.co.uk/us/
[3] Volley: http://developer.android.com/training/volley/index.html
[4] RxJava: http://reactivex.io/
[5] вот это: https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
[6] операторов: http://reactivex.io/documentation/operators.html
[7] Retrofit: https://github.com/square/retrofit
[8] MVP: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter
[9] MVVM: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel
[10] тестовом проекте: https://github.com/ivacf/archi
[11] отдельной статье: https://medium.com/ribot-labs/approaching-android-with-mvvm-8ceec02d5442#.x98dqfu6m
[12] подписки: http://reactivex.io/RxJava/javadoc/rx/Subscription.html
[13] планировщики: http://reactivex.io/documentation/scheduler.html
[14] наш репозиторий на Github: https://github.com/ribot/android-boilerplate
[15] рекомендации по построению архитектуры: https://github.com/ribot/android-guidelines/blob/master/architecture_guidelines/android_architecture.md
[16] Источник: https://habrahabr.ru/post/278815/
Нажмите здесь для печати.