Skip to content

When We Talk About MVP, MVVM, and MVI — What Are We Really Talking About?

💡 Introduction

  • When we talk about MVP, MVVM, or MVI,

  • What we are really discussing is:

    • A UI layer architecture pattern (Presentation Layer Architecture Pattern)
    • Specifically, how responsibilities are divided and how data flows within the presentation layer.
  • These are forms of decoupled architectural patterns for managing UI data flow.


📦 Background: Why Do We Need These Patterns?

  • In early UI development (whether Android, iOS, or Web), a common issue was:

    • All logic lived in one massive controller, Activity, or ViewController.
  • This led to:

    • Heavy coupling between UI and business logic;
    • Poor testability;
    • Low reusability;
    • Difficult state management.

Hence, the goal of architectural patterns is to:

  • Structure the UI layer more clearly,
  • Clarify data flow, and
  • Make state management predictable and controllable.

MVC

  • The most traditional approach is MVC.

    • In Android’s view system, layout files are defined in XML, while Activity or Fragment act as the Controller, responsible for refreshing UI data.
    • However, due to the limited capability of XML layouts, many UI behaviors have to be implemented directly inside Activity/Fragment, making the Controller also responsible for part of the View logic.
    • If data fetching also stays inside Activity/Fragment, the code becomes bloated and difficult to maintain.
  • Therefore, Google officially recommended using MVP as a replacement for traditional MVC.


MVP

  • MVP uses contracts and interfaces to implement data callbacks between layers.
  • In MVP, the Presenter takes over data generation from the View, requests data through the service layer, and returns the result back to the View for rendering.
  • However, this pattern has its own drawbacks.

While many argue that the Presenter becomes too heavy, this is not an inherent problem of MVP itself. It’s actually because developers didn’t properly separate the data control layer into smaller components such as Repository, UseCase, etc. This fine-grained separation is necessary even in MVVM.

In my opinion, the main reason MVP gradually faded out is:

Presenter–View dependency injection and lifecycle management had to be done manually.

This made MVP fragile, depending heavily on how each team understood and implemented it.

Common issues:

  • Some developers implemented MVP too simplistically:

    • They only set up the data callback (Presenter → View) without properly binding the lifecycle.
    • As a result, when a View is destroyed while a network request is still ongoing, the Presenter might try to update a destroyed View, causing NullPointerExceptions or memory leaks.
  • In Kotlin, null-safety reduces crashes, but the leaks and potential ANRs still exist.

  • Another pain point: MVP relies on a large number of interfaces for callbacks. While this is idiomatic Java, it makes tracing business logic cumbersome and verbose.

To solve these problems, Google introduced ViewModel in Jetpack — a lifecycle-aware component with a much simpler usage pattern.

For a detailed implementation of MVP, see here.


MVVM

  • MVVM is a unidirectional data structure that implements data callbacks using the Observer pattern.
  • Its core structure (illustrated in the diagram below) varies slightly depending on interpretation — adapt it according to your own business context. ..mvvm-architecture-framework

Creating a ViewModel is simple. Following Google’s official recommendation, it should be instantiated with lifecycle awareness using a factory:

Local lifecycle

kotlin
private val viewModel: MyViewModel by lazy {
    ViewModelProvider(
        this,
        MyViewModelFactory()
    )[MyViewModel::class.java]
}

Parent lifecycle

kotlin
private val parentViewModel: RouteViewModel? by lazy {
    parentFragment?.let {
        ViewModelProvider(
            it,
            MyViewModelFactory()
        )[RouteViewModel::class.java]
    }
}

private val activityViewModel: ActivityViewModel? by lazy {
    activity?.let {
        ViewModelProvider(
            it,
            MyViewModelFactory()
        )[ActivityViewModel::class.java]
    }
}

ViewModel implementation with LiveData

kotlin
class MyViewModel(
    private val repository: Repository,
): ViewModel() {
    private val _data: MutableLiveData<String> = MutableLiveData()
    val data: LiveData<String> = _data
    
    private val _errorMessage: MutableLiveData<Throwable> = MutableLiveData()
    val errorMessage: LiveData<Throwable> = _errorMessage
    
    private val _isRefresh: MutableLiveData<Boolean> = MutableLiveData()
    val isRefresh: LiveData<Boolean> = _isRefresh
    
    fun fetchData() {
        _isRefresh.value = true
        viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable ->
            _errorMessage.value = throwable
            _isRefresh.value = false
        }) {
            _data.value = repository.fetchData()
            _isRefresh.value = false
        }
    }
}
kotlin
class MyFragment: Fragment() {
    private val viewModel: MyViewModel by lazy {
        ViewModelProvider(this)[MyViewModel::class.java]
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.isRefresh.observe(this) {
            showHideSpinner(it)
        }
        
        viewModel.errorMessage.observe(this) {
            Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
        }
        
        viewModel.data.observe(this) {
            updateUI(it)
        }
        
        viewModel.fetchData()
    }
    
