Mobile Development 16 min read

Understanding and Implementing MVI Architecture in Android Applications

This article introduces Google's recommended MVI (Model-View-Intent) architecture for Android, compares it with MVVM, explains the benefits of unidirectional data flow, and provides practical Kotlin implementations—including LiveData state handling, single-event management, and step-by-step code examples—to guide developers in adopting MVI in mobile projects.

IEG Growth Platform Technology Team
IEG Growth Platform Technology Team
IEG Growth Platform Technology Team
Understanding and Implementing MVI Architecture in Android Applications

Preface

Google's Android app architecture guide now recommends the MVI (Model-View-Intent) pattern instead of the previous MVVM approach. MVI is the latest Google‑recommended architecture and is worth a deep dive.

Box is an internal e‑sports live‑stream product used at Tencent.

Review of MVVM‑AAC

Because MVI is similar to MVVM, we first briefly recap the non‑two‑way‑binding version of MVVM.

Process

View sends events (initialisation, clicks, etc.) to ViewModel.

ViewModel requests or updates data from Model.

ViewModel updates LiveData (states) with the latest data.

State changes drive the View to create or refresh UI elements.

The cycle repeats for new interactions.

What is MVI?

MVI (Model‑View‑Intent) is an architecture that follows reactive and stream‑processing ideas. It resembles MVVM but borrows concepts from front‑end frameworks, emphasizing a single source of truth and unidirectional data flow.

Unidirectional Data Flow

Data moves in a closed loop and never backwards. The flow consists of:

User actions are expressed as Intents and sent to the Model.

The Model updates the State based on the Intent.

The View receives the new State and refreshes the UI.

Why use unidirectional flow?

It enables separation of concerns by isolating the source, transformation, and consumption of state changes.

The benefits are:

Data consistency – the UI has a single trusted source.

Testability – state origins are independent and can be tested without the UI.

Maintainability – state changes follow a well‑defined pattern driven by user events and data fetches.

Android MVI

Process

View sends Intent events to ViewModel.

ViewModel fetches or updates data from the Data layer.

ViewModel updates ViewState with the new data.

ViewState changes drive the View to create or refresh UI elements.

The cycle repeats.

MVVM → MVI

MVI consolidates non‑single events and states from MVVM into a single flow, achieving a true unidirectional data model.

Intent: Encapsulates operations; the View creates an Intent and passes it to ViewModel, centralising event handling and decoupling the View from specific ViewModel methods.

ViewState: Represents the whole page state; the UI subscribes to a single ViewState, ensuring consistent data flow.

Pros and Cons of MVI vs MVVM

MVI Practical Implementation

While MVI unifies events and states, practical migration from MVVM introduces new challenges, such as handling one‑time events and avoiding unnecessary UI refreshes.

How to listen to LiveData properties for partial refresh?

When ViewState contains many fields, any change triggers a UI update, which can hurt performance. Using distinctUntilChanged() on LiveData limits updates to changed values.

fun <T, A> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    action: (A) -> Unit
) {
    this.map {
        StateTuple1(prop1.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a) ->
        action.invoke(a)
    }
}

Map converts State to the property value.

distinctUntilChanged() prevents callbacks for unchanged values.

KProperty1 requires the State data class to avoid obfuscation.

One‑time state events compatibility?

One‑time events (e.g., Toast, Dialog) wrapped in ViewState can be lost on configuration changes. MVVM uses SingleLiveEvent ; the same can be applied in MVI, but distinctUntilChanged may filter unchanged events.

Approach 1 – Mavericks style

Airbnb’s Mavericks component uses a uniqueOnly listening mode to separate one‑time events from regular state, preserving unidirectional flow while handling events centrally.

Advantages: Solves both problems and keeps a single ViewState.

Disadvantages: Highly custom, based on Kotlin Flow rather than LiveData, increasing learning and migration cost.

Approach 2 – Split LiveData

Separate regular state (MutableLiveData) from one‑time events (SingleLiveEvent wrapped in a sealed class). This reduces migration effort but breaks the single source of truth.

class MainViewModel : ViewModel() {
    private val _viewStates = MutableLiveData<MainViewState>(MainViewState())
    val viewStates = _viewStates.asLiveData()
    private val _viewEvents = SingleLiveEvents<MainViewEvent>()
    val viewEvents = _viewEvents.asLiveData()
}
// ViewState
data class MainViewState(
    val fetchStatus: FetchStatus = FetchStatus.NotFetched,
    val newsList: List<NewsItem> = emptyList()
)

// One‑time events
sealed class MainViewEvent {
    data class ShowSnackbar(val message: String) : MainViewEvent()
    data class ShowToast(val message: String) : MainViewEvent()
}

Advantages: Low migration cost.

Disadvantages: One‑time events need a separate channel; the pure single‑source principle is weakened.

Proposed Solution – SingleEventState wrapper

Wrap one‑time properties in SingleEventState that generates a unique diff tag each time, allowing distinctUntilChanged to fire even when the underlying value is unchanged.

data class SingleEventState<DATA>(val data: DATA,
    val diffTag: String? = if (data != null) System.currentTimeMillis().toString() + data.hashCode() else null) : ISingleEventState {
    override fun isNone(): Boolean = data == null
}
interface ISingleEventState { fun isNone(): Boolean }

Update observeState to ignore ISingleEventState.isNone() during initialisation.

fun <T, A> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    action: (A) -> Unit
) {
    this.map { StateTuple1(prop1.get(it)) }
        .distinctUntilChanged()
        .observe(lifecycleOwner) { (a) ->
            if (a is ISingleEventState && a.isNone()) return@observe
            action.invoke(a)
        }
}

Then define ViewState, Intent, and Event sealed classes, register observers in the Activity, and update ViewState in the ViewModel as shown in the original code snippets.

Conclusion

Using SingleEventState allows one‑time events to bypass the diff function while keeping normal ViewState updates unchanged, fully satisfying MVI principles with LiveData.

Summary Table

Approach

Pros

Cons

Adopt?

Mavericks

Unified ViewState management

Flow‑based, higher learning cost

No

MVI best‑practice (LiveData)

Low migration cost

Splits ViewState, breaks single source

No

Box internal proposal

Unified ViewState, low LiveData cost

None identified

Yes

Android MVI Overall Architecture Diagram

MVI Beyond Android

Compose and Flutter also adopt reactive/declarative UI patterns where UI rebuilds are driven by State changes. The same unidirectional data‑flow concepts apply, using ViewModel + LiveData on Android or Provider + Consumer on Flutter.

Further Reading

Android App Architecture Guide – https://developer.android.com/jetpack/guide

Google recommends MVI – https://mp.weixin.qq.com/s/dd5mfTVSt8WgW7aOOf-CDg?token=891227444&lang=zh_CN

MVI best practice with LiveData – https://app.yinxiang.com/fx/92b88756-9014-4fd5-9eee-88b8b29aeb73

architectureViewModelandroidKotlinLiveDataMVIUnidirectional data flow
IEG Growth Platform Technology Team
Written by

IEG Growth Platform Technology Team

Official account of Tencent IEG Growth Platform Technology Team, showcasing cutting‑edge achievements across front‑end, back‑end, client, algorithm, testing and other domains.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.