Полгода назад я пришел в Eventbrite на должность старшего инженера Android. Проработав здесь полгода, я понял, что эта компания ориентирована не только на продукты, но и на технологии. То, какая у нас архитектура, и то, с какими блестящими умами мне приходится работать, радует меня каждый день.
Eventbrite — это глобальная интерактивная платформа для продажи билетов на живые мероприятия. Она позволяет всем желающим создавать, продвигать, находить и посещать мероприятия, которые доставляют им удовольствие и обогащают жизненный опыт. Речь идет об организации самых разных событий — от музыкальных фестивалей, марафонов, конференций, общественных собраний и акций по сбору средств до киберспортивных турниров и конкурсов игры на воздушной гитаре. Наша миссия — объединить мир с помощью живого опыта.
Eventbrite предлагает два продукта.
- Приложение “Organizer” (“Организатор”), предназначенное для тех, кто разрабатывает мероприятия и хочет проводить их на Eventbrite. Это приложение помогает управлять всем ходом организации мероприятия — создавать и редактировать его, распространять и отслеживать продажи билетов, а также регистрировать гостей.
- Приложение “Attendee” (“Участник”), предназначенное для посетителей, которые хотят посетить мероприятие, проходящее поблизости. Они могут забронировать билеты с помощью этой платформы.
На платформе Eventbrite Android-приложения основаны на архитектуре MVI. В этой статье я расскажу, что такое архитектура MVI, чем она отличается от MVVM и в чем ее преимущества. Кроме того, вы узнаете, как имплементировать ее в приложение на примере одной из страниц приложения “Attendee”.
MVI [Model View Intent]
Архитектурный паттерн MVI (Model-View-Intent — модель-представление-намерение) часто приписывают Cycle.js — JavaScript-фреймворку, разработанному Андре Стальцем. Однако MVI был принят и адаптирован различными разработчиками и сообществами на разных языках программирования и платформах.
В Android этот паттерн был признан после выхода статьи Ханнеса Дорфмана. Он подробно рассказал об архитектуре MVI в своих статьях, с которыми можно ознакомиться здесь.
Рассмотрим компоненты MVI.
- Model (модель). Представляет данные и бизнес-логику приложения. В MVI Model не изменяется, представляя собой текущее состояние приложения.
- View (представление). Отвечает за рендеринг пользовательского интерфейса и реакцию на ввод данных пользователем. Однако, в отличие от MVVM (Model-View-ViewModel) и MVC (Model-View-Controller), View в MVI является пассивным компонентом. Он не взаимодействует с Model напрямую и не принимает решений на основе данных. Вместо этого он получает обновления состояния и пользовательские намерения от ViewModel.
- Intent (намерение). Представляет собой действия пользователя или события, происходящие в пользовательском интерфейсе, такие как нажатие кнопки и ввод текста. В MVI эти намерения перехватывает View и отправляет во ViewModel для обработки.
- ViewModel. ViewModel в MVI отвечает за управление состоянием приложения и бизнес-логикой. ViewModel получает пользовательские намерения от View, обрабатывает их и соответствующим образом обновляет Model. Затем ViewModel выдает новое состояние, которое View отслеживает и отображает.
Рассмотрим архитектуру MVI на примере потока данных в приложении Eventbrite. Применим концепцию Model-View-Intent.
Это страница “Event Detail” (“Детали мероприятия”) в приложении “Attendee”. Пользователь может получить доступ к этой странице, как правило, из двух мест: из раздела “Event list” (“Перечень мероприятий”) и из поиска.
На этой странице отображаются такие сведения о мероприятии, как название, дата, время, место, организатор и краткое описание. Кроме того, здесь размещено несколько кнопок, включая Like (нравится), Unlike (не нравится), Share (поделиться), Follow Creator (подписаться на организатора) и Get Tickets (купить билеты).
Разберем шаг за шагом, как все это реализуется с помощью MVI.
# Model
ViewState
Преимущество перед MVVM
Управление состоянием. MVI обеспечивает четкий и централизованный подход к управлению состоянием приложения. Представляя состояние в виде неизменного компонента Model и обрабатывая обновления состояния в ViewModel, MVI снижает сложность управления изменениями состояния в отличие от MVVM, где управление состоянием может стать фрагментированным для нескольких ViewModel.
Для страницы “Event Detail” характерны следующие состояния:
- Loading (загрузка);
- Content (контент);
- Error (ошибка).
Это 3 основных состояния для каждого экрана.
internal sealed class ViewState { @Immutable class Loading(val onBackPressed: () -> Unit = {}) : ViewState() @Immutable class Content(val event: UiModel) : ViewState() @Immutable class Error(val error: ErrorUiModel): ViewState() }
Начальное состояние для экрана — Loading. Показываем индикатор выполнения, пока не закончим получать данные о событиях с сервера.
В Compose проверим состояние и загрузим View соответствующим образом.
@Composable internal fun Screen( state: State, ) { when (state) { is State.Loading -> Loading() is State.Error -> Error(state.error) is State.Content -> Content(state.event) } }
Теперь при намерении изменить UI сделать это напрямую нельзя. Нужно сообщить об этом намерении состоянию, и UI будет наблюдать за состоянием, чтобы внести изменения.
# Intent
События
Преимущество перед MVVM
Поток данных. В MVI однонаправленность информационного потока View → ViewModel → Model упрощает движение данных и событий в приложении. Это обеспечивает предсказуемое и последовательное поведение, облегчая его понимание в отличие от двунаправленной привязки данных в MVVM.
Событие — это sealed-класс, определяющий действие.
sealed class Event {
data object Load : Event()
class FetchEventError(val error: NetworkFailure) : Event()
class FetchEventSuccess(val event: ListingEvent) : Event()
class Liked(val event: LikeableEvent) : Event()
class Disliked(val event: LikeableEvent) : Event()
class FollowPressed(val user: FollowableOrganizer) : Event()
}
Разберемся с каждым событием по отдельности.
Load (событие загрузки):
Load — начальное событие, запускаемое из Фрагмента. В OnCreate устанавливаем события. Начальным событием является Load, которое обрабатывается ViewModel.
override suspend fun handleEvent(event: Event) {
when (event) {
is Event.Load -> load()
}
}
В функции Load получаем данные о событии с сервера. При успехе (API Success) или ошибке (API Error) изменяем состояние пользовательского интерфейса (UI State), которое наблюдается UI и обновляется UI соответствующим образом.
getEventDetail.fetch(eventId)
.fold({ error ->
state {
ViewState.Error(
error = error.toUiModel(events)
}
}) { response ->
state { ViewState.Content(event.toUiModel(events, effect)) }
}
Получение изменений в View:
internal fun EventDetailScreen(
state: ViewState
) {
when (state) {
is ViewState.Loading -> Loading()
is ViewState.Error -> Error(state.error)
is ViewState.Content -> Content(state.event)
}
}
# Reducer
State Reducer (редьюсер состояния) — концепт из функционального программирования, который принимает на входе предыдущее состояние и вычисляет новое состояние из предыдущего.
Разберем это на примере функции кнопки “Follow Creator” (подписаться на организатора) и посмотрим, что происходит в приложении “Attendee”, когда пользователь нажимает на эту кнопку.
Изначально у нас есть UIModel, который содержит состояние контента, и с помощью этого объекта данные отображаются данные в UI.
internal data class UiModel(
val eventTitle: String,
val date: String,
val location: String,
val summary: String,
val organizerInfo: OrganizerState,
val onShareClick: () -> Unit,
val onFollowClick: () -> Unit
)
Теперь разберемся в этом пошагово.
Действие 1: реализация слушателя User Click и триггерного события
onClick { events(EventDetailEvents.FollowPressed(followableOrganizer)) }
Действие 2: обработка события во ViewModel
Если пользователь уже подписан на организатора, то событие будет определено как onUnfollow (отписаться), в противном случае — onFollow (подписаться).
if (followableOrganizer.isFollowed) { state { onUnfollow(::event, ::effect) } } else { state { onFollow(::event, ::effect) } }
Действие 3: редьюсер
События onUnFollow и onFollow обрабатываются редьюсером, который получает предыдущее состояние, изменяет его и отправляет обратно в View.
private fun getFollowContent(
event: UiModel,
newState: Boolean,//Показывает Following или UnFOllowing
events: (Event) -> Unit
) = ViewState.Content(
event.copy(
organizerState = with((event.organizerState as OrganizerState)) {
val hasChanged = newState != isFollowing
OrganizerState.Content(copy(
isFollowing = newState,
listeners = OrganizerListeners(
onFollowUnfollow = {
val followableUser = event.toFollowableModel(newState, it.toBookmarkCategory())
events(Event.FollowPressed(followableUser))
}
)
)
)
}
)
)
getFollowContent возвращает состояние View.
Действие 4: возврат состояния View из ViewModel
state { onUnfollow(::event, ::effect) }
Действие 5: наблюдение за изменением в View и изменение UI
Заключение
Внедрение архитектуры Model-View-Intent (MVI) в Eventbrite не только улучшило наше Android-приложение, но и упростило процесс разработки. Приняв MVI, мы оптимизировали управление состояниями, оптимизировали поток данных и обеспечили более предсказуемое и последовательное поведение приложений.
Ключевые преимущества MVI по сравнению с традиционными архитектурами, такими как MVVM, очевидны. MVI обеспечивает четкий централизованный подход к управлению состоянием, где Model представляет неизменное состояние приложения, View пассивно отображает пользовательский интерфейс на основе обновлений состояния, а Intent беспрепятственно фиксирует действия пользователя. Такой однонаправленный поток данных упрощает движение данных и событий, облегчая понимание поведения приложения и снижая сложность, часто связанную с управлением изменениями состояния в MVVM.
Более того, реализация MVI в приложении Eventbrite, продемонстрированная на примере страницы “Event Detail”, убеждает в практичности и эффективности этой архитектуры. Определяя четкие состояния, обрабатывая события и используя редьюсеры для вычисления новых состояний, мы добились более эффективной и удобной в обслуживании кодовой базы.
Таким образом, внедрение архитектуры MVI не только позволило разработать надежные и масштабируемые Android-приложения в Eventbrite, но и создало прецедент для упрощения процессов разработки в целом. Четкое разделение задач, предсказуемый поток данных и централизованное управление состоянием делают эту архитектуру ценным инструментом.
Читайте также:
- Миграция UI-ориентированной библиотеки Android на Compose Multiplatform (Android/iOS)
- Атака Activity hopping: угроза безопасности
- Изучаем AndroidManifest.xml: <service> как подэлемент <application>
Читайте нас в Telegram, VK и Дзен
Перевод статьи Karishma Agrawal: MVI at Eventbrite