avatar it-technology

it-technology



подробнее...

Следить за персональным блогом

Автоматизированная система Промышленная безопасность и охрана труда

Обновления главной ленты блогов
Вконтакте Facebook Twitter RSS Почта Livejournal
Внимание

На нашем портале можно бесплатно публиковать информацию о своей компании, размещать товары и услуги и цены на них.
Ведите свой личный или корпоративный блог и его ежедневно увидят 30 тысяч посетителей нашего сайта.

14 ноября 2017, 03:04

Подробный гайд по разработке Android-приложений с помощью Clean Architecture


Данный туториал поможет вам разобраться в очень полезном подходе по разработке приложений Clean Architecture.

С того времени, как я начал разработку Android-приложений, у меня сложилось впечатление, что это можно было сделать лучше. За свою карьеру я видел множество плохих программных решений, часть из которых была моя. А если смешать сложность Android c плохим программным решением, например, плохой архитектурой, то получится рецепт катастрофы. Однако учится на своих ошибках это важно, главное при этом не совершать их в дальнейшем и улучшать свои навыки. После долгих поисков оптимального способа разработки приложений я наткнулся на подход под названием Clean Architecture. После того, как я применил данный подход к Android-приложениям с некоторой доработкой и воодушевлением от подобных проектов, я решил, что этот подход достаточно практичен и заслуживает внимания.

Целью статьи является предоставление пошаговой инструкции разработки Android-приложений с применением подхода Clean Architecture. Суть моего подхода заключается в том, что я на довольно успешном примере создания своих приложений для реальных клиентов, объясняю суть Clean Architecture.

Что такое Clean Architecture?

Я не собираюсь вдаваться в подробности, потому что есть статьи, в которых это объясняется лучше, чем смогу сделать я. Однако следующий параграф рассматривает ключевой вопрос, который вам необходимо знать, чтобы понять, как устроен подход Clean Architecture.

Как правило в Clean Architecture код разделен на несколько уровней, по структуре схожей со структурой обычного лука, с одним правилом зависимости: внутренний уровень не должен зависеть от каких-либо внешних уровней. Это означает, что зависимости должны указываться внутри каждого из уровня, чтобы не было зависимостей между уровнями (слоями).

Далее приведена визуализация вышесказанного:

Clean Architecture

Clean Architecture, как говорится во всех связанных с ней статьях, делает ваш код:

  • Независящим от фреймворков;
  • Тестируемым;
  • Независящим от UI;
  • Независящим от Базы данных;
  • Независимым от какого-либо внешнего воздействия.

Я надеюсь, что вам станет понятно, как каждый из этих пунктов достигается, за счет приведенных ниже примеров. Для более детального объяснения данного подхода я настоятельно рекомендую ознакомиться с этой статьей и данным видео.

Что это значит для Android?

Как правило, ваше приложение имеет произвольное количество уровней (слоев), однако если вам не нужна бизнес-логика Enterprise, которую необходимо применять в каждом Android-приложении, то скорее всего у вас будет только 3 уровня:

  • Внешний: Уровень реализации;
  • Средний: Уровень интерфейса;
  • Внутренний: Уровень бизнес-логики.

Уровень реализации – это место где описывается структура и поведение всего. Код структуры включает в себя каждую строчку кода, которая не решает поставленную задачу. Сюда входит любое содержимое Android такое, как: создание операций и фрагментов, отправка намерений, и другой структурный код наподобие сетевого кода и кода базы данных.

Целью уровня интерфейса является обеспечение соединения между уровнем реализации и уровнем бизнес логики.

Самым важным уровнем считается уровень бизнес логики. Данный уровень — это то, где вы фактически решаете, поставленную задачу, собственно ради которой и создавалось приложение. Уровень бизнес логики не содержит какого-либо структурного кода, и вы должны уметь запускать его без эмулятора. Таким образом, если вы будете придерживаться подобного подхода при построении бизнес-логики, то получится уровень легко тестируемый, разрабатываемый и его будет легко поддерживать. Пожалуй, это самая большая выгода при использовании Clean Architecture.

