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

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

Что такое чистая архитектура?

Я не буду здесь вдаваться в подробности, поскольку есть статьи, которые объясняют это намного лучше, чем я. Но в следующем абзаце содержится суть того, что вам нужно знать, чтобы понять Clean.

Обычно в Clean код разделяется на слои в форме луковицы с одним правилом зависимости: внутренние слои не должны ничего знать о внешних слоях. Это означает, что зависимости должны указывать внутрь.

Это визуализированный предыдущий абзац:

Чистая архитектура, как упоминалось в предоставленных статьях, делает ваш код:

  • Независимо от платформ
  • Подходит для тестирования.
  • Независимо от пользовательского интерфейса.
  • Независимо от базы данных.
  • Независимо от каких-либо внешних агентств.

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

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

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

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

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

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

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

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

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

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

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

Как мне начать писать чистые приложения?

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

Вы можете найти стартовый проект здесь: Android Clean Boilerplate

Приступаем к написанию нового варианта использования

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

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

Состав

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

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

Наружный слой

Как уже упоминалось, здесь подробно рассказывается о фреймворке.

Пользовательский интерфейс - сюда вы помещаете все свои действия, фрагменты, адаптеры и другой код Android, связанный с пользовательским интерфейсом.

Хранилище - код конкретной базы данных, который реализует интерфейс, который наши взаимодействующие лица используют для доступа к данным и их хранения. Сюда входят, например, ContentProviders или ORM, такие как DBFlow.

Сеть - Здесь можно найти такие вещи, как Модернизация.

Средний слой

Склейте слой кода, который связывает детали реализации с вашей бизнес-логикой.

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

Конвертеры - объекты конвертера отвечают за преобразование внутренних моделей во внешние и наоборот.

Внутренний слой

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

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

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

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

Executor - этот пакет содержит код для запуска Interactors в фоновом режиме с помощью исполнителя рабочего потока. Этот пакет обычно не нужно менять.

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

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

  • пакет презентация
  • пакет хранилище
  • пакет домен

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

Пакет Presentation отвечает за все, что связано с отображением объектов на экране - он включает в себя весь стек MVP (это означает, что он также включает в себя как UI, так и пакеты Presenter, даже если они принадлежат разным уровням).

ОК - меньше разговоров, больше кода.

Написание нового Interactor (внутренний / основной уровень)

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

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

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

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

public interface MessageRepository { 
    String getWelcomeMessage();
}

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

Он просто пытается получить сообщение и отправляет сообщение или ошибку в пользовательский интерфейс, чтобы отобразить его. Мы уведомляем пользовательский интерфейс с помощью нашего обратного вызова, который на самом деле будет нашим докладчиком. Это суть нашей бизнес-логики. Все остальное, что нам нужно сделать, зависит от платформы.

Давайте посмотрим, какие зависимости есть у этого Interactor:

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

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

Теперь мы можем запустить и протестировать наш Interactor без запуска эмулятора. Итак, давайте напишем простой тест JUnit, чтобы убедиться, что он работает:

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

Написание презентационного слоя

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

Начнем с написания интерфейса наших Presenter и View. Единственное, что нужно сделать нашему представлению, - это отобразить приветственное сообщение:

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

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

В нашем классе MainActivity мы переопределяем метод onResume ():

@Override
protected void onResume() {
    super.onResume();
    // let's start welcome message retrieval when the app resumes
    mPresenter.resume();
}

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

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

Мы запускаем Interactor внутри класса MainPresenter в методе resume ():

@Override
public void resume() {
    mView.showProgress();
    // initialize the interactor
    WelcomingInteractor interactor = new WelcomingInteractorImpl(
            mExecutor,
            mMainThread, 
            this, 
            mMessageRepository
    );
    // run the interactor
    interactor.execute();
}

Метод execute () просто выполнит метод run () метода WelcomingInteractorImpl в фоновой ветке. Метод run () можно увидеть в разделе Написание нового Interactor.

Вы можете заметить, что Interactor ведет себя аналогично классу AsyncTask. Вы снабжаете его всем, что нужно для его запуска и выполнения. Вы можете спросить, почему мы просто не использовали AsyncTask? Потому что это код для Android, и вам понадобится эмулятор для его запуска и тестирования.

Мы предоставляем интерактору несколько вещей:

  • Экземпляр ThreadExecutor, который отвечает за выполнение взаимодействующих элементов в фоновом потоке. Я обычно делаю это синглтон. Этот класс фактически находится в пакете domain, и его не нужно реализовывать на внешнем уровне.
  • Экземпляр MainThreadImpl, который отвечает за отправку исполняемых файлов в основной поток из Interactor. Основные потоки доступны с использованием кода, специфичного для фреймворка, поэтому мы должны реализовать его на внешнем уровне.
  • Вы также можете заметить, что мы предоставляем this для Interactor - MainPresenter - это объект обратного вызова, который Interactor будет использовать для уведомления пользовательского интерфейса о событиях.
  • Мы предоставляем экземпляр WelcomeMessageRepository, который реализует интерфейс MessageRepository, который использует наш интерактор. WelcomeMessageRepository рассматривается позже в разделе Создание уровня хранения.

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

Что касается this, MainPresenter MainActivity действительно реализует интерфейс обратного вызова:

public class MainPresenterImpl extends AbstractPresenter implements MainPresenter, WelcomingInteractor.Callback {

И именно так мы слушаем события от Interactor. Это код из MainPresenter:

@Override 
public void onMessageRetrieved(String message) {
    mView.hideProgress(); 
    mView.displayWelcomeMessage(message);
} 
 
@Override 
public void onRetrievalFailed(String error) {
    mView.hideProgress(); 
    onError(error);
}

Представление, показанное в этих фрагментах, - это наша MainActivity, которая реализует этот интерфейс:

public class MainActivity extends AppCompatActivity implements MainPresenter.View {

Которая затем отвечает за отображение приветственного сообщения, как показано здесь:

@Override 
public void displayWelcomeMessage(String msg) {
    mWelcomeTextView.setText(msg);
}

И это почти все, что касается уровня представления.

Написание уровня хранения

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

Для сложных данных вы можете использовать ContentProviders или инструменты ORM, такие как DBFlow. Если вам нужно получить данные из Интернета, вам поможет Retrofit. Если вам нужно простое хранилище ключей и значений, вы можете использовать SharedPreferences. Вы должны использовать правильный инструмент для работы.

Наша база данных на самом деле не является базой данных. Это будет очень простой класс с некоторой имитацией задержки:

public class WelcomeMessageRepository implements MessageRepository { 
    @Override 
    public String getWelcomeMessage() {
        String msg = "Welcome, friend!"; // let's be friendly
 
        // let's simulate some network/database lag 
        try { 
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
 
        return msg;
    } 
}

Что касается нашего WelcomingInteractor, задержка может быть связана с реальной сетью или по любой другой причине. На самом деле ему все равно, что находится под MessageRepository, если он реализует этот интерфейс.

Резюме

Этот пример доступен в репозитории git здесь. Обобщенная версия вызовов по классам выглядит следующим образом:

MainActivity - ›MainPresenter -› WelcomingInteractor - ›WelcomeMessageRepository -› WelcomingInteractor - ›MainPresenter -› MainActivity

Важно отметить поток управления:

Внешний - Средний - Основной - Внешний - Основной - Средний - Внешний

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

Заключение

Для меня это пока лучший способ разработки приложений. Разделенный код позволяет легко сосредоточить ваше внимание на конкретных проблемах, не мешая множеству вредоносных программ. В конце концов, я думаю, что это довольно ТВЕРДЫЙ подход, но к нему нужно время, чтобы привыкнуть. По этой же причине я написал все это, чтобы помочь людям лучше понять с помощью пошаговых примеров. Если что-то останется неясным, я с радостью отвечу на эти вопросы, так как ваш отзыв очень важен. Еще очень хотелось бы услышать, что можно улучшить. Здоровая дискуссия принесет пользу всем.

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

И снова этот образец приложения был построен на основе стартового пакета Clean, который можно найти здесь: Android Clean Boilerplate

дальнейшее чтение

Это руководство должно было расширить эту замечательную статью. Разница в том, что я использовал обычную Java в своих примерах, чтобы не добавлять слишком много накладных расходов при демонстрации этого подхода. Если вам нужны примеры RxJava с Clean, загляните сюда.