Blog Infos
Author
Published
Topics
, , , ,
Published

Behind every tradition lies a reason — and behind that, a story.
– Anonymous

Welcome to this article! My goal is to help you truly understand what the real MVI pattern is, why I’m confident in my perspective, and where this pattern originally comes from. But before you dive in, a quick warning:

📝 This article comprises an extensive and deep research on the history and evolution of the MVI architectural pattern as well as the opinion of the author. Be ready to identify facts from opinions 😉

Table of Contents
  1. Introduction: Beyond the Buzzword
  2. Architectural Precursors: Patterns That Led to MVI
  3. The Origin of MVI: How the Pattern Took Shape
  4. The Problem: The MVI Misconception
  5. MVI in Android: How Does It Really Look?
  6. Final Thoughts: My Personal Take
1. Introduction: Beyond the Buzzword

MVI has earned its place as a powerful architectural pattern in Android development, offering clarity and predictability in managing state and UI complexity. When applied thoughtfully, it provides a strong mental model that helps developers build maintainable and scalable apps.

Yet, a faction I call the “MVI Police 👮” insists that only strict MVU or Redux-style implementations count as real MVI, dismissing other valid approaches as mislabeling. This narrow view overlooks MVI’s true origins and the practical ways it can be adapted to different contexts without losing its core principles.

In this article, we’ll explore MVI’s roots, the problems it was designed to solve, and how embracing its spirit — rather than rigid rules — can empower developers to write clean, effective architecture. After all, good architecture is about solving problems, not following dogma.

2. Architectural Precursors: Patterns That Led to MVI

MVI was not introduced in a paper nor a book we can refer to, like other design patterns. It was the result of a thoughtful process by its creator, who hold a case against the existing solutions on his time (123).

While many assume MVI is a direct descendant of Redux (2015) or Elm’s MVU (2011), its true conceptual roots lie in two earlier patterns: Model–View–Controller (MVC) and Flux according to this blog post from 2014 by André Staltz (the creator of cycle.js and the MVI pattern).

Model-View-Controller (MVC) — Circa 1979

One of the earliest and most influential architectural patterns, MVC separated an application into three main components:

  • Model: Manages the data and business logic
  • View: Responsible for rendering UI elements
  • Controller: Handles user input and updates the Model accordingly

Source: cycle.js.org

André Staltz revisited MVC through the lens of reactive programming. In his 2014 blog post “Reactive MVC and the Virtual DOM”, he proposed a modern rethinking of MVC tailored for stream-based UIs. In this reimagining:

  • The Intent replaces the Controller: it captures user interactions as streams of events.
  • The Model reacts to these streams to update application state.
  • The View renders the state reactively.

He called this restructured architecture Model–View–Intent (MVI), making it the first formal mention of the pattern. This version of MVC respected unidirectional data flow while aligning naturally with observable-based systems like RxJS.

Flux (MVC) — May 2014

Shortly before Staltz’s blog post, Facebook had introduced Flux alongside React. While React focused on UI rendering, Flux introduced a new architecture for managing data flow:

  • Actions describe what happened.
  • Dispatcher routes actions to stores.
  • Stores hold application state and update in response to actions.
  • Views listen for store changes and re-render.

source: reactjs.org

Flux’s most important innovation was unidirectional and circular data flow, which brought order to the chaos of shared mutable state in UI apps. Staltz recognized this innovation but criticized how Flux often mixed imperative and reactive paradigms. Still, the principle of keeping state changes predictable and linear became a key part of MVI.

In MVI, Staltz adopted Flux’s unidirectional data flow but simplified it. He removed the Dispatcher, avoided imperative stores, and instead used observables to directly model user intent, state, and UI rendering as pure, reactive transformations. In his own words:

“The combo React/Flux is clearly inspired by Reactive Programming principles, but the API and architecture are an unjustified mix of Interactive and Reactive patterns… we can do better.”

3. The Origin of MVI: How the Pattern Took Shape

The MVI pattern was introduced in the javascript framework cycle.js by its creator André Staltz around the year 2014, one year before Redux came to existence.

The MVI Pattern in Practice

Model-View-Intent (MVI) is a reactive UI architecture pattern that enforces a single, immutable state (the Model), a unidirectional flow of Intents (user actions) into the system, and a View that renders each state.

  • Model holds the app state.
  • View renders the state.
  • Intent replaces the Controller as a stream of user interactions.

The MVI Cycle

You can read the definition of MVI as well as its components directly from its creator in this blog post about architectural patterns he wrote back in 2015.

Flux’s focus on immutable state and unidirectional flow influenced MVI’s design, which formalizes the flow: user intents → state changes → UI rendering. This synthesis of ideas from classic MVC, Flux, and reactive programming principles gave birth to MVI — not as a derivative of Redux (which arrived later) or Elm, but as a distinct architecture tailored for reactive systems.

Code Examples

In a blog post Hannes Dorfmann gave his interpretation of the earliest ideas of André Staltz about MVI and he expressed the idea as a math expression:

source: hannesdorfmann.com