Каждый уровень, расположенный над основным уровнем, также отвечает за преобразование моделей в модели нижнего уровня, перед тем как нижний уровень сможет их использовать. Нижний уровень может не иметь ссылки на модель класса, который принадлежит внешнему уровню. Несмотря на это, внешний уровень может использовать и ссылочные модели внутреннего уровня. Опять же таки, это возможно благодаря нашему правилу зависимости. Это приводит к большему ресурсопотреблению, но оно является необходимым для того, чтобы убедиться, что наш код не привязан к какому-либо из уровней.

Почему данное преобразование моделей является обязательным?

К примеру, ваши модели бизнес-логики могут оказаться некорректными для непосредственного отображения их пользователю. Возможно, вам необходимо отображать сочетание нескольких моделей бизнес-логики. По этой причине, я рекомендую вам создать класс ViewModel, который позволит вам упростить отображение моделей в интерфейсе пользователя. После чего, вы просто используете класс converter внешнего уровня для преобразования ваших бизнес-моделей в соответствующие ViewModel.

Еще одним примером может быть следующее: Давайте скажем, что объект Cursor, принадлежащий ContentProvider во внешнем уровне базы данных. Значит, что внешний уровень, в первую очередь преобразует его в бизнес-модель внутреннего уровня, а затем уже отдаст его на обработку соответствующему уровню бизнес-логики.

Внизу статьи я добавлю больше ресурсов для изучения данного вопроса. Сейчас же мы уже знаем об основных принципах подхода Clean Architecture, а значит давайте «замараем» руки кодом. Далее, я покажу вам как создать рабочий функционал с использованием Clean Architecture.

Как начать создание Чистых приложений?

Специально для вас я создал шаблонный проект, в котором уже есть все что вам нужно. Своего рода, это стартовый набор для тех, кто хочет начать придерживаться Clean-подхода. Данный стартовый набор предназначен для скорейшего создания приложений, с помощью, уже встроенных, самых распространённых инструментов. Вы можете скачать этот набор абсолютно бесплатно, затем модифицировать его под свои нужды и создавать свои приложения.

Вы можете найти стартовый набор здесь: Шаблонный проект для создания Чистых приложений на Android.

Первые шаги по написанию новых прецедентов

Этот раздел будет объяснять весь необходимый вам код, для создания своих прецедентов с помощью подхода Clean Architecture, так скажем поверх шаблона, приведенного в предыдущем разделе. Прецедент – это просто некоторый изолированный функционал приложения. Прецедент может быть запущен или не может быть запущен пользователем (например, по нажатию пользователя).

Во-первых, давайте объясним структуру и терминологию этого подхода. И да, это просто то, как я создавал свои приложения, то есть это не является чем-то незыблемым, и вы можете организовывать свои приложения по-другому, как вам хочется.

Структура

Общая структура Android-приложения выглядит, как показано ниже:

  • Пакеты внешнего уровня: Интерфейс пользователя, хранилище, сеть и т.д.;
  • Пакеты среднего уровня: Представители, конвертеры;
  • Пакеты внутреннего уровня: Interactors, модели, репозитории, исполнители.

Внешний уровень

Как уже было сказано, это то, где описываются детали структуры.

Интерфейс пользователя (UI) – Это то, куда вы помещаете все ваши Операции, Фрагменты, Адаптеры и любой другой Android-код, связанный с интерфейсом пользователя.

Хранилище – Отдельный код для базы данных, который реализует интерфейс наших Интеракторов, используемых для доступа к базе данных и для хранения данных. Например, сюда включается Поставщик контента или ORM-ы такие, как DBFlow.

Сеть – Вещи подобные Retrofit отправляются сюда.

Средний уровень