    private fun updateUI(data: String) {
        println(data)
    }
}

Key Points

  • Unlike MVP, MVVM doesn’t require strict two-way binding. Google actually recommends one-way data flow, since DataBinding’s two-way binding is quite limited.
  • With lifecycle-aware tools like LiveData, it’s easy to propagate updates from the ViewModel to the View.
  • For single-Activity, multi-Fragment architectures, using the parentFragment lifecycle to handle navigation is a great approach.
  • One downside: each LiveData often represents one state (loading, success, error), which can lead to the so-called LiveData explosion.

MVI

  • MVI can be seen as a refined evolution of MVVM.
  • To avoid LiveData explosion, MVI uses a sealed class (in Kotlin) to enumerate all possible UI states of a request.
  • The View observes a single state, and updates itself accordingly using when or switch statements.

Example

kotlin
sealed class MyUIState {
    class Success(val data: String): MyUIState()
    class Failed(val throwable: Throwable): MyUIState()
    object Refreshing: MyUIState()
}

class MyViewModel(
    private val repository: Repository,
): ViewModel() {
    private val _uiState: MutableLiveData<MyUIState> = MutableLiveData()
    val uiState: LiveData<MyUIState> = _uiState

    fun fetchData() {
        _uiState.value = MyUIState.Refreshing
        viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable ->
            _uiState.value = MyUIState.Failed(throwable)
        }) {
            _uiState.value = MyUIState.Success(repository.fetchData())
        }
    }
}

class MyFragment: Fragment() {
    private val viewModel: MyViewModel by lazy {
        ViewModelProvider(this)[MyViewModel::class.java]
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.uiState.observe(this) {
            when (it) {
                is MyUIState.Success -> {
                    showHideSpinner(false)
                    updateUI(it.data)
                }
                is MyUIState.Failed -> {
                    showHideSpinner(false)
                    Toast.makeText(context, it.throwable.message, Toast.LENGTH_SHORT).show()
                }
                is MyUIState.Refreshing -> {
                    showHideSpinner(true)
                }
            }
        }
    }

    private fun updateUI(data: String) {
        println(data)
    }
}

In this structure:

  • The Repository produces UIState,
  • The ViewModel handles threading and dispatch,
  • The View simply renders the corresponding state.

📈 Comparison Table

PatternCore ComponentsResponsibility DivisionData Flow Direction
MVP (Model–View–Presenter)Model, View, PresenterView handles UI; Presenter handles logic; Model provides dataTwo-way (Presenter ↔ View)
MVVM (Model–View–ViewModel)Model, View, ViewModelViewModel exposes observable data (LiveData, StateFlow, etc.); View observesOne-way binding (data changes drive UI)
MVI (Model–View–Intent)Model, View, Intent (or State)Everything is an event; state is immutable; View renders based on stateStrict unidirectional data flow

🧐 Analysis

FeatureMVPMVVMMVI
Data FlowTwo-wayOne-way (View observes ViewModel)Strictly unidirectional
State ManagementScattered in PresenterManaged by ViewModelCentralized immutable state
TestabilityGoodBetter (less UI coupling)Best (pure functional flow)
ComplexityMediumMedium-highHigh (but highly maintainable)
Typical Use CasesEarly Android appsAndroid JetpackJetpack Compose, React, Flutter

Additional Notes

Generally, a robust MVVM architecture can be organized into five layers:

  1. View – The imperative UI components (Activity, Fragment, XML layouts) that render data via UIState.

  2. ViewModel – Handles threading and exposes UIState via LiveData or Flow.

  3. Repository – Constructs the UIState from data models.

    • This layer can be the most interesting one — it requires some discrete math thinking (e.g., proper set mappings) to correctly assemble data.
    • Most of the other layers are boilerplate and can be abstracted using AOP (Aspect-Oriented Programming).
  4. UseCase – Defines different data sources (local DB, remote API, etc.).

  5. Service – The raw data providers.

If there is only one data source, you can skip the UseCase layer for simplicity — this should be decided case by case.

Just something casual. Hope you like it. Built with VitePress