MVP/MVC to Reactive Architectures for Jetpack Compose

For any Android application that’s more than five years old, this will probably be a common issue as developers try to integrate Jetpack Compose. Google, prior to the release of the Android Architecture Components in 2017, didn’t have an opinionated/recommended architecture. As a result, people writing applications copy/pasted examples from the Android documentation website and used this as “architecture”; the examples at the time were not as good as the ones today, so people ended up putting a lot of business logic in their Fragments and Activities. This was a pseudo MVC (Model -View-Controller) architecture with the Fragment/Activity being the “Controller”, the XML layout being the “View”, and any data classes were the “Model” (DB/network interactions were sometimes in the model and sometimes in the view depending on how developers interpreted the Android documentation). Applications with MVC tended to look like this:

Android MVC structure from https://www.codementor.io/@dragneelfps/implementing-mvc-pattern-in-android-with-kotlin-i9hi2r06c

Folks who did realize that putting business logic in Fragments/Activities made it hard to test (you usually needed Robolectric), ended up using an MVP (Model-View-Presenter) model (and in some cases, they broke large Presenters down to use cases and interactors or adopted something like the iOS VIPER model). The “View” was an interface that was defined against a Fragment/Activity. The “Model” stayed almost the same but also was where the DB/network access happened consistently. The business logic ended up in the Presenter and the View would call the Presenter and the Presenter would call the View to update the model changes into the view; the Presenter would also handle Android lifecycles with onStart/onDestroy() methods called by the view. MVP was a very calllback-oriented architecture. This is a good summary of what MVP looked like in larger apps:

Typical MVP structure From https://github.com/MindorksOpenSource/android-mvp-interactor-architecture
Typical MVP structure From https://github.com/MindorksOpenSource/android-mvp-interactor-architecture

Unfortunately, these older MVC/MVP architectures are not a natural fit for Reactive UI like Jetpack Compose. The Presenter just ends up being an almost empty data relay middleman which doesn’t have any real logic.

Architectures that Work Best with Reactive UI

Reactive UIs (ReactJS, SwiftUI, Flutter, and Jetpack Compose) work best with, no surprise, reactive data flows. Reactive UIs, by definition, react to changes in their data models, so there is no need for a Presenter (which, by definition, controls the View). Any architecture that has data that can be observed, therefore, will have the least friction when using Reactive UI.

MVVM

The officially supported Android Architecture is MVVM (Model-View-ViewModel); support comes in the form of documentation and library support. Google released this in 2017 as part of the Android Architecture Components (AAC) initiative.

Business logic in a ViewModel updates Fields created as LiveData (for apps not using Kotlin Coroutines) or StateFlow (for apps using Kotlin Coroutines). Apps using RxJava typically keep all network and database calls and other threading in RxJava until they reach the ViewModel boundary where the RxJava streams are exposed as LiveData. For older Android XML layout based applications, Databinding is typically used to bind XML layout field attributes to the LiveData fields on the ViewModel. This typically looks like this:

Folks who adopted the MVVM architecture in 2017 can use this same architecture with Jetpack Compose. However, to optimize MVVM usage for Compose, the fields should be grouped into a state class.

Ironically, when AAC MVVM was announced, the Android community was already starting to adopt the MVI architecture a year earlier.

MVI/Redux

ReactJS was open sourced by Facebook in 2013 and has been the source of inspiration for most of today’s Reactive UI frameworks, and the techniques used in ReactJS also apply to Jetpack Compose. The Redux framework (open sourced in 2015) is a very popular state management framework from the ReactJS world. Redux isn’t usually used in the app world because it requires a global app data store but native apps have multiple screens that get created/destroyed, so you should only consider this for simpler apps.

MVI dates back to a library, inspired by Redux, called “Mosby”, that Hannes Dorfman created in 2016. MVI emits a continuous stream of screen state which Compose can consume directly; this means that the ViewModel is no longer needed in a pure Compose app (or you can consider the MVI state engine as the “viewmodel”). However, an Android Arch Components ViewModel is still useful for retaining state during a screen rotation and handling other Android lifecycle issues like saving state for app resurrection and providing a lifecycle for Kotlin coroutines.

State in this MVI architecture diagram is consumed directly by Reactive UIs like Compose. Because of the rigid definitions of events and state, state can also be played back (aka “time machine” or “time travel” support) for both MVI and Redux.

BLoC

BLoC is Google’s official framework for Flutter which stands for “block of logic”. It’s generally tied to a component/widget, but can also be wired together to create more complex business logic. It was created in 2019, but oddly was never ported over to Android by Google. BloCs are used like viewmodels in Dart.

Elm/MVU

Elm is a web frontend architecture that predates Redux and inspired the Redux (at least according to their documentation :-). It’s also very similar to MVI as you can see:

MVU architecture from https://steemit.com/utopian-io/@tensor/using-the-elm-architecture-or-the-mvu-pattern-with-dartea-inside-of-dart-s-flutter-framework
MVU architecture from https://steemit.com/utopian-io/@tensor/using-the-elm-architecture-or-the-mvu-pattern-with-dartea-inside-of-dart-s-flutter-framework
From https://steemit.com/utopian-io/@tensor/using-the-elm-architecture-or-the-mvu-pattern-with-dartea-inside-of-dart-s-flutter-framework

The main difference is that it provides a View which includes the rendering. In a Jetpack Compose context, an Elm architecture provides a Composable which is the view. This keeps the View rendering closer to the model for tighter coupling.

Pros/Cons

Every architecture has tradeoffs, so you’ll have to decide what works best for you.

MVVM is the Google official standard, but does not rigorously enforce Unidirectional Data Flow (UDF). It will also be the most widely known by developers. The libraries are heavily tied to Android so business logic can’t be shared with Kotlin Multiplatform easily (the Moko MVVM library helps though). Least boilerplate (aka contract definitions with interfaces).

Redux inherently enforces UDF but the data store is at the application level. “Time travel” debugging is supported.

MVI inherently enforces UDF and business logic is easily tested. Data store “Time traval” debugging is supported. Data store is at the screen level.

BLoC has been tested extensively by Flutter applications, but is not part of the Android Arch Components supported by Google. It also enforces UDF. “Time machine” support probably isn’t possible because of the private data stores. Logic maps to widget level while the frameworks usually map to screen/app level.

MVU has tight coupling between the view and model which allows you to group them better and also has UDF enforcement. Tighter coupling doesn’t allow you to use the same business logic for different views, however.

Usage in Jetpack Compose

The short summary is that all these architectures can be used in Compose. If you’re still on MVC/MVP, migrate your code to use a UDF architecture for best results; during migration, you’ll find that if you want to keep MVC/MVP, the Presenter will just end up looking like MVVM with an observable state.

MVVM tends to encourage people to expose individual fields as observables because the DataBinding used in the old XML/Layout system needed widget properties exposed individually. People who have used MVVM prior to Jetpack Compose will tend to use separate fields in their viewmodels that are used for Compose as well.

In contrast, MVI and the other frameworks encourage screen state to be further subdivided into what is needed for each subsection of screen state. Compose widgets can take in a single view state class that is used for view state. MVI also tends to have more boilerplate in interface/class definitions to help enforce proper usage.

It’s also possible to build a layer over MVVM to create an MVVM+ framework which adds more guardrails to encourage using a single state class. We’ll cover existing MVI/MVVM+ frameworks and their tradeoffs in another blog.

Mobile (Native Android), Backend (Spring Boot, Quarkus), Devops (Jenkins, Docker, K8s) software engineer. Currently Kotlin All The Things!