MVI at Eventbrite

Karishma Agrawal
Level Up Coding
Published in
8 min readMay 7, 2024

--

6 months ago I joined Eventbrite as Senior Android Engineer. After working for 6 months here, one thing that I found is, this is just not a product-based company but it is a tech-based company too. The kind of architecture we have and the kind of brilliant minds I get to work with excites me every day.

Eventbrite is a global self-service ticketing platform for live experiences that allows anyone to create, share, find, and attend events that fuel their passions and enrich their lives. From music festivals, marathons, conferences, community rallies, and fundraisers, to gaming competitions and air guitar contests. Our mission is to bring the world together through live experiences.

We have two products at Eventbrite:

  1. Organizer App : This app is for creators, who want to host their event on Eventbrite. This app helps our creators to Manage events on the go — create & edit, track & sell tickets, & check-in guests.
  2. Attendee App : This app is for attendees who want to attend an event nearby. They can book their tickets using this platform.

At Eventbrite, our Android app is based on MVI architecture. In this article I will explain what is MVI architecture, and how is it diff from MVVM. What are the benefits of it. And how we can implement it into our app with an example where we are checking out an event on attendee app.

MVI[Model View Intent]

The Model-View-Intent (MVI) architecture pattern is often attributed to Cycle.js, a JavaScript framework developed by André Staltz. However, MVI has been adopted and adapted by various developers and communities across different programming languages and platforms.

You can also check out this video to understand it.

In Android, it was recognized after Hannes Dorfmann's article. They have explained extensively about MVI architecture in their blogs. Which you can access from Here

  1. Model: The Model represents the data and business logic of the application. In MVI, the Model is immutable and represents the current state of the application.
  2. View: The View is responsible for rendering the UI and reacting to user input. However, unlike MVVM and MVC, the View in MVI is a passive component. It does not directly interact with the Model or make decisions based on the data. Instead, it receives state updates and user intents from the ViewModel.
  3. Intent: The Intent represents user actions or events that occur in the UI, such as button clicks or text input. In MVI, these intents are captured by the View and sent to the ViewModel for processing.
  4. ViewModel: The ViewModel in MVI is responsible for managing the application state and business logic. It receives user intents from the View, processes them, and updates the Model accordingly. The ViewModel then emits a new state, which the View observes and renders.

Let’s understand MVI with a flow from Eventbrite app. Let’s apply concept of Model — View — Intent.

This is an Event Detail page in Attendee app. User can access this page from typically two places, one Event list and another from search.

This page shows details about an event like Name, Date , time , place, hosted by, summary of event. And there are some clicks: Like, Unlike, share, Follow Creator, Get Tickets, etc.

Let’s understand its implementation step by step using MVI.

# Model

ViewState

Advantage Over MVVM
State management
: MVI provides a clear and centralized approach to managing the application state. By representing the state as an immutable Model and handling state updates in the ViewModel, MVI reduces the complexity of managing state changes, compared to MVVM where state management can become fragmented across multiple ViewModels.

For our Event Detail page we can have following states:

  1. Loading
  2. Content
  3. Error

These are 3 basic states for each screen.

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()

}

The initial State for Screen is Loading. We will show a progress bar until we are not finished fetching event details from the server.

In Compose we will check the state and Load View accordingly

@Composable
internal fun Screen(
state: State,
) {
when (state) {
is State.Loading -> Loading()
is State.Error -> Error(state.error)
is State.Content -> Content(state.event)
}
}

So now whenever you want to change UI, you don't change it directly but communicate to the state and UI will Observe the state to make changes.

# Intent

Events

Advantage Over MVVM
Data flow
: In MVI, the unidirectional data flow from View to ViewModel to Model simplifies the flow of data and events in the application. This ensures a predictable and consistent behavior, making it easier to reason about the application’s behavior compared to the bidirectional data binding in MVVM.

An event is a sealed class that defines the action.

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()
}

Let’s understand each event one by one.

Load Event:

Load is an initial event, which gets triggered from Fragment. In OnCreate, we are setting our events. And initial event is Load, which is handled by ViewModel.

 override suspend fun handleEvent(event: Event) {
when (event) {
is Event.Load -> load()
}
}

In the Load function then we are fetching event details from server. On Success or error of this API we change Ui State, which gets observed by UI and UI gets updated Accordingly.

 getEventDetail.fetch(eventId)
.fold({ error ->
state {
ViewState.Error(
error = error.toUiModel(events)
}
}) { response ->
state { ViewState.Content(event.toUiModel(events, effect)) }
}

Receive changes in 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 is a concept from functional programming that takes the previous state as input and computes a new state from the previous state

Let’s understand this with a feature where Attendee Follow’s a creator, what happens when the user clicks on follow.

FIrst we have an UiModel which contains content state, and using this object we show data on 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
)

Now let’s understand this step by step:

Action 1: Implement User Click listener and trigger event

onClick {
events(EventDetailEvents.FollowPressed(followableOrganizer))
}

Action 2: Handle Event In ViewModel

If Organizer is Already followed then unFollow them Otherwise Follow them.

if (followableOrganizer.isFollowed) {
state { onUnfollow(::event, ::effect) }
} else {
state { onFollow(::event, ::effect) }
}

Action 3: Reducer

onUnFOllow and onFollow is handled by reducer, where it is getting the prev state and modifying it and then sending back to view.

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 is returning View state

Action 4: Return View state from view Model

state { onUnfollow(::event, ::effect) }

Action 5: Observe this change in View and modify the UI

Conclusion

In conclusion, adopting the Model-View-Intent (MVI) architecture at Eventbrite has not only enhanced our Android app but also simplified the development process. By embracing MVI, we’ve streamlined state management, improved data flow, and ensured a more predictable and consistent behavior within our applications.

The key advantages of MVI over traditional architectures like MVVM are evident. With MVI, we benefit from a clear and centralized approach to state management, where the Model represents the immutable state of the application, the View renders the UI passively based on state updates, and the Intent captures user actions seamlessly. This unidirectional data flow simplifies the flow of data and events, making it easier to reason about our app’s behavior and reducing the complexity often associated with managing state changes in MVVM.

Moreover, the implementation of MVI within our Eventbrite app, as demonstrated through the Event Detail page example, showcases its practicality and effectiveness. By defining clear states, handling events, and employing reducers to compute new states, we’ve achieved a more efficient and maintainable codebase.

In summary, the adoption of MVI architecture has not only empowered us to build robust and scalable Android apps at Eventbrite but also sets a precedent for simplifying development processes across the board. Its clear separation of concerns, predictable data flow, and centralized state management make it a valuable paradigm that every developer should consider incorporating into their projects. With MVI, the path to creating exceptional user experiences through intuitive and well-structured applications becomes clearer and more attainable.

Your feedbacks are important for us to keep improving so let them coming.

I hope this article was helpful to you. If you have any suggestions or comments please let me know in the comment section or you can write me back at karishma.agr1996@gmail.com .

Your claps are appreciated to help others find this article 😃 .

--

--

Android Developer @Eventbrite | Wanted to be a writer so I write code now | Reader