Blog Infos
Author
Published
Topics
Published
How to take advantage of Android ViewModels in KMM projects

Photo by Brian Wangenheim on Unsplash

 

When I joined ūüĆĪ¬†Klima¬†more than 2 years ago, I had my first real contact with Kotlin Multiplatform. Even though we weren’t sharing a lot of code across different platforms back then, the Android app started by¬†Leandro¬†was highly inspired by¬†Jake Wharton’s SdkSearch, with KMP as a first-class citizen. So not only did we have multiplatform modules (e.g. for our¬†SQLDelight¬†database), but our presenters were also (mostly) multiplatform.

Fastforward 2 years and we’ve released our new app: ūüĆ欆Planet Wild. Now our presenters are truly multiplatform and are actually used on iOS! Sharing presenters across platforms is not easy and that’s what this article is about.

Presenters vs ViewModels

We have to start with an important disclaimer: when we say presenter, it doesn’t mean we’re talking about MVP (Model-View-Presenter)! We’re simply talking about the object responsible for dealing with the view (i.e. exposing view state, handling user input), whatever the view is, however the plumbing is implemented. The goal isn’t to pick any particular architecture, and what we cover here can be used on MVVM, MVI, and any other letter variations you can think of.

The differentiation is important because if we want to have real multiplatform presenters (living in the¬†commonMain¬†source set), they can’t be a¬†Jetpack¬†ViewModel¬†as that’s an Android specific dependency. So if we name them¬†ViewModel, but they’re not a Jetpack¬†ViewModel‚Ķ it gets confusing really fast.¬†ViewModel¬†has now become a loaded expression. From now on, any time we mention it here, it’s supposed to mean Jetpack¬†ViewModel¬†exclusively.

Having to avoid Android references in presenters so we can share them with iOS doesn’t mean we must ditch the¬†ViewModel, though! There are a few good reasons why we might want to keep it:

Everything together means we have a component that can reliably power our Android views, out-of-the-box. None of this is essential, though, and we can build it all ourselves. We can actually see how¬†SdkSearch took advantage of the¬†lastNonConfigurationInstance¬†API to survive configuration change without having to rely on a¬†ViewModel. But wouldn’t it be better if we could have of all of this with no extra effort?

The status quo

There are already a few different existing methods that allow us to have multiplatform presenters that can be used on iOS without sacrificing our ViewModel.

DIY expect/actual

The first approach doesn’t require any libraries and is what Touchlab is doing in their¬†KaMPKit¬†repository. They define¬†their own¬†ViewModel¬†as an¬†expect¬†class:

expect abstract class ViewModel() {
val viewModelScope: CoroutineScope
protected open fun onCleared()
}
view raw ViewModel.kt hosted with ❤ by GitHub

The¬†Android implementation¬†of that is trivial. The one interesting thing about it is the name clash being resolved at the import level since they’re already using the¬†ViewModel¬†name in their multiplatform abstract class and platform-specific implementations like this one:

import androidx.lifecycle.ViewModel as AndroidXViewModel
import androidx.lifecycle.viewModelScope as androidXViewModelScope
actual abstract class ViewModel actual constructor() : AndroidXViewModel() {
actual val viewModelScope: CoroutineScope = androidXViewModelScope
actual override fun onCleared() {
super.onCleared()
}
}
view raw ViewModel.kt hosted with ❤ by GitHub

The iOS implementation is a bit more interesting:

actual abstract class ViewModel {
actual val viewModelScope = MainScope()
protected actual open fun onCleared() {}
fun clear() {
onCleared()
viewModelScope.cancel()
}
}
view raw ViewModel.kt hosted with ❤ by GitHub

Job Offers

Job Offers


    Senior Android Engineer – Courier Apps (m/f/d)

    Just Eat Takeaway.com
    Berlin
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

,

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

A quick introduction to conflict-free replicated data types (CRDTs) and how you can use these to build a real-time collaborative tool, where multiple clients can make edits to the same data without conflicts, even while…
Watch Video

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleberg & Carlo Rapisarda
Lead Android Developer & iOS Tech Lead
Bontouch

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleberg ...
Lead Android Develop ...
Bontouch

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleber ...
Lead Android Developer & ...
Bontouch

Jobs

We don’t have a built-in scope here, so instead we create one ourselves and make sure it’s cancelled when the¬†ViewModel¬†is cleared. And that’s it! With just this tiny bit of glue code it’s now possible to create a¬†ViewModel¬†in the¬†commonMain¬†source set like¬†this one.

Using that on iOS is still not trivial, so they wrap the common ViewModel in an iOS only ViewModel wrapper that allows consuming a Flow on the iOS side as a callback. So in the end iOS actually consumes a wrapper of the ViewModel that lives in the common source set. Writing a thin Swift layer to make things easier on the iOS side is a common and encouraged practice, so there are no surprises here.

This strategy is great for Android developers as nothing really changes on the Android side ‚ÄĒ we’re basically translating Android’s¬†ViewModel¬†to iOS so we can have a multiplatform version of it. On the other hand, we’re now implicitly embedding this Android knowledge underneath all presenters, and we’re imposing this Android-style mental model of what a¬†ViewModel¬†is regardless of what might be the current perception of the rest of the team. If you’re ever planning to share presenters on the web, then this becomes even more relevant.

