Site icon AppTractor

MVI в Eventbrite

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

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

У нас есть два основных приложения для Eventbrite:

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

MVI [Model View Intent]

Архитектурный паттерн Model-View-Intent (MVI) часто приписывают Cycle.js, JavaScript-фреймворку, разработанному Андре Стальцем. Однако MVI был принят и адаптирован различными разработчиками и сообществами на разных языках программирования и платформах.

Вы также можете посмотреть это видео, чтобы понять его.

В Android архитектура была признана после статьи Ханнса Дорфмана. Он подробно рассказал об архитектуре MVI в своем блоге.

Давайте разберем MVI на примере приложения Eventbrite и применим концепцию Model — View — Intent.

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

На этой странице отображаются такие сведения о событии, как его название, дата и время проведения, место проведения, организатор, описание события и т.д. Кроме того, здесь есть интерактивные элементы, например: Like, Unlike, Share, Follow Creator, Get Tickets и т.д.

Давайте пошагово разберем его реализацию с помощью MVI.

Model

ViewState

Преимущество перед MVVM

Управление состоянием: MVI обеспечивает четкий и централизованный подход к управлению состоянием приложения. Определяя состояние в виде неизменяемой модели и обрабатывая обновления состояния в ViewModel, MVI снижает сложность управления изменениями состояния, по сравнению с MVVM, где управление состоянием может стать фрагментированным для нескольких ViewModel.

Для нашей страницы с подробным описанием событий мы можем иметь следующие состояния

  1. Загрузка
  2. Контент
  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 мы проверим состояние и загрузим представление соответствующим образом:

@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 путь к созданию исключительного пользовательского опыта с помощью интуитивно понятных и хорошо структурированных приложений становится более понятным и достижимым.

Источник

Еще про MVI

Exit mobile version