This expression is great to understand how the MVI components are intertwined and represent a dependency to each other: The intent is passed to the Model which generates a new State that is provided to the View to render it. This mathematical expression condenses the spirit of MVI.

The cycle.js implementation for this would be something like:

import xs from 'xstream';
import { run } from '@cycle/run';
import { div, button, p, makeDOMDriver } from '@cycle/dom';
// Intent: capture user actions as streams
function intent(DOMSource) {
const increment$ = DOMSource.select('.increment').events('click')
.mapTo({ type: 'INCREMENT' });
const decrement$ = DOMSource.select('.decrement').events('click')
.mapTo({ type: 'DECREMENT' });
return xs.merge(increment$, decrement$);
}
// Model: update the state based on actions
function model(action$) {
const initialState = 0;
return action$.fold((state, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}, initialState);
}
// View: render the UI from the current state
function view(state$) {
return state$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p(`Count: ${count}`)
])
);
}
// Main: glue everything together
function main(sources) {
const action$ = intent(sources.DOM);
const state$ = model(action$);
const vdom$ = view(state$);
return {
DOM: vdom$
};
}
run(main, {
DOM: makeDOMDriver('#app')
});
view raw mvi_cyclejs.js hosted with ❤ by GitHub

You can see this math expression literally represented in lines 44–46.

This looks good, but how does the idea actually translate to Kotlin?

Well, not so fast, we’ll come to that. For now, let’s continue our unveiling journey…

4. The Problem: The MVI Misconception

As the MVI pattern gained popularity in the Android ecosystem, particularly with the rise of Jetpack Compose, coroutines, and unidirectional state management, a wave of skepticism emerged. Some developers — jokingly referred to as the MVI Police — began questioning whether many Kotlin/Android implementations could truly be called “MVI.”

The MVI & Redux Misconception

One of the most common misconceptions is that MVI is just Redux under a different name, or that MVI was directly inspired by Redux. But as you’ve seen in the previous sections, this isn’t historically accurate: MVI was introduced in 2014 by André Staltz, in his Reactive MVC blog post, several months before Redux was released in late 2015 by Dan Abramov.

That said, it’s easy to understand where the confusion comes from. MVI and Redux do share several core ideas — but not because MVI was derived from Redux. Instead, both were independently influenced by the same underlying architecture: Flux.

These similarities include:

  • MVI treats user intents similarly to Redux actions.
  • MVI models state as immutable and evolving over time, like Redux’s store.
  • MVI uses reducers to derive new state from previous state and user input.

However, within these similarities lie key architectural differences:

By now you should agree on that a Kotlin/Android MVI implementation doesn’t need to strictly follow the Redux design pattern.

The MVI & MVVM Misconception

Another common misconception is that many popular MVI implementations are simply MVVM with a single state data class and a sealed interface for intents. To clarify this, let’s first revisit the fundamentals of what a design pattern truly is.

According to the seminal 1994 book Design Patterns: Elements of Reusable Object-Oriented Software by the Gang of Four:

“A design pattern systematically names, motivates, and explains a general design that addresses a recurring design problem in object-oriented systems.”

“A design pattern is a general repeatable solution to a commonly occurring problem in software design. It is not a finished design that can be transformed directly into code but is a description or template for how to solve a problem that can be used in many different situations.”

Much like patterns found in nature — whether numerical, visual, or otherwise — mutating or modifying an existing pattern often results in a new, distinct pattern:

So, are developers following the MVI trend actually doing MVVM without realizing it? To untangle this, let’s compare the similarities and differences between the two patterns, much like we did in the previous section.

To keep opinions grounded, we’ll reference a well-regarded Microsoft article — the creators of MVVM back in 2005.

Both patterns share several traits:

  • Clear separation of concerns
  • Use of observable streams to update the UI
  • Employment of ViewModels (especially on Android)

However, when we examine the details side-by-side, it becomes clear that despite these surface similarities, MVI and MVVM differ fundamentally in key ways — showing how evolving a pattern can lead to a brand-new architectural approach.

As software development often teaches us, things that look similar at first glance can diverge significantly once you look closer.

A clean and simple MVVM implementation in Android would look something like this:

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
// Collect state from the ViewModel
val username by viewModel.username.collectAsStateWithLifecycle()
val age by viewModel.age.collectAsStateWithLifecycle()
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = username,
onValueChange = { viewModel.onUsernameChanged(it) },
label = { Text("Username") }
)
Text(text = "Age: $age")
Button(onClick = { viewModel.onAgeIncrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Increase Age")
}
Button(onClick = { viewModel.onAgeDecrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Decrease Age")
}
}
}
view raw UserScreen.kt hosted with ❤ by GitHub
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
// Collect state from the ViewModel
val username by viewModel.username.collectAsStateWithLifecycle()
val age by viewModel.age.collectAsStateWithLifecycle()
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = username,
onValueChange = { viewModel.onUsernameChanged(it) },
label = { Text("Username") }
)
Text(text = "Age: $age")
Button(onClick = { viewModel.onAgeIncrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Increase Age")
}
Button(onClick = { viewModel.onAgeDecrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Decrease Age")
}
}
}
view raw UserScreen.kt hosted with ❤ by GitHub

