- Построение Android приложений шаг за шагом, часть первая
- Введение
- Шаг 1. Простая архитектура
- Model
- Presenter
- Часть 2. Усложненная архитектура
- Model
- Presenter
- Заключение или to be continued…
- Архитектура Android-приложений… Правильный путь?
- Приступая к работе
- Наш сценарий
- Android-архитектура
- Presentation Layer (Слой представления)
- Domain Layer (Слой бизнес-логики)
- Data Layer (Слой данных)
- Обработка ошибок
- Тестирование
- Покажите мне код
- Заключение
Построение Android приложений шаг за шагом, часть первая
В этой статье мы поговорим о проектировании архитектуры и создании мобильного приложения на основе паттерна MVP с использованием RxJava и Retrofit. Тема получилась довольно большой, поэтому подаваться будет отдельными порциями: в первой мы проектируем и создаем приложение, во второй занимаемся DI с помощью Dagger 2 и пишем тесты unit тесты, в третьей дописываем интеграционные и функциональные тесты, а также размышляем о TDD в реалиях Android разработки.
Содержание:
- Введение
- Шаг 1. Простая архитектура
- Разделение по слоям, MVP
- Model
- Retrofit
- POJO
- Presenter
- View
- Шаг 2. Усложненная архитектура
- Retrolambda
- Разные модели данных для разных слоев
- Model
- Presenter
- View
- Заключение или to be continued
Введение
Для лучшего понимания и последовательного усложнения кода, разделим проектирование на два этапа: примитивная (минимально жизнеспособная) и обычная архитектура. В примитивной обойдемся минимальным количество кода и файлов, потом улучшим этот код.
Все исходники вы можете найти на github. Бранчи в репозитории соответствуют шагам в статье: Step 1 Simple architecture — первый шаг, Step 2 Complex architecture — второй шаг.
Для примера попробуем получить список репозиториев для конкретного пользователя с помощью Github API.
В нашем приложении мы будем использовать Rx, поэтому для понимания статьи необходимо иметь общее представление об этой технологии.Рекомендуем почитать серию публикаций Грокаем RxJava, эти материалы дадут хорошее представление о реактивном программировании.
Шаг 1. Простая архитектура
Разделение по слоям, MVP
При проектировании архитектуры будем придерживаться паттерна MVP. Более подробно можно почитать тут:
https://ru.wikipedia.org/wiki/Model-View-Presenter
http://habrahabr.ru/post/131446/
Разделим всю нашу программу на 3 основных слоя:
Model — тут получаем и храним данные. На выходе получаем Observable.
Presenter — в данном слое хранится вся логика приложения. Получаем Observable, подписываемся на него и передаем результат во view.
View — слой отображения, содержит все view элементы, активити, фрагменты и прочее.
Model
Слой данных должен отдавать нам Observable
>, напишем интерфейс:
Retrofit
Для упрощения работы с сетью используем Retrofit. Retrofit – библиотека для работы с REST API, она возьмет на себя всю работу с сетью, нам остается только описать запросы с помощью интерфейса и аннотаций.
Про Retrofit в рунете достаточно много материалов (http://www.pvsm.ru/android/58484, http://tttzof351.blogspot.ru/2014/01/java-retrofit.html).
Основное отличие второй версии от первой в том, что у нас пропала разница между синхронными и асинхронными методами. Теперь мы получаем Call у которого можем вызвать execute() для синхронного или execute(callback) для асинхронного запроса. Также появилась долгожданная возможность отменять запросы: call.cancel(). Как и раньше, можно получать Observable , правда теперь с помощью специального плагина
Интерфейс для получения данных о репозиториях:
Работа с данными, POJO
Retrofit (и GSON внутри него) работают с POJO (Plain Old Java Object). Это значит, что для получения обьекта из JSON вида:
Нам понадобится класс User, в который GSON запишет значения:
Руками генерировать такие классы естественно не нужно, для этого есть специальные генераторы, например: www.jsonschema2pojo.org.
Скармливаем ему наш JSON, выбираем:
Source type: JSON
Annotation style: Gson
Include getters and setters
и получаем код наших файлов. Можно скачать как zip или jar и положить в наш проект. Для репозитория получилось 3 обьекта: Owner, Permissions, Repo.
Presenter
Презентер знает что загрузить, как показать, что делать в случае ошибки и прочее. Т.е отделяет логику от представления. View в таком случае получается максимально «легкой». Наш презентер должен уметь обрабатывать нажатие кнопки поиска, инициализировать загрузку, отдавать данные и отписываться в случае остановки Activity.
View реализуем как Activity, которое умеет отображать полученные данные, показывать ошибку, уведомлять о пустом списке и выдавать имя пользователя по запросу от презентера. Интерфейс:
В результате у нас получилось простое приложение, которое разделено по слоям.
Некоторые вещи требуют улучшения, однако, общая идея ясна. Теперь усложним нашу задачу, добавив новую функциональность.
Часть 2. Усложненная архитектура
Добавим новую функциональность в наше приложение, отображение информации о репозитории. Будем показывать списки branches и contributors, они получаются разными запросами у API.
Retrolambda
Работа с Rx без лямбд — это боль, необходимость каждый раз писать анонимные классы быстро утомляет. Android не поддерживает Java 8 и лямбды, но на помощь нам приходит Retrolambda (https://github.com/evant/gradle-retrolambda). Подробнее о лямбда-выражениях: http://habrahabr.ru/post/224593/
Разные модели данных для разных слоев.
Как видно, мы на всех трех слоях работаем с одним и тем же объектом данных Repo. Такой подход хорош для простых приложений, однако в реальной жизни мы всегда можем столкнутся со сменой API, необходимостью изменять объект или чем-то другим. Если над проектом работают несколько человек, то существует риск изменения класса в интересах другого слоя.
Поэтому зачастую применяется подход: один слой = один формат данных. И если изменятся какие-то поля в модели, это никак не повлияет на View слой. Мы можем производить любые изменения в Presenter слое, но во View мы отдаем строго определенный объект (класс). Благодаря этому достигается независимость слоев от моделей данных, у каждого слоя своя модель. При изменении какой либо модели, нам нужно будет переписать маппер и не трогать сам слой. Это похоже на контрактное программирование, когда мы точно знаем какой объект придет в наш слой и какой мы должны отдать дальше, тем самым защищая себя и коллег от непредсказуемых последствий.
В нашем примере нам вполне хватит двух типов данных, DTO — Data Transfer Object (полностью копирует JSON объект) и View Object (адаптированный объект для отображения). Если будет более сложное приложение, возможно понадобятся Business Object (для бизнес процессов) или например Data Base Object (для хранения сложных объектов в базе данных)
Переименуем Repo в RepositoryDTO, создадим новый класс Repository и напишем маппер, реализующий интерфейс Func1
>, List >
(перевод из List в List )
Model
Мы ввели разные модели данных для разных слоев, интерфейс Model теперь отдает DTO объекты, в остальном все также.
Presenter
В Presenter-слое нам необходим общий класс. Презентер может выполнять самые разные функции, это может быть простой презентер «загрузи-покажи», может быть список с необходимостью подгрузки элементов, может быть карта, где мы будем запрашивать объекты на участке, а также множество других сущностей. Но всех их объединяет необходимость отписываться от Observable во избежание утечек памяти. Остальное зависит от типа презентера.
Если мы используем несколько Observable, то нам необходимо отписываться от всех разом в onStop. Для этого возможно использование CompositeSubscription: добавляем туда все наши подписки и отписываемся по команде.
Также добавим в презентеры сохранение состояния. Для этого создаем и реализуем методы onCreate(Bundle savedInstanceState) и onSaveInstanceState(Bundle outState). Для перевода DTO в VO используем мапперы.
Будем использовать активити для управления фрагментами. Для каждой сущности свой фрагмент, который наследуется от базового фрагмента. Базовый фрагмент используя интерфейс базового презентера отписывается в onStop().
Также обратите внимание на восстановление состояния, вся логика переехала в презентер — View должно быть максимально простым.
Общая схема приложения на втором шаге (кликабельно):
Заключение или to be continued…
В результате у нас получилось работающее приложение с соблюдением всех необходимых уровней абстракции и четким разделением ответственности по компонентам (исходники). Такой код проще поддерживать и дополнять, над ним может работать команда разработчиков. Но одним из главных преимуществ является достаточно легкое тестирование. В следующей статье рассмотрим внедрение Dagger 2, покроем тестами существующий код и напишем новую функциональность, следуя принципам TDD.
Источник
Архитектура Android-приложений… Правильный путь?
От переводчика: Некоторые термины, которые использует автор, не имеют общепринятого перевода (ну, или я его не знаю:), поэтому я решил оставить большинство на языке оригинала — они всё равно понятны и для тех, кто пишет под android, но не знает английский.
Куда писать об ошибках и неточностях, вы знаете.
За последние несколько месяцев, а также после дискуссий на Tuenti с коллегами вроде @pedro_g_s и @flipper83 (кстати говоря, 2 крутых Android-разработчика), я решил, что имеет смысл написать заметку о проектировании Android-приложений.
Цель поста — немного рассказать о подходе к проектированию, который я продвигал в последние несколько месяцев, и также поделиться всем тем, что я узнал во время исследования и реализации этого подхода.
Приступая к работе
Мы знаем, что написание качественного программного обеспечения — сложное и многогранное занятие: программа должна не только соответствовать установленным требованиям, но и быть надёжной, удобной в сопровождении, тестируемой и достаточно гибкой для добавления или изменения функций. И здесь появляется понятие “стройной архитектуры”, которое неплохо бы держать в уме при разработке любого приложения.
Идея проста: стройная архитектура основывается на группе методов, реализующих системы, которые являются:
- Независимыми от фреймворков.
- Тестируемыми.
- Независимыми от UI.
- Независимыми от БД.
- Независимыми от любой внешней службы.
Кругов необязательно должно быть 4 (как на диаграмме), это просто схема; важно учитывать Правило Зависимостей: код должен иметь зависимости только во внутренние круги и не должен иметь никакого понятия, что происходит во внешних кругах.
Вот небольшой словарь терминов, необходимых для лучшего понимания данного подхода:
- Entities (Сущности): это бизнес-логика приложения.
- Use Cases (Методы использования): эти методы организуют поток данных в Entities и из них. Также их называют Interactors (Посредниками).
- Interface Adapters (Интерфейс-Адаптеры): этот набор адаптеров преобразовывает данные из формата, удобного для Методов использования и Сущностей. К этим адаптерами принадлежат Presenter-ы и Controller-ы.
- Frameworks and Drivers: место скопления деталей: UI, инструменты, фреймворки, БД и т. д.
Для лучшего понимания обратитесь к данной статье или к данному видео.
Наш сценарий
Я начал с простого, чтобы понять, как обстоят дела: создал простенькое приложение, которое отображает список друзей, который получает с облака, а при клике по другу отображает более детальную информацию о нём на новом экране.
Я оставлю здесь видео, чтобы вы поняли, о чем идёт речь:
Android-архитектура
Наша цель — разделение задач таким образом, чтобы бизнес-логика ничего не знала о внешнем мире, так, чтобы можно было тестировать её без никаких зависимостей и внешних элементов.
Для достижения этого я предлагаю разбить проект на 3 слоя, каждый из которых имеет свою цель и может работать независимо от остальных.
Стоит отметить, что каждый слой использует свою модель данных, таким образом можно достигнуть необходимой независимости (вы можете увидеть в коде, для выполнения трансформации данных необходим преобразователь данных (data mapper). Это вынужденная плата за то, чтобы модели внутри приложения не пересекались). Вот схема, как это выглядит:
Примечание: я не использовал внешних библиотек (кроме gson для парсинга json и junit, mockito, robolectric и espresso для тестирования) для того, чтобы пример был более наглядным. В любом случае, не стесняйтесь использовать ORM для хранения информации или любой dependency injection framework, да и вообще инструменты или библиотеки, которые делают вашу жизнь легче (помните: изобретать велосипед заново — не лучшая идея).
Presentation Layer (Слой представления)
Здесь логика связывается с Views (Представлениями) и происходят анимации. Это не что иное, как Model View Presenter (то есть, MVP), но вы можете использовать любой другой паттерн вроде MVC или MVVM. Я не буду вдаваться в детали, но fragments и activities — это всего лишь views, там нету никакой логики кроме логики UI и рендеринга этого самого отображения. Presenter-ы на этом слое связываются с interactors (посредниками), что предполагает работу в новом потоке (не в UI-потоке), и передачи через коллбэки информации, которая будет отображена во view.
Если вы хотите увидеть крутой пример эффективного Android UI, который использует MVP и MVVM, взгляните на пример реализации от моего друга Pedro Gómez.
Domain Layer (Слой бизнес-логики)
Вся логика реализована в этом слое. Рассматривая проект, вы увидите здесь реализацию interactor-ов (Use Cases — методы использования).
Этот слой — модуль на чистой джаве без никаких Android-зависимостей. Все внешние компоненты используют интерфейсы для связи с бизнес-объектами.
Data Layer (Слой данных)
Все данные, необходимые для приложения, поставляются из этого слоя через реализацию UserRepository (интерфейс находится в domain layer — слое бизнес-логики), который использует Repository Pattern со стратегией, которая, через фабрику, выбирает различные источники данных, в зависимости от определенных условий.
Например, для получения конкретного пользователя по id источником данных выбирается дисковый кэш, если пользователь уже загружен в него, в противном случае облаку отправляется запрос на получение данных для дальнейшего сохранения в тот же кэш.
Идея всего этого заключается в том, что происхождение данных является понятным для клиента, которого не волнует, поступают данные из памяти, кэша или облака, ему важно только то, что данные будут получены и доступны.
Примечание: что касается кода, помня, что код — это учебный пример, я реализовал очень простой, даже примитивный кэш, используя хранения shared preferences. Помните: НЕ СТОИТ ИЗОБРЕТАТЬ ВЕЛОСИПЕД, если существуют библиотеки, хорошо решающие поставленную задачу.
Обработка ошибок
Это большая тема, в которой всегда есть, что обсудить (и здесь автор предлагает делиться своими решениями). Что до моей реализации, я использовал коллбэки, которые, на случай, если что-то случается, скажем, в хранилище данных, имеют 2 метода: onResponse() и onError(). Последний инкапсулирует исключения во wrapper class (класс-обертку) под названием “ErrorBundle”: Такой подход сопряжен с некоторыми трудностями, потому что бывают цепочки обратных вызовов один за другим, пока ошибка не выходит на presentation layer, чтобы отобразиться. Читаемость кода из-за этого может быть немного нарушена.
С другой стороны, я реализовал систему event bus, которая бросает события, если что-то не так, но такое решение похоже на использование GOTO, и, по-моему, иногда, если не управлять событиями предельно внимательно, можно заблудиться в цепи событий, особенно когда их одновременно бросается несколько.
Тестирование
Что касается тестирования, я применил несколько решений, в зависимости от слоя.
- Presentation Layer: существующий android инструментал и espresso для интеграции и функционального тестирования.
- Domain Layer: JUnit + mockito использовались для юнит-тестов.
- Data Layer: Robolectric (так как этот слой имеет android-зависимости) + junit + для интеграции и юнит-тестов.
Покажите мне код
Думаю, в этом месте вам интересно посмотреть на код. Что ж, вот ссылка на github, где можно найти, что же у меня получилось. Что стоит упомянуть о структуре папок так это то, что разные слои представлены разными модулями:
- presentation: это android-модуль для presentation layer.
- domain: java-модуль без android-зависимостей.
- data: android-модуль, откуда поступают все данные.
- data-test: тесты для data layer. Из-за определённых ограничений, при использовании Robolectric мне пришлось использовать отдельный java-модуль.
Заключение
Дядя Боб сказал: “Архитектура — это намерения, а не фреймворки”, и я полностью с ним согласен. Конечно, есть разные способы реализовать какую-либо вещь, и я уверен, что вы, как и я, каждый день сталкиваетесь с трудностями в этой области, но используя данные приёмы, вы сможете быть уверены, что ваше приложение будет:
- Простым в поддержке.
- Простым в тестировании.
- Составлять единое целое,
- Будучи разделённым.
В заключение я настоятельно рекомендую вам попробовать эти методы, посмотреть на результат и поделиться своим опытом как в отношении этого, так любого другого подхода, который, по вашим наблюдениям, работает лучше: мы уверены, что постоянное совершенствование всегда полезно и положительно.
Источник