Architecture in Jetpack Compose — MVP, MVVM, & MVI

Ian Alexander
Mobile at Octopus Energy

--

Written as of beta01. Compose is evolving rapidly so some syntax may have changed.

App architecture has come a long way in Android world over the last decade. From god activities to MVP to MVVM to MVI with a sprinkling of event buses, clean architecture, and a variety of other patterns along for the ride.

With Jetpack Compose likely hitting stable sometime in 2021 we can assume the next few years will bring a selection of new patterns to Android as declarative UI changes the way we develop apps.

This articles an experiment looking at how the architecture patterns we’re all familiar with transfer into the compose paradigm.

Want to work with Jetpack Compose, SwiftUI, KMM; AND make a dent in climate change?

Octopus Energy is always hiring talented, passionate mobile developers, so if you like what you see here please reach out! Or apply directly at: https://jobs.lever.co/octoenergy.

The App

Nothing complex for this experiment, I just want to test extracting logic from the UI layer. So the app asks a question does some validation on the answer and displays a result screen saying whether you were right or wrong.

From the left to right, the question screen, the wrong answer screen, and the right answer screen

When submitting an answer, the app also calls a fake API service so there’s a loading state that needs displayed before progressing to the results screen.

Architecture? What architecture?

To start, let’s see what happens when we skip architecture and try and jam all our logic in the compose UI layer.

It is possible— if a little harder than in traditional android — and the results were as ugly as you’d expect, so I won’t include snippets, but if you’d like to check it out the samples here.

There is one interesting bit to this experiment. As composables can run multiple times — even hundreds of times in the case of animations — all object initialisation or logic needs wrapped in remember {} to ensure the work doesn’t run expensively on each re-composition (in some cases you might also use effects).

This is great as every time you write remember it forces you to question whether that code should be there in the first place. This isn’t to say you shouldn’t use remember — it’s a core part of what makes compose function — but often if you’re wrapping application logic in remember there’s probably another, better way to achieve the same outcome.

And as you’ll see from the sample, jamming logic in compose leads to messy and hard to maintain code. It’s safe to say this wouldn’t scale well to anything beyond a group of screens in a prototype. But nonetheless, in some cases that’s all you need and there’s no use delving into architecture patterns.

Pattern overview

MVP, MVVM, and MVI are some of the common patterns in Android and they can broadly be thought of as different flavours of the same core concept — abstracting logic from the UI into classes without references to Android. The main way in which they differ is how those classes accept their inputs and how they then output updates to the UI.

Of course there are many subtle variations of each pattern depending on the problem they’re solving, what libraries are put to work, and the preferences of the developers making the project — it’d be impossible to cover all the possibilities. For this experiment I’ll try to approximate the most common implementations of the patterns to give a rough idea of how they work in compose.

MVP

The defining feature of MVP is that data is communicated back to the view imperatively. Strangely enough this is likely impossible to implement in Compose. Let’s take a look why.

To imperatively update the UI, presenters rely on having a reference to the view, which is usually an activity or fragment injected into the presenter as an interface. The presenter can call functions on the interface to cause changes to the UI.

But a quick glance at a composable shows there’s no return value — so no view references to pass to our presenter.

From the Compose docs:

The function doesn’t return anything. Compose functions that emit UI do not need to return anything, because they describe the desired screen state instead of constructing UI widgets.

Compose is still part of Android, so the hierarchy will always start from an activity or fragment, so perhaps it’s theoretically possible to use this root Android object as the view reference and call this from presenters before recomposing the entire compose tree. Although that would lead to some monsterous Frankenstein view objects. Not to mention hellishly poor performance & UX from recreating the entire screen UI on even minor changes — defeating one of the big benefits of Compose.

There is also potentially the option of using CompositionLocals in some way to make MVP work, although it seems a bit of an abuse, and if you’re imperatively setting data to trigger re-compositions you may as well cut out the middle man and use MVVM.

So it’s probably safe to say that if you’re excited about integrating Compose into your MVP-based-app the place to start is probably in migrating away from MVP. Which is also suggested within the Compose docs:

Unidirectional Data Flow (UDF) architecture patterns work seamlessly with Compose. If the app uses other types of architecture patterns instead, like Model View Presenter (MVP), we recommend you migrate that part of the UI to UDF before or whilst adopting Compose.

MVVM

Ever since the folks making Android triggered a MVVM stampede by the fortuitous naming of the Jetpack library ViewModel, it’s fair to say that it’s become pretty popular. With MVP being not-so-Compose-friendly, that looks like quite the happy accident.

Where MVVM differs from MVP is in how data is communicated back to the view. Rather than imperatively calling the view, in MVVM changes are described by data which are then observable by the view. The UI can then reactively re-compose itself based on the new data.

But how does the code to make this happen look? (full sample here) We’ll do this without using Jetpack ViewModel as whether you use Jetpack ViewModel or not, the majority of the code will stay the same. The only thing which changes with Jetpack ViewModel is VM creation & VM lifecycle.

The Compose tree will start in the Android hierarchy so the Activity is as good a place as any to begin. Interestingly in a 100% Compose app you would only have a single activity and all your screens will be defined by composable destinations — adios Fragment 👋.

The Activity is also where you can handle object graph injection — at least in small projects. As projects expand you’ll most likely want to scope VMs to certain screens or groups of screens. In that case you’ll want to create VMs lazily in the nav graph composables — although I won’t go into that in the samples here.

Navigation will happen near the root of your tree, here the navigation library will deal with the heavy lifting of switching in & out destinations on the tree and looking after the backstack. It’s also where view models are passed to your Compose destinations.

