Author: Karishma Agrawal, Senior Android Engineer, Eventbrite India
At Eventbrite, our Android apps (Organizer App and Attendee App) are based on MVI [Model View Intent] architecture pattern that is often attributed to Cycle.js, a JavaScript framework developed by André Staltz. MVI has been adopted and adapted by various developers and communities across different programming languages and platforms. This article will throw light on MVI architecture, its benefits and how different it is from MVVM.
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.
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.
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.
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 the Eventbrite app. Let’s apply the concept of Model — View — Intent.
This is an Event Detail page in the Attendee app. Users 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:
- Loading
- Content
- 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 the 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 follows 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 the 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 the 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. It’s 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.