Зачем нужны и как понять эти ваши архитектуры в Android?
Предисловие
Каждый новичок, что пытается писать под Android, рано или поздно сталкивается с моментом, когда наступает понимание, что писать весь код в одной Activity или даже Fragment'е неудобно. И в этой заметке хочу рассмотреть некоторые вопросы про архитектуру. Думаю, что каждый слышал про то, какие в принципе бывают архитектурные паттерны, но на всякий случай начну с их перечисления:
- MVC
- MVP (VIPER и другие производные от MVP, в том числе PIDOR)
- MVVM
- MVI
Цель самой архитектуры - написание кода, который легче поддерживать. И тут отмечу важный момент. Стадии, какие прошел я и какие, как мне кажется, проходят почти все разработчики, что смотрят в сторону архитектурных решений:
- Ужас от написанного своего приложения без архитектуры.
- Первые попытки написания кода на архитектурном паттерне. Этот этап характерен ощущением, что код стал более легкоподдерживаемым, но при этом самого кода становится больше. То есть, плюс к читабельности, но минус ко времени написания фичей.
- Грамотное выделение абстракций и сокращение boilerplate-кода.
С первой стадией все понятно. А вот со второй и третьей сложнее. Вторая стадия может длиться довольно долго. А третья стадия - высшая точка понимания, потому что наступает момент, когда хорошо изучены тонкости языка и код начинает быть более автоматизированным, абстрагируется все больше удобных вещей в Base-классах.
Разработчик сталкивается с разными проектами, разными нюансами и разным стеком технологий. И архитектуры, что являлась бы серебряной пулей, попросту не существует, иначе все бы разработчики писали идентичный код по сэмплу, например, от Google.
Теперь рассмотрим подробнее каждый шаблон и попробую изложить свое видение, как лучше их понять.
MVC
Это самый простой шаблон, где основная бизнес-логика содержится в классе Model. Мне кажется, что нет смысла долго останавливаться на данном шаблоне даже новичку и на мой взгляд погружение в архитектуру вполне нормально начинать с MVP. Но если все таки есть желание попробовать руками MVC, то рекомендую книгу "Android. Программирование для профессионалов". В новых изданиях внимания этому шаблону уделяется вроде бы меньше, смотрел мельком, поэтому желательно рассмотреть этот вариант, если уж собрались, на основе более старых изданий, например, третьего.
MVP
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
Чуть более сложный для понимания шаблон, поэтому настоятельно рекомендую попробовать в первую очередь MVP. Забегу вперед и мотивирую: если освоить MVVM-шаблон, то по сути нужно будет дальше приложить минимальное усилие, чтоб познать MVI и собрать в голове полную копилку знаний об основных архитектурных паттернах проектирования Android-приложений. Первое и главное отличие в названиях слоев бизнес-логики: в MVP и его производных она хранится в Presenter, тогда как в MVVM эту роль исполняет ViewModel. Во вторых, это способ взаимодействия View-слоя и ViewModel. Это осложнено тем, что ViewModel не знает явно о View-слое, но при этом им управляет. Как такое может быть? Об этом чуть дальше.
Прежде чем начать разбираться с MVVM нужно обговорить более детально о видах взаимодействия View и ViewModel и вот какие они бывают:
- Прослушивание событий во ViewModel, происходящие во VIew, примеры: нажатие кнопки, изменение текста в EditText.
- Изменения во ViewModel, которые после configuration changes должны подтягиваться актуальные значения, примеры: пришли данные с сервера и нужно показать/скрыть виджет на View-слое, изменить текст в TextView.
- Исполнение View-слоем однократных событий по приказу ViewModel'и, пример: показать диалог, показать тост или перейти на другой фрагмент/экран.
Схематично ViewModel выглядит так:
class MainViewModel( val mainModel: MainContract.Model ) : ViewModel(), MainContract.ViewModel { ... }
Понятно, что связь View с ViewModel никуда не девается, не смотря на отсутствие в конструкторе View, она лишь усложняется и варианты управления View-слоем ViewModel'ю есть такие:
- связывание XML-кода и ViewModel'и через DataBinding;
- реализация шаблона проектирования Observer для отправки событий из ViewModel во View для изменения состояний отдельного View-widget'а или даже целиком View-слоя.
Далее немного более детально о том, как в каждом из способов разруливаются вышеназванные виды взаимодействия.
MVVM через DataBinding
DataBinding - библиотека, основанная на кодогенерации и содержит в себе Observable-классы, которые лежат во ViewModel и связываются напрямую с XML-разметкой. Подробнее о работе с событиями:
- Прослушивание событий происходит через реализацию функций напрямую в 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, по двум причинам:
- В DataBinding кодогенерация, что дает ощутимое замедление времени сборки проекта.
- Вчера 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.