In this simple sample I pre-created the ViewModel manually in the Activity In bigger projects with VM scoping you’ll probably want to create your view models here. You’ll also likely use a DI framework in bigger projects. There’s no direct way to inject into Composables so you’ll need someway to create the object graph lazily on a scoped basis.

In a bigger project I use Dagger and do this by injecting providers into the Activity (e.g. Provider<MyViewModel>) and creating objects lazily in the nav graph where they can be scoped to single destinations or nested destinations.

Next up the destinations (or screens) of our small app.

They’re quite simple screens, so there’s nothing complex in the way of UI. The interesting things to note are:

  • Each composable has a CoroutineScope bound to it’s lifecycle. LaunchedEffect will run on that scope. So cancellation is handled automatically for us when the composable leaves the tree — no boilerplate!
  • Anything inside LaunchedEffect will only be run once. So no need to worry that the code will re-run between re-compositions of the same composable.
  • collectAsState().value is a small wrapper provided for us which hooks kotlin flow into the compose world. Every time a new value is emitted the appropriate parts of this view will (smartly) re-compose with the new data received.

And finally let’s take a look at the view model.

All our logic is abstracted and testable, data view state is communicated back to the view with MutableStateFlow, single shot events like navigation/snackbars/etc. are communicated to the view layer by Channel, and any long running work can be offloaded to background coroutines — success 🎊!

Overall MVVM fits Compose very well and if your app is already built with a MVVM structure, integrating Compose is as simple as switching your traditional Android UI for Compose. Your view models largely stay the same.

Jetpack ViewModel

When using Jetpack’s ViewModel, very little will change compared to the above example as communication functions in exactly the same way. So I won’t show code snippets here in the interest of avoiding repetition. Although check out the full sample here if you’re interested.

The main change is in how the ViewModel is created. I used Hilt, although you can alter the code to fit your favoured DI framework (or no DI at all). So with a few Hilt annotations and private val jetpackMvvmViewModel: JetpackMvvmViewModel by viewModels() you’re good to go and get the added benefits of Jetpack ViewModel’s extended lifecycle and simple integration with other Jetpack libraries.

One thing to note is that in the sample I create the ViewModel in the Activity and pass it into Compose world. In anything beyond a small project this wouldn’t really be feasible.

It seems from here and here that the Jetpack ViewModel will have easy creation and scoping integration with compose & the navigation library—however, I couldn’t quite make the sample code play ball. If you can do one better than me and make this work, the simple Compose integration is definitely a big plus point to using Jetpack ViewModels. Integrations between jetpack libraries still feel very much early alpha, so this is sure to be improved in the near future.

MVI

This is a very similar pattern to MVVM, with some important differences. In many ways it’s a merging of MVVM & Redux.

MVI pre-defines a selection of events the ViewModel handles and the view publishes a stream of these events as they happen. But the major difference is communication back to the view. Whereas in MVVM there is usually a separate publisher for each piece of data, in MVI a single object defining the entire state of the view is published.

Often there’s a section of MVI which follows Redux style reducing, but as that would all occur well outside of Compose, we’ll ignore that here and just focus on the communication portion of MVI.

In the activity and nav graph nothing changes, but there are changes in how view state and events are listened to.

As there’s now a single stream of view states and a single stream of one shot actions, we only need to start listening to these streams in one place — at the beginning of the composable they’re used in. As VMs expand this means less boiler plate to hook up for View-ViewModel communication.

There’s also a few changes in the ViewModel.

Within the ViewModel layer there’s three data structures — ViewState ,OneShotEvents , and UiActions— these define your View-ViewModel interface. This makes it much easier to understand what the ViewModel is doing instead of digging through logic. And there’s also the benefits that you only need a single MutableStateFlow and Channel definition, so ViewModel ballooning is rarer.

The other thing to mention is that here I use a simple ViewState definition, but there’s no reason this can’t be more complex, for instance it’s often useful to use sealed classes to model ViewState which can have loading/error/result states. Combined with the single stream of ViewState this ensures only one state can be on screen at any time, so reducing programming errors.

Similar to MVVM, if your app is already built with MVI, integrating Compose is as simple as switching how your view is built. The rest will largely stay the same.

Clean architecture?

Remember that Compose is a UI framework only. Large portions of our apps won’t change at all. If you’re working with use cases, services, entities, interface orientated business logic, etc. then these parts of your app will all stay the same.

These layers only deal with fetching, storing, and manipulating data before sending it to the view. Whether the view is traditional Android, Compose, or a toaster these portions of your app don’t care — as a bit of tangent, this is why clean architecture could suit Kotlin Multiplatform so well, it’s a small stretch from UI independent data logic to data logic shared across multiple platforms. Apps already built with clean architecture in mind are a short hop from being able to take advantage of all the benefits of KMP.

Conclusion

From digging into Compose, it’s brilliant. Kind of like taking a breath of fresh air after being stuck in an airlocked basement with someone that’s skipped one too many showers. The Android UI framework always had so many gotchas lurking around every corner, whereas Compose learns from those problems and fixes many of them, making development simpler.

If you’re stuck on a MVP code base or worse, a code base with no logic abstracted from the UI, updating to Compose is gonna be tough. If you’re looking to take advantage of Compose then the beta period would be a great chance to upgrade your architecture so you can jump onto Compose when it hits stable.

On the other hand if you’re lucky enough to have a MVVM code base or better yet MVI, then you’re ready to have a relatively pain free migration to Compose.

Beyond MVI?

Declarative UI may be new in Android, but the concept was popularised in web with React. So with five years head start, web patterns would be a great place to research new ideas which could fit well with Compose.

This was an early experiment with Compose so there’s sure to be better ways to write the samples. Please drop a comment with any suggestions!

Click here to find all the samples in full

--

--