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

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

Eventbrite предлагает два продукта.

  1. Приложение “Organizer” (“Организатор”), предназначенное для тех, кто разрабатывает мероприятия и хочет проводить их на Eventbrite. Это приложение помогает управлять всем ходом организации мероприятия — создавать и редактировать его, распространять и отслеживать продажи билетов, а также регистрировать гостей.
  1. Приложение “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, но и создало прецедент для упрощения процессов разработки в целом. Четкое разделение задач, предсказуемый поток данных и централизованное управление состоянием делают эту архитектуру ценным инструментом.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Karishma Agrawal: MVI at Eventbrite

Предыдущая статьяМониторинг приложения Golang с Prometheus, Grafana, New Relic и Sentry