This idea is supported by this diagram created by the Android team a while back in an effort to suggest an architectural pattern for the apps:

Note the multiple LiveData instances in the viewModel. Now to see an MVI example, let’s jump to the next section.

5. MVI in Android: How Does It Really Look?

After our deep dive and comparisons with related patterns, we can confidently identify the core characteristics that define a true MVI implementation :

  • Single immutable State object: This state is decentralized and typically resides within the ViewModel.
  • UI interactions as Intents: All user actions are represented as data streams, often sealed classes or interfaces.
  • Reducer function: A pure function that takes the previous state and an Intent, then returns a new State (Old State + Intent → New State).
  • Unidirectional data flow: The architecture strictly respects the flow from Intent → Model → State → View.
  • Side-effects handled separately: Effects like navigation, network calls, or showing messages are modeled distinctly and processed outside the reducer.

I can already hear some objections, but let’s put theory aside and see how this looks in code.

The MVI Components as models

For our example, we’ll define three essential components:

  • UserUiState: The immutable state model exposed as a Kotlin Flow.
  • UserIntent: Represents all possible user actions within the UI.
  • UserSideEffect: Defines side-effects that happen outside the pure state update logic.
// UI State data class (immutable)
data class UserUiState(
val username: String = "",
val age: Int = 0,
val isLoading: Boolean = false
)
// User intents: user actions
sealed interface UserIntent {
data class ChangeUsername(val newName: String) : UserIntent
object IncrementAge : UserIntent
object DecrementAge : UserIntent
}
// Side effects (one-time events)
sealed interface UserSideEffect {
data class ShowMessage(val message: String) : UserSideEffect
}
The ViewModel Role in MVI

In an Android MVI feature, the ViewModel is responsible for:

  • Exposing the state as an immutable, observable stream.
  • Exposing side effects as a separate immutable stream.
  • Providing a public function to accept and process Intents, applying the reducer logic to update the state accordingly.
class UserViewModel : ViewModel() {
private val _sideEffects = MutableSharedFlow<UserSideEffect>()
val sideEffects = _sideEffects.asSharedFlow()
private val _uiState = MutableStateFlow(UserUiState())
val uiState = _uiState.asStateFlow()
// Process a single intent immediately
fun processIntent(intent: UserIntent) {
when (intent) {
is UserIntent.ChangeUsername -> {
_uiState.update { it.copy(username = intent.newName) }
}
UserIntent.IncrementAge -> {
_uiState.update { it.copy(age = it.age + 1) }
viewModelScope.launch {
_sideEffects.emit(UserSideEffect.ShowMessage("Age increased!"))
}
}
UserIntent.DecrementAge -> {
if (current.age > 0) {
_uiState.update { current ->
current.copy(age = current.age - 1)
}
}
}
}
}
}

As you’ve probably realized by now, this closely resembles the very implementations that some have criticized as being “not real MVI.” But the historical context and evidence compiled throughout this article suggest otherwise: the community has been getting it right all along. Perhaps it’s time for the MVI Police to revisit their own definition of the pattern.

📝 Note: I don’t recommend this side effects implementation, but I’m keeping it simple for educational purposes. Read my article about best practices on one-off events to get an in-depth explanation on this topic.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

6. Final Thoughts: My Personal Take

Even though I never aligned with the “MVI Police” way of thinking, I realized I lacked solid information to support my views. This extensive investigation taught me several valuable lessons — some not even about design patterns or software engineering. I’d like to share a few with you:

  • In both software development and life, what sounds logical to you isn’t always true.
  • Concepts have a history and evolve over time; recognizing that process is crucial.
  • Before telling someone they’re wrong just because you believe your way is the only right way, think twice — sometimes even thrice. Multiple valid approaches can coexist.

On the technical side, the biggest takeaway is really about soft skills:

  • A design pattern is a recurring solution to a common problem. If that solution changes significantly, it becomes a new pattern.
  • Yes, changes like using a single immutable data class for state, a sealed interface for intents, and a reducer function inside the ViewModel can justify defining a distinct pattern.
  • Yes, you can tweak/personalize a pattern and still avoid telling the other people yours is the “real” or “best” implementation.
  • History matters. Many people who claim MVI was inspired by Redux may not even realize that MVI existed first.
  • Lastly, MVI implementations must not include a global store like Redux. Doing so means it’s not MVI. (Yes, I’m looking at you, MVI Police — bring it on! 🚓)
  • The MVI creator didn’t even like Redux in the first place
  • MVI is simple and straight to the point. Redux not so much…

At the end of the day, what truly matters is understanding the why behind these patterns, respecting their evolution, and applying them thoughtfully to solve real problems. Let’s embrace the diversity of ideas instead of policing the “right way.” This approach will foster healthier discussions and ultimately better software.

Follow me on Medium and LinkedIn to know when I write the article to show you how I personally leverage MVI in my projects.

If this article helped you, clap, comment and share 😉

Sources:

This article was previously published on proandroiddev.com.

Menu