Уровень, на котором располагается связующий код. Его главной задачей является связывание различных реализаций, с уровнем вашей бизнес-логики.

Представители (presenter) – представители обрабатывают события от UI (например, клик пользователя), и чаще всего работают как callback-и из внутренних уровней (Interactors).

Конвертеры – Преобразуют объекты, которые ответственны за конвертацию моделей внутреннего уровня в модели внешнего уровня и в обратном порядке.

Внутренний уровень

Основной уровень, который содержит самый высокоуровневый код. Все классы здесь являются POJO, то есть это простые Java-объекты, не унаследованные от какого-то специфического объекта и не реализующие никаких служебных интерфейсов сверх тех, которые нужны для бизнес-модели.

Классы и объекты данного уровня никак не оповещаются, что будут запущены именно в Android-приложении, поэтому их легко можно перенести на любую JVM.

Interactors – классы, которые фактически содержат код вашей бизнес-логики. Они запускаются в фоновом режиме и передают события верхнему уровню с помощью callback-ов. Их также называют Прецедентами (UseCases) в некоторых проектах, возможно, такое называние им больше подходит. Наличие множества небольших Interactor-классов в ваших проектах для решения определенных задач считается нормой. Это полностью соответствует принципу единственной ответственности и, как мне кажется, проще для восприятия и понимания.

Модели – это ваши бизнес-модели, которыми вы управляете в своей бизнес-логике.

Репозитории (repositories ) –  данный пакет включает в себя только те интерфейсы, которые реализованы с помощью базы данных или каких-либо других внешних уровней. Эти интерфейсы используются Interactor-классы для доступа и хранения данных. Это также называется паттерн Repository.

Исполнитель (executor) – данный пакет содержит код для запуска Interactor-классов в фоновом режиме с помощью рабочего потока-исполнителя. Чаще всего вам не придется изменять этот пакет.

Простой пример

В этом примере, нашим прецедентом будет: «Приветствие пользователя сообщением, когда приложение запущено и данное сообщение помещено в базу данных.» Данный пример будет наглядной демонстрацией того, как создать следующие пакеты, необходимые для корректной работы нашего прецедента:

  • Пакет представления;
  • Пакет хранилища;
  • Пакет домена.

Первые два пункта относятся к внешнему уровню, в то время как последний относится к внутреннему/основному уровню.

Пакет представления ответственен за все, что связано с отображением вещей на экране, он содержит весь стек шаблона проектирования MVP. Это означает, что он содержит в себе как UI, так и Presenter-пакеты, даже если они относятся к разным уровням.

Отлично – меньше слов, больше кода!

Создание нового Interactor-а (внутренний/основной уровень)

На самом деле, вы можете начать разработку своего приложения с любого уровня представленной архитектуру, но я рекомендую начать именно с основного уровня бизнес-логики. Вы можете написать весь необходимы для этого код, протестировать его и убедиться, что он работает даже без создания операции.

Итак, давайте начнем создание Interactor-а. Interactor – это то место, где располагается основная логика работы нашего прецедента. Все Interactor-ы запускаются в фоновом потоке, поэтому не должно быть никакого воздействия на производительность интерфейса пользователя. Давайте создадим новый Interactor с приятным названием «WelcomingInteractor».

public interface WelcomingInteractor extends Interactor {       interface Callback {           void onMessageRetrieved(String message);           void onRetrievalFailed(String error);     } }

123456789public interface WelcomingInteractor extends Interactor {      interface Callback {          void onMessageRetrieved ( String message ) ;          void onRetrievalFailed ( String error ) ;     } }

Callback отвечает за общение с интерфейсом пользователя (UI) в основном потоке, мы помещаем его в интерфейс Interactor-а, поэтому нет необходимости в подобном названии «WelcomingInteractorCallback», чтобы отличать его от других callback-ов. Теперь реализуем логику получения сообщения. Давайте скажем, что у нас есть интерфейс MessageRepository, в котором будет наше сообщение приветствия.

