KMM — Think smarter when sharing state

Photo by Fotis Fotopoulos on Unsplash

In this short blog, we will explore how we can drive the UI from a shared platform specific state in KMM

Kotlin Multiplatform Mobile (KMM) has grown over the past number of months. There has been an influx of interest amongst engineers exploring how they can use Kotlin to share business logic across mobile platforms.

I’ve been working in KMM for around 18 months now. One of my favourite aspects of KMM is that you can decide how much of the codebase you want to share.

In the interests of maintaining platform consistency, I’ve found it effective to share data domain and presentation logic. This removes the need for business logic on platforms and allows platforms to build dumb UI. This lends particularly well to a declarative UI with both Jetpack Compose and Swift UI.

Let’s take a look at a very simple use case. Consider a screen that needs to display a counter. The number displayed starts and stops incrementing when a user clicks on a button:

Now consider the case where the current count value is emitted from some repository:

We can construct this CounterRepository in our shared module. On both iOS and Android we would then implement a platform ViewModel/Presenter. This presenter would collect the value from the repository and update the screens state.

Flow apis aren’t supported on ios, however we can wrap them with apis that rely on callbacks like NativeFlow (checkout this blog post for more details)

This arguably introduces a risk of platform inconsistency. We have introduced more business logic on both platforms. Both platforms are required to create their own Presenter logic, manage and retain UI state. Here are some considerations that need to be made for this screen, on each platform:

  1. What text is displayed on the button
  2. What value is displayed for the current count
  3. What happens then the user clicks on the button

This is all very achievable and does still reap some of the benefits of KMM. We still have a shared data/domain layer and each platform is managing its feature state. Yet, say the action of the button is to change in the future, or some other business decision was to change. This risks the change needing made on both platforms. We would want to be able to make this kind of change once.

Dealing with a Shared Presenter

What if we wanted to create a shared presenter and perform the presentation logic in KMM?

If we expand a little further on our shared logic we can explore creating a shared Presenter. Here the state and behaviour for each screen is written in our KMM module.

First we will define a state for the screen:

Then we can create a simple state holder for the screen that will expose and internally mutate the screens state, based on actions sent from the UI:

Implementation of this on Android and iOS is relatively straightforward. We would retrieve an instance of this Presenter and observe the corresponding Flow’s to trigger a redraw of the UI when something changes.

The main problem here is that we are forced to expose two different state observables; one for each platform.

There is state which is a StateFlow<CounterState> which can be collected on Android. Then iosState which is a NativeFlow which can be collected on iOS.

This causes unwanted complexity. Not only in maintaining as the project grows, but also is confusing for other engineers. When an engineer retrieves an instance of this class, it’s not clear why there are two streams representing state.

So, how can we get around this?

Enter Expect/Actual

KMM has a lot of cool features, but one that comes in very useful here is expect / actual feature. This feature allows you to define an expected class and/or function in the common source set. Here you can specify a blueprint for that class with the expect keyword. Then write the actual implementation of this on each of the platforms source sets with the actual keyword. This is more commonly used for accessing platform specific APIs, yet even though that is not our need here, it is still a useful tool.

We’ll start by defining a StateHandler as an expect interface (where VS represents the ViewState object):

src/commonMain

Then we will create the actual implementations of this interface for both Android and iOS in the androidMain and iosMain source sets respectively:

src/androidMain
src/iosMain

Notice that both interfaces implement a collectState function each with different signatures. This allows us to define different state collection logic on each platform. This ensures we only expose one single stream of state per platform. Next up, we’ll need to define an abstract Presenter which will implement this interface. We’ll also need to use expect/actual here to retain separate state logic for each platform. (where VS represents the ViewState object):

src/commonMain
src/androidMain
src/iosMain

Notice how the collectState implementation is different on iOS and Android. Now we have an implementation of the BasePresenter class for both iOS and Android. There are some similarities between these implementations, but the key main difference is the function for collecting/observing the ViewState. Using this abstract class, we can then update our previous implementation of CounterPresenter:

src/commonMain

Implementing our Shared Presenter on iOS and Android

Now we have a presenter that reflects the true state observable for each platform. We can then take a look at how we would implement this on both Android on iOS.

On Android, with Jetpack Compose, there are several ways to do this. But a very quick and simple implementation, would look something like this:

We have a little extra lift on iOS if we want to work with SwiftUI. In which case we will need to create an ObservableObject in order to trigger the state updates:

Here we are creating a wrapper of sorts to host the presenter, collect its state updates, and hook this into the SwiftUI @Published property.

Now we have a state holder in our KMM module, which takes responsibility for view state. This reduces the complexity on platforms by delegating all our business logic to KMM; platform engineers can then spend more time writing great UI. By driving the view state from the KMM shared module, we can make changes on both platforms from one place.

Thats all for now! I hope you enjoyed the article and hope it gives you some inspiration into thinking more about state emissions on KMM! Or perhaps has encouraged you to explore sharing the presentation logic in your project!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store