Шесть месяцев назад я пришла в Eventbrite на должность Senior Android инженера. Проработав здесь шесть месяцев, я поняла, что Eventbrite — это не просто компания, основанная на продукте, но и настоящая технологическая компания. То, какая у нас архитектура, и то, с какими блестящими умами мне приходится работать, радует меня каждый день.
Eventbrite — это глобальный рынок событий, который позволяет всем желающим создавать, делиться, находить и посещать мероприятия, которые подпитывают их страсти и обогащают их жизнь. От музыкальных фестивалей, марафонов, конференций, общественных митингов и сборов средств до игровых соревнований и конкурсов игры на гитаре. Наша миссия — объединить мир с помощью живого опыта.
У нас есть два основных приложения для Eventbrite:
- Eventbrite Organizer: Это приложение предназначено для создателей, которые хотят разместить свое мероприятие на Eventbrite. Это приложение помогает нашим создателям управлять мероприятиями на ходу — создавать и редактировать их, отслеживать и продавать билеты, а также регистрировать гостей.
- Eventbrite Attendee App: Это приложение предназначено для посетителей, которые хотят найти и посетить мероприятие поблизости. Они могут забронировать билеты с помощью этой платформы.
Приложение Eventbrite для Android основано на архитектуре MVI. В этой статье я расскажу, что такое архитектура MVI, чем она отличается от MVVM, в чем ее преимущества и как мы можем реализовать ее в нашем приложении. Я также приведу пример, в котором мы проверяем мероприятие в приложении для посетителей Eventbrite.
MVI [Model View Intent]
Архитектурный паттерн Model-View-Intent (MVI) часто приписывают Cycle.js, JavaScript-фреймворку, разработанному Андре Стальцем. Однако MVI был принят и адаптирован различными разработчиками и сообществами на разных языках программирования и платформах.
Вы также можете посмотреть это видео, чтобы понять его.
В Android архитектура была признана после статьи Ханнса Дорфмана. Он подробно рассказал об архитектуре MVI в своем блоге.
- Model: Модель представляет данные и бизнес-логику приложения. В MVI модель неизменяема и представляет собой текущее состояние приложения.
- View: Представление отвечает за отрисовку пользовательского интерфейса и реакцию на ввод данных пользователем. Однако, в отличие от MVVM и MVC, представление в MVI является пассивным компонентом. Он не взаимодействует с моделью напрямую и не принимает решений на основе данных. Вместо этого он получает обновления состояния и пользовательские намерения от ViewModel.
- Intent: Намерение представляет собой действия пользователя или события, происходящие в пользовательском интерфейсе, такие как нажатие кнопки или ввод текста. В MVI эти намерения перехватываются представлением и отправляются во ViewModel для обработки.
- ViewModel: В MVI она отвечает за управление состоянием приложения и бизнес-логикой. Она получает пользовательские намерения от представления, обрабатывает их и соответствующим образом обновляет модель. Затем ViewModel выдает новое состояние, которое View наблюдает и отображает.
Давайте разберем MVI на примере приложения Eventbrite и применим концепцию Model — View — Intent.
Это страница события в приложении Eventbrite для посетителей. Пользователи обычно попадают сюда, найдя мероприятие в нашем каталоге событий или выполнив поиск по нему.
На этой странице отображаются такие сведения о событии, как его название, дата и время проведения, место проведения, организатор, описание события и т.д. Кроме того, здесь есть интерактивные элементы, например: Like, Unlike, Share, Follow Creator, Get Tickets и т.д.
Давайте пошагово разберем его реализацию с помощью MVI.
Model
ViewState
Преимущество перед MVVM
Управление состоянием: MVI обеспечивает четкий и централизованный подход к управлению состоянием приложения. Определяя состояние в виде неизменяемой модели и обрабатывая обновления состояния в ViewModel, MVI снижает сложность управления изменениями состояния, по сравнению с MVVM, где управление состоянием может стать фрагментированным для нескольких ViewModel.
Для нашей страницы с подробным описанием событий мы можем иметь следующие состояния
- Загрузка
- Контент
- Ошибка
Это три основных состояния для каждого экрана.
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 мы проверим состояние и загрузим представление соответствующим образом:
@Composable internal fun Screen( state: State, ) { when (state) { is State.Loading -> Loading() is State.Error -> Error(state.error) is State.Content -> Content(state.event) } }
Теперь, когда вы хотите изменить пользовательский интерфейс, вы не меняете его напрямую, а обращаетесь к состоянию, а пользовательский интерфейс наблюдает за состоянием, чтобы внести изменения.
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() }
Давайте разберемся в каждом событии по отдельности.
Загрузка события:
Загрузка — это начальное событие, которое запускается из фрагмента. В OnCreate
мы устанавливаем наши события. И начальным событием является Load
, которое обрабатывается ViewModel.
override suspend fun handleEvent(event: Event) { when (event) { is Event.Load -> load() } }
В функции загрузки мы получаем данные о мероприятии с сервера. При успехе или ошибке этого API мы изменяем состояние UI, за которым наблюдает 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
Редуктор состояния — это концепция из функционального программирования, которая принимает предыдущее состояние на вход и вычисляет новое состояние из предыдущего.
Давайте разберем это на примере функции, в которой пользователь следует за создателем, и что происходит, когда пользователь нажимает на кнопку Follow.
Сначала у нас есть модель UI, которая содержит состояние контента, и с помощью этого объекта мы отображаем данные в 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
Если организатор уже зафоловлен, анфоловим его, в противном случае подписываемся на него:
if (followableOrganizer.isFollowed) { state { onUnfollow(::event, ::effect) } } else { state { onFollow(::event, ::effect) } }
Действие 3: редуктор
Действия onUnfollow
и onFollow
обрабатываются редуктором, который получает состояние предыдущее и изменяет его, а затем отправляет обратно в представление:
private fun getFollowContent( event: UiModel, newState: Boolean,//Shows Following or 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 и изменяем пользовательский интерфейс
Заключение
В заключение хочу сказать, что внедрение архитектуры Model-View-Intent (MVI) в Eventbrite не только улучшило наше приложение для Android, но и упростило процесс разработки. Приняв MVI, мы оптимизировали управление состояниями, улучшили поток данных и обеспечили более предсказуемое и последовательное поведение наших приложений.
Ключевые преимущества MVI по сравнению с традиционными архитектурами, такими как MVVM, очевидны. С MVI мы получаем преимущества от четкого и централизованного подхода к управлению состоянием, где Модель представляет неизменяемое состояние приложения, Представление пассивно отображает пользовательский интерфейс на основе обновлений состояния, а Намерение беспрепятственно фиксирует действия пользователя. Такой однонаправленный поток данных упрощает движение данных и событий, облегчая рассуждения о поведении нашего приложения и снижая сложность, часто связанную с управлением изменениями состояния в MVVM.
Более того, реализация MVI в нашем приложении Eventbrite, как показано на примере страницы Event Detail, демонстрирует его практичность и эффективность. Определяя четкие состояния, обрабатывая события и используя редукторы для вычисления новых состояний, мы добились более эффективной и удобной в обслуживании кодовой базы.
Таким образом, внедрение архитектуры MVI не только позволило нам создавать надежные и масштабируемые приложения для Android в Eventbrite, но и создало прецедент для упрощения процессов разработки. Четкое разделение задач, предсказуемый поток данных и централизованное управление состояниями делают ее ценной парадигмой, которую каждый разработчик должен рассмотреть возможность внедрения в свои проекты. С MVI путь к созданию исключительного пользовательского опыта с помощью интуитивно понятных и хорошо структурированных приложений становится более понятным и достижимым.