This might not be a big deal as ultimately what we’re carrying from an Android¬†ViewModel¬†there is quite trivial. But it still adds up to the cognitive load iOS developers need to face when working on KMM projects.

KMM-ViewModel

Rick Clephas released this great library a few months ago, and just as KMP-NativeCoroutines helps us consuming coroutines on the iOS side, KMM-ViewModel helps us sharing ViewModels.

This is basically the previous approach turned into a library. It also starts with a¬†multiplatform definition of a¬†ViewModel¬†that looks pretty close to what we saw in KaMPKit. The library goes beyond, though, and it also exposes¬†its own multiplatform¬†MutableStateFlow¬†implementation¬†and¬†SwiftUI property wrappers¬†to help working with the¬†ViewModel¬†on the iOS side (similarly to KaMPKit’s¬†wrapper).

There are similar tradeoffs here with what KaMPKit is doing, but with the big advantage of having everything nicely packaged into a library. If all you want is to be able to consume an Android¬†ViewModel¬†on the iOS side, especially for brownfield apps that are built on top of¬†ViewModel, this might be a good pick ‚ÄĒ you can see it in action in the¬†Confetti app. But there are still other ways!

Architecture components libraries

Another option is to embrace libraries offering multiplatform architecture components like¬†moko-mvvm¬†and¬†Decompose. They have a significantly larger scope than simply providing a way to share presenters, but they’re still capable of doing that so I thought it would be fair to mention them here.

Slack’s Circuit¬†is another interesting option for the future (they only just started working on iOS support), and I’m sure many more will pop up. Adopting a framework is always a more delicate decision, though, especially when we’re already dealing with a framework still in beta (hopefully not for too long!). But even if you decide not to go with them, it’s always a good idea to keep an eye out and potentially learn from what they’re doing.

✨ Introducing Retained

We’ve been using Retained in production for over two years now. It’s maintained by¬†Marcello¬†since 2019 and it’s been stable for a while. It provides a great API for wrapping arbitrary objects (i.e. our multiplatform presenters) in a¬†ViewModel¬†in a way that we’re able to take advantage of everything it brings without having to keep any direct references to it (or to anything wrapping it).

It has extensions on top of Android components just like a¬†ViewModel¬†so we can easily retain our object in whatever scope we want. Here’s how it looks like retaining a presenter in an activity or in the scope of a¬†NavBackStackEntry:

// In an activity:
private val presenter by retain { entry ->
// entry exposes the viewModelScope, savedStateHandle, and a way to listen to onClear()
MyPresenter()
}
// Or in a composable, implicitly:
val presenter = retain { MyPresenter() } // this will look at LocalViewModelStoreOwner.current
// Or explicitly:
val activity = ...
val presenter = retain (owner = activity) { MyPresenter() }
// In a Compose NavHost:
composable(route = "myRoute") { navBackStackEntry ->
val presenter = retain(navBackStackEntry) { MyPresenter() }
}
view raw Retained.kt hosted with ❤ by GitHub

When retaining an instance, we have access to a RetainedEntry that exposes everything we need, including the viewModelScope, the savedStateHandle, and a way to listen to onClear() calls. You can check examples of these being used in the docs.

This means even though we’re able to access everything we need from¬†ViewModel, our presenter has no¬†ViewModel¬†or any other Android references in it, and we can keep it in¬†commonMain¬†without any¬†expect/actual¬†plumbing. Whatever we get from Android’s¬†ViewModel¬†is only relevant on the Android side, and with Retained we’re able to keep it all contained there without leaking anything to the iOS side.

ūüćé The iOS side

This is a significantly leaner solution, and it keeps the presenters in a more ‚Äúpure‚ÄĚ multiplatform state ‚ÄĒ iOS developers will be able to look at them without having to understand or abstract away any Android-specific references.

However, there’s nothing here to help with the consumption on the iOS side. This is by design as that’s not in the scope of the library, and it actually matches our needs and expectations in how we consume multiplatform presenters on iOS as this is very particular to our architecture. For us, it’s better to handle that in our own custom (and thin) Swift layer written according to our specific needs.

How to consume a multiplatform presenter on the iOS side is a topic worthy of its own article. We’ve scratched the surface when we looked at KaMPKit and KMM-ViewModel, but it’ll really depend on the architecture in place, so it’s not something we’ll explore further here. Regardless of any implementation details, our main goal was to find a lean and non-opinionated way to keep our presenters multiplatform where we could still take advantage of¬†ViewModel¬†features on the Android side, and Retained proved to be the best tool for the job.

If you’ve found this article helpful, you might be interested in reading about how we’re dealing with dependency injection in our KMM project:

ūüĎč Feel free to reach out to me on¬†Twitter¬†or¬†Mastodon, or drop a comment here if I missed anything or if you have any questions!

Also published on Klima Engineering Hashnode.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE
blog
Kotlin Multiplatform Mobile (or simply Multiplatform Mobile) is a new SDK from JetBrains that…
READ MORE
blog
One of my 2021 new year‚Äôs resolutions was to dive in into¬†Kotlin Multiplatform Mobile¬†(KMM).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu