Android
July 29, 2021

Зачем нужны и как понять эти ваши архитектуры в Android?

Предисловие

Каждый новичок, что пытается писать под Android, рано или поздно сталкивается с моментом, когда наступает понимание, что писать весь код в одной Activity или даже Fragment'е неудобно. И в этой заметке хочу рассмотреть некоторые вопросы про архитектуру. Думаю, что каждый слышал про то, какие в принципе бывают архитектурные паттерны, но на всякий случай начну с их перечисления:

  • MVC
  • MVP (VIPER и другие производные от MVP, в том числе PIDOR)
  • MVVM
  • MVI

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

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

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

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

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


MVC

Model-View-Controller

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


MVP

Model-View-Presenter

MVP - настоящая взрослая архитектура. Мне в свое время понадобилось много времени, чтобы вообще понять, как в Android построить MVP. Главной ошибкой было то, что я пытался читать статьи, вникать и понимать, а не писать код. А построение архитектуры - это практический навык. Есть уже написанная шикарная статья о том, как построить MVP-архитектуру: MVP в Android для самых маленьких.

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

class MainPresenter(
   val mainView: MainContract.View, 
   val mainModel: MainContract.Model
) : MainContract.Presenter { ... }

MVVM

Для MVVM ничего не придумал :(

Чуть более сложный для понимания шаблон, поэтому настоятельно рекомендую попробовать в первую очередь MVP. Забегу вперед и мотивирую: если освоить MVVM-шаблон, то по сути нужно будет дальше приложить минимальное усилие, чтоб познать MVI и собрать в голове полную копилку знаний об основных архитектурных паттернах проектирования Android-приложений. Первое и главное отличие в названиях слоев бизнес-логики: в MVP и его производных она хранится в Presenter, тогда как в MVVM эту роль исполняет ViewModel. Во вторых, это способ взаимодействия View-слоя и ViewModel. Это осложнено тем, что ViewModel не знает явно о View-слое, но при этом им управляет. Как такое может быть? Об этом чуть дальше.

Прежде чем начать разбираться с MVVM нужно обговорить более детально о видах взаимодействия View и ViewModel и вот какие они бывают:

  1. Прослушивание событий во ViewModel, происходящие во VIew, примеры: нажатие кнопки, изменение текста в EditText.
  2. Изменения во ViewModel, которые после configuration changes должны подтягиваться актуальные значения, примеры: пришли данные с сервера и нужно показать/скрыть виджет на View-слое, изменить текст в TextView.
  3. Исполнение View-слоем однократных событий по приказу ViewModel'и, пример: показать диалог, показать тост или перейти на другой фрагмент/экран.

Схематично ViewModel выглядит так:

class MainViewModel(
   val mainModel: MainContract.Model
) : ViewModel(), MainContract.ViewModel { ... }

Понятно, что связь View с ViewModel никуда не девается, не смотря на отсутствие в конструкторе View, она лишь усложняется и варианты управления View-слоем ViewModel'ю есть такие:

  1. связывание XML-кода и ViewModel'и через DataBinding;
  2. реализация шаблона проектирования Observer для отправки событий из ViewModel во View для изменения состояний отдельного View-widget'а или даже целиком View-слоя.

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

MVVM через DataBinding

DataBinding - библиотека, основанная на кодогенерации и содержит в себе Observable-классы, которые лежат во ViewModel и связываются напрямую с XML-разметкой. Подробнее о работе с событиями:

  1. Прослушивание событий происходит через реализацию функций напрямую в XML-разметке, пример в виде прослушивания изменений в EditText-виджете:
android:onTextChanged =
"@{(text, start, before, count) -> viewModel.onUsernameTextChanged(text)}"

2. Изменения во ViewModel происходит через вышеназванные Observable-классы-обертки и подобным образом связываются с XML. Пример: мы сделали внутри viewModel поле:

 val name = new ObservableField<String>()

Тогда связывание поля с XML будет выглядеть так:

 android:text="@={viewModel.name}"

3. Исполнение View-слоем однократных событий можно реализовать через предложенный Google класс - SingleLiveEvent. Пользоваться им нужно, как обычной LiveData. Класс можно скопипастить в свой проект. Есть статья, где наглядно показано, как этим всем пользоваться.

MVVM через шаблон-Observer

Для такой реализации MVVM нужно будет использовать LiveData или Kotlin Flow, что как раз и реализуют принцип шаблона Observer, а LiveData еще и согласована с жизненным циклом нашего View. Подробнее о работе с событиями:

1. Прослушивание событий происходит банально через колбэки. View знает явно и хранит в себе ссылку на ViewModel, что как раз нам позволяет использовать её методы.

2. Изменения во ViewModel происходит через MutableLiveData или MutableStateFlow. У нас всегда на View-слое будет актуальное значение нашего конкретного виджета или всего View-слоя.

3. Исполнение View-слоем однократных событий можно реализовать, как в DataBinding'е через SingleLiveEvent.

Мини-резюме по MVVM

При правильном и опытном подходе MVVM упрощает процесс написания тест-кода (в следствие меньшего количество явных связей) и по сути может являться чуть менее многословной архитектурой, чем MVP с ее производными. Из этих двух подходов рекомендую реализацию через шаблон-Observer, по двум причинам:

  1. В DataBinding кодогенерация, что дает ощутимое замедление времени сборки проекта.
  2. Вчера Google выкатили релизную версию JetPack Compose, что делает подход с DataBinding'ом еще менее актуальным, ведь наступает эра отказа от XML в пользу кодовой верстки экранов через Kotlin.

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


MVI

Здесь тоже творческий кризис

Думаю по тексту было заметно, что глобальное отличие MVP от MVVM по сути лишь во взаимодействии между View и ViewModel-Presenter'ом. Так вот, мотивировал выше не зря. MVVM еще проще трансформируется в MVI, чем MVP в MVVM. Когда я только пытался изучать MVI - было мало статей и по ним не получалось понять основной принцип. И когда в очередной раз я на своем pet-проекте писал, как я думал, MVVM, то в один прекрасный момент до меня дошло, что это как раз получилась MVI, такой вот каламбур.

Идея MVI построена вокруг более пристального контроля View-слоя. У нас есть некий ViewState data-класс, который хранит в себе кучу полей, что контролируют состояние View. И из ViewModel'и напрямую во View летит через Observer-шаблон только этот data-class. Кроме ViewState может быть еще Action-сущность на уровне ViewModel'и. Для наглядности приведу пример: мы загружаем данные с сервера. И в зависимости от того, какие данные пришли, например, пустой список для отображения на нашем экране или нет, мы выбираем один из определенных class'ов sealed-класса Action'a, вызываем функцию sendAction(Action.NotEmptyList), далее этот метод у себя под капотом вызывает onReduceState(Action.NotEmptyList). И функция onReduceState имеет возвращаемый тип ViewState, где как раз определены все наши ViewState'ы через функцию копирования copy() у Data-классов.

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