- PVSM.RU - https://www.pvsm.ru -
От переводчика: — я давненько интересуюсь тем, как сделать код Android-приложений чище, и это, наверное, первая статья, после которой у меня не возникло мыслей: "Зачем вот это вот все?" и "Он вообще пробовал когда-то это использовать в жизни?" Поэтому решил перевести, может, еще кому-то будет полезно.
Написать Hello World всегда легко. Код выглядит просто и прямолинейно, и кажется, что SDK очень адаптирована под ваши нужды. Но если у вас есть опыт написания более сложных Android-приложений, вы знаете, что с рабочим кодом все не так. Можно провести часы за попыткой понять, почему ваша корзина покупок не обновляется после изменения ориентации телефона, если недоступен WiFi. Вы предполагаете, что решением проблемы, возможно, будет добавить ещё один if в 457-строчном методе onCreate() вашей активити — где-то между тем кодом, который исправляет падение на самсунгах с Android 4.1 на борту, и тем, который показывает купон на 5$ в день рождения пользователя. Что ж, есть способ получше.
Мы в Remind (прим. пер. — название компании, где работает автор) выкатываем новые функции каждые две недели, и для того чтобы поддерживать эту скорость и высокое качество продукта, нужен способ сохранять код простым, поддерживаемым, разделённым (прим. пер. — "decoupled", в смысле слабой связанности) и тестируемым. Использование архитектурного паттерна MVP позволяет нам делать это и сосредоточиваться на самой значимой части нашего кода — нашей бизнес-логике.
MVP [1], или Model-View-Presenter, это один из нескольких паттернов, который способствует разделению ответственности при реализации пользовательского интерфейса. В каждом из этих паттернов роли слоев слегка отличаются. Целью этой статьи не является описание отличий между паттернами, а показать, как это можно применить на андроиде (по аналогии с современными UI-фреймворках, такими как Rails [2] и iOS [3]), и как от этого выиграет ваше приложение.
Пример кода, который иллюстрирует большинство подходов, описанных далее, вы можете найти здесь:
https://github.com/remind101/android-arch-sample [4]
Разделение ответственности, которое подразумевается Android-фреймворком, выглядит так: Модель может быть любым POJO, Представление — это XML-разметка, а фрагмент (или изначально активити) выступает в роли Контроллера/Презентера. В теории это работает довольно неплохо, но как только ваше приложение разрастается, в Контроллере появляется много кода, относящегося к Представлению. Все потому, что не так много можно сделать с XML, так что вся привязка данных (дата-биндинг), анимации, обработка ввода и т. д. производится во фрагменте, наряду с бизнес-логикой.
Все становится ещё хуже, когда сложные элементы интерфейса размещаются в списках или гридах (прим. пер. — имеет в виду GridView/GridLayout, да и вообще "сеточные элементы"). Теперь на адаптер ложится ответственность не только хранить код представления и контроллера для всех этих элементов, но и управлять ими как коллекцией. Так как все эти элементы сильно связаны, их становится очень трудно поддерживать и ещё сложнее тестировать.
MVP предоставляет нам возможность выделить весь тот скучный низкоуровневый Android-код, который нужен для отображения нашего интерфейса и взаимодействия с ним, в Представление, а более высокоуровневую бизнес-логику того, что наше приложение должно делать, выселить в Презентер.
Для достижения этого на андроиде надо рассматривать активити или фрагмент как слой представления, и предоставить легковесный презентер для того, чтобы контролировать представление. Самое важное — определить ответственность каждого слоя, и стандартизировать интерфейс между ними. Вот общее описание разделения, которое весьма неплохо работает у нас:
Представление (активити или фрагмент) отвечает за:
Презентер отвечает за:
Вот пример того, каким может быть интерфейс между представлением и презентером:
interface MessageView {
// Методы представления должны звучать как указания, так как представление только вызывает инструкции у презентера
// Методы для обновления представления
void setMessageBody(String body);
void setAuthorName(String name);
void showTranslationButton(boolean shouldShow);
// Методы навигации
void goToUserProfile(User user);
}
interface MessagePresenter {
// Методы презентера в основном должны быть коллбеками, так как представление сообщает презентеру о событиях
// Методы событий жизненного цикла
void onStart();
// Методы входных событий
void onAuthorClicked();
void onThreeFingersSwipe();
}
Есть пара интересных моментов, которые стоит рассмотреть в связи с этим интерфейсом:
setMessage(Message message), который будет обновлять все, так как форматирование того, что надо отобразить, должно быть ответственностью презентера. Например, в будущем вы захотите отображать "Вы" вместо имени пользователя, если текущий пользователь является автором сообщения, а это является частью бизнес-логики.MessagePresenter.onAuthorClicked() и MessageView.goToAuthorProfile(). Реализация представления, вероятно, будет иметь клик лисенер, который будет вызывать данный метод презентера, а тот в свою очередь будет вызывать goToAuthorProfile(). Не нужно ли убрать все это и переходить в профиль автора непосредственно из клик лисенера. Нет! Решение, переходить ли в профиль пользователя при нажатии на его имя, является частью вашей бизнес-логики, и за это отвечает презентер.Как выяснено на практике, если код вашего презентера содержит код Android-фреймворка, а не только pure Java, вероятно, вы что-то делаете неверно. И соответственно, если ваши представления нуждаются в ссылке на модель, видимо, вы также делаете что-то неправильно.
Как только возникнет вопрос тестов, большинство кода, который вам необходимо протестировать, будет в презентере. Что круто, так это то, что этому коду не нужен Android для запуска, так как у него есть только ссылки на интерефейс представления, а не на его реализацию в контексте Android. Это значит, что вы можете просто мокнуть интерфейс представления и написать чистые JUnit-тесты для бизнес-логики, проверяющие правильность вызова методов у мокнутого представления. Вот так [5] теперь выглядят наши тесты.
До настоящего момента мы предполагали, что наши представления — это активити и фрагменты, но в реальности они могут быть чем угодно. У нас довольно неплохо получилось работать со списками, имея ViewHolder, реализующий интерфейс представления (как RecyclerView.ViewHolder, так и обычный старый ViewHolder для использования в связке с ListView). В адаптере вам всего лишь нужна базовая логика для обработки присоединения/отсоединения презентеров (пример всего этого есть в гит-репозитории).
Если вы посмотрите на пример экрана, содержащего список сообщений, прогресс загрузки и пустую вьюху, разделение ответственности будет следующим:
Все эти компоненты слабо связаны и могут быть протестированы отдельно друг от друга.
Более того, если у вас есть экран списка сообщений и экран подробностей, вы можете переиспользовать тот же презентер сообщения и просто иметь две разные реализации интерфейса представления (во ViewHolder-е и фрагменте). Это сохраняет ваш код DRY (прим. пер. — "Don't Repeat Yourself", или "Не повторяйтесь", кто не знает).
Подобным образом, интерфейс представления могут реализовывать кастомные вьюхи. Это позволяет вам использовать MVP в кастомных виджетах, чтобы переиспользовать это в разных частях приложения, или же просто разбивать сложные интерфейсы на блоки попроще.
Если вы уже какое-то время пишете под Android, вы знаете, сколько боли доставляет поддержка смены ориентации и конфигурации:
Правильное использование MVP может решить этот вопрос без необходимости вообще задумываться об этом. Так как у презентеров нет сильной ссылки на текущий UI, они очень легковесные и могут быть восстановлены при смене ориентации! Так как презентер хранит ссылку на модель и состояние представления, он может восстановить нужное состояние представления после смены ориентации. Вот примерное описание того, что происходит при повороте экрана, если используется данный паттерн:
Как сохранять фрагменты между сменами ориентации, можно увидеть в репозитории, в классе PresenterManager.
Да, это конец. Надеюсь, получилось продемонстрировать, как разделение ответственности наподобие MVP поможет вам писать поддерживаемый и тестируемый код.
Резюмируя:
Реализацию вышеописанного можно найти в репозитории ArchExample [4].
Также есть множество библиотек, которые могут помочь вам использовать такой подход, например, Mosby [6], Flow и Mortar [7], или Nucleus [8]. Советую их рассмотреть.
Автор: Bringoff
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android-development/114544
Ссылки в тексте:
[1] MVP: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter
[2] Rails: http://guides.rubyonrails.org/getting_started.html
[3] iOS: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
[4] https://github.com/remind101/android-arch-sample: https://github.com/remind101/android-arch-sample
[5] Вот так: https://github.com/remind101/android-arch-sample/blob/master/app/src/test/java/com/remind101/archexample/presenters/CounterPresenterTest.java
[6] Mosby: http://hannesdorfmann.com/mosby/
[7] Flow и Mortar: https://corner.squareup.com/2014/01/mortar-and-flow.html
[8] Nucleus: https://github.com/konmik/nucleus
[9] Источник: https://habrahabr.ru/post/278769/
Нажмите здесь для печати.