public interface MessageRepository {     String getWelcomeMessage(); }

123public interface MessageRepository {     String getWelcomeMessage ( ) ; }

Далее реализуем этот интерфейс вместе с нашей бизнес-логикой. Важно, чтобы реализация наследовала AbstractInteractor, который отвечает за запуск нашего интерфейса в фоновом потоке.

public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {     ...     private void notifyError() {         mMainThread.post(new Runnable() {             @Override             public void run() {                 mCallback.onRetrievalFailed("Nothing to welcome you with :(");             }         });     }

    private void postMessage(final String msg) {         mMainThread.post(new Runnable() {             @Override             public void run() {                 mCallback.onMessageRetrieved(msg);             }         });     }     @Override     public void run() {         // получение сообщения         final String message = mMessageRepository.getWelcomeMessage();         // проверяем, получили ли мы сообщение         if (message == null || message.length() == 0) {             // уведомляем об ошибке основной поток             notifyError();             return;         }        // мы получили наше сообщение, уведомляем об этом UI в основном потоке         postMessage(message);     }

123456789101112131415161718192021222324252627282930313233public class WelcomingInteractorImpl extends AbstractInteractor implements WelcomingInteractor {     . . .     private void notifyError ( ) {         mMainThread . post ( new Runnable ( ) {             @ Override             public void run ( ) {                 mCallback . onRetrievalFailed ( "Nothing to welcome you with :(" ) ;             }         } ) ;     }     private void postMessage ( final String msg ) {         mMainThread . post ( new Runnable ( ) {             @ Override             public void run ( ) {                 mCallback . onMessageRetrieved ( msg ) ;             }         } ) ;     }     @ Override     public void run ( ) {         // получение сообщения         final String message = mMessageRepository . getWelcomeMessage ( ) ;         // проверяем, получили ли мы сообщение         if ( message == null || message . length ( ) == 0 ) {             // уведомляем об ошибке основной поток             notifyError ( ) ;             return ;         }        // мы получили наше сообщение, уведомляем об этом UI в основном потоке         postMessage ( message ) ;     }

Что же, взглянем на зависимости, создаваемые нашим Interactor:Этот фрагмент кода, пытается получить сообщение, затем переслать его или же отправить сообщение об ошибке интерфейсу пользователя, чтобы он отобразил сообщение или ошибку. Для этого мы уведомляем интерфейс пользователя с помощью нашего callback-а, который по факту и будет Presenter-ом. Собственно, в этом и заключается суть всей нашей бизнес-логики. Все что нам остается – это построить структурные зависимости.

import com.kodelabs.boilerplate.domain.executor.Executor;  import com.kodelabs.boilerplate.domain.executor.MainThread;  import com.kodelabs.boilerplate.domain.interactors.WelcomingInteractor;  import com.kodelabs.boilerplate.domain.interactors.base.AbstractInteractor;  import com.kodelabs.boilerplate.domain.repository.MessageRepository;

12345import com . kodelabs . boilerplate . domain . executor . Executor ;   import com . kodelabs . boilerplate . domain . executor . MainThread ;   import com . kodelabs . boilerplate . domain . interactors . WelcomingInteractor ;   import com . kodelabs . boilerplate . domain . interactors . base . AbstractInteractor ;   import com . kodelabs . boilerplate . domain . repository . MessageRepository ;

Как вы можете заметить, здесь нет ни одного упоминания о каком-либо Android-коде. Это и есть главное преимущество данного подхода. Также вы можете увидеть, что пункт: «Независимость от фреймворков» все также соблюдается. Кроме того, нам не нужно отдельно определять интерфейс пользователя или базу данных, мы просто вызываем методы интерфейса, которые кто-то, где-то на внешнем уровне реализует. Следовательно, мы независим от UI и независим от Базы данных.

Тестирование нашего Interactor -а

На данный момент мы можем запустить и начать тестирование нашего Interactor-а без запуска эмулятора. Поэтому давайте напишем простой Junit-тест, чтобы убедиться, что все работает:

...     @Test     public void testWelcomeMessageFound() throws Exception {

        String msg = "Welcome, friend!";         when(mMessageRepository.getWelcomeMessage())                 .thenReturn(msg);         WelcomingInteractorImpl interactor = new WelcomingInteractorImpl(             mExecutor,             mMainThread,             mMockedCallback,             mMessageRepository         );         interactor.run();         Mockito.verify(mMessageRepository).getWelcomeMessage();         Mockito.verifyNoMoreInteractions(mMessageRepository);         Mockito.verify(mMockedCallback).onMessageRetrieved(msg);     }

12345678910111213141516171819. . .     @ Test     public void testWelcomeMessageFound ( ) throws Exception {         String msg = "Welcome, friend!" ;         when ( mMessageRepository . getWelcomeMessage ( ) )                 . thenReturn ( msg ) ;         WelcomingInteractorImpl interactor = new WelcomingInteractorImpl (             mExecutor ,             mMainThread ,             mMockedCallback ,             mMessageRepository         ) ;         interactor . run ( ) ;         Mockito . verify ( mMessageRepository ) . getWelcomeMessage ( ) ;         Mockito . verifyNoMoreInteractions ( mMessageRepository ) ;         Mockito . verify ( mMockedCallback ) . onMessageRetrieved ( msg ) ;     }

И вновь, этот Interactor даже не подозревает, что будет находиться внутри Android-приложения. Это доказывает, что наша бизнес-логика является тестируемой, а это был наш пункт номер два.

Создание уровня представления

Код представления относится ко внешнему уровню подхода Clean Architecture. Уровень представления состоит из структурно зависимого кода, который отвечает за отображение интерфейса пользователя, собственно, пользователю. Мы будем использовать класс MainActivity для отображения нашего приветствующего сообщения пользователю, когда приложение возобновляет свою работу.

Давайте начнем с создания интерфейса нашего Presenter и Отображения (View). Единственное, что должно делать наше отображение – это отображать приветствующее сообщение:

public interface MainPresenter extends BasePresenter {       interface View extends BaseView {         void displayWelcomeMessage(String msg);     } }

123456public interface MainPresenter extends BasePresenter {      interface View extends BaseView {         void displayWelcomeMessage ( String msg ) ;     } }

Итак, как и где мы запускаем наш Interactor, когда приложение возобновляет работу? Все, что не имеет строгой привязки к отображению, должно помещаться в класс Presenter. Это помогает достичь принципа Разделения ответственности и предотвратить классы Операций от чрезмерного увеличения размера кода. Сюда включается весь код, который работает с Interactor-ми.

В нашем классе MainActivity мы переопределяем метод onResume(): @Override protected void onResume() {     super.onResume();  // начнем возврат приветствующего сообщения, при возобновлении работы приложения     mPresenter.resume(); }

12345@ Override protected void onResume ( ) {     super . onResume ( ) ;   // начнем возврат приветствующего сообщения, при возобновлении работы приложения     mPresenter . resume ( ) ; }

Все Presenter-объекты реализуют метод resume(), при наследовании BasePresenter.

Примечание: Самые внимательные читатели могли заметить, что я добавил Android-методы жизненного цикла в интерфейс BasePresenter в качестве вспомогательных методов, хотя сам Presenter находится на более низком уровне. Наш Presenter должен знать все на уровне UI, к примеру, что что-то на этом уровне имеет жизненный цикл. Тем не менее, здесь я не указываю конкретное событие, так как каждый UI для конкретного пользователя может отрабатывать разные события, в зависимости от действий пользователя. Представьте, я назвал его onUIShow() вместо onResume(). Теперь все хорошо, верно?