Kotlin Multiplatform Mobile — sharing the UI State management | by Guilherme Delgado | Apr, 2022

In my previous story, I’ve talked about why I believe we can strongly improve the UI State management between the View and ViewModel on Android, by using a Model-View-Intent (MVI) architecture with the help of a Finite State Machine (FSM):

In this story, I’ll guide you through the steps needed to upgrade this solution to the Kotlin Multiplatform Mobile (KMM) universe, where one can benefit from a common source, containing MVI+FSMso that both platforms – Android and iOS -, can inherit its benefits making them only responsible for platform dependent implementations: the UI/UX.

Before we start, I’ll assume the reader has basic knowledge about KMM, how to setup a project, how to create common code, how to request platform specific implementations (expect/actual), and have read my previous article.

Android:

Jetpack Compose and Flow (Job).

iOS:

SwiftUI and Combine (Publishers & Description).

After creating a new KMM project, we need to make sure we can use our FSM and MVI implementations.

FSM:

Tinder’s State Machine is not yet upgraded to be used as a multiplatform library, but luckily there’s a pull request (PR) with that implementation, which is quite simple actually. Until this PR is accepted and published, one option is to copy StateMachine.kt and add it to our project in the shared module.

note: If you wonder why can’t we take advantage of JitPack service, there’s an issue about it.

MVI:

Orbit Multiplatform library – you can guess it by its name -, is already multiplatform-ready. Orbit also provides us with a swift-gradle-plugin to generate .swift helper classes so we don’t have to worry about how things work under the hood. To listen for state changes we just consume an ObservableObject inside a View and the Combine/Flow Communications and lifecycles are automatically managed for us.

note: At the time of writing, the authors are in the process of updating it for newer Kotlin versions. Right now it doesn’t work with versions starting from 1.6.0.

This plugin does the code generation heavy-lifting for us, but I believe we gain from knowing what’s happening behind the curtains, that’s why I’ll guide you through the logic of creating those classes. In the end, we’re not strictly dependent on it.

ViewModel:

To take advantage of a shared lifecycle scope – to launch a coroutine that will be canceled when the ViewModel is cleared -, I’ll use IceRock’s moko-mvvm library ViewModel (dev.icerock.moko:mvvm-core:${latest-version}) as the parent class of our shared ViewModel (instead of the Android’s one).

I’ll be using the same project I’ve used in the previous article to illustrate this journey. As you can see below, the output is the same, and that’s because we’re taking advantage of the same business logic and architectural implementation – written, tested and validated once – leaving only the UI creation for each platform to implement. The beauty of KMM.

Android
iOS (screen recording with the simulator lags the animations)

Android UI’s it’s already done, we don’t need to change it.

The next steps are:

  1. Sharing the FSM+MVI architecture and the ViewModel;
  2. Handle Flow‘s and Publisher‘s lifecycles;
  3. Consume state changes on iOS.

All the FSM and MVI code will be moved to the commonMain folder inside the shared module:

shared module

Sharing the ViewModel

Koin will help us with this task. First, we need to create a class where we’ll define the expect “rules”:

commonMain

This class lives inside commonMain’s folder and it also contains the dependency injection initiation logic. Next, we need to create one for each platform with its actual implementation:

androidMain
iosMain

They are very similar, but on iOS’s implementation, we need to expose a getter for the ViewModel. On Android, Koin offers handy getViewModel extensions.

Architecture shared ✅

Exposing a Job for a Publisher

To consume state emissions from our shared code we use Kotlin Flowsbut we need to bridge the gap between them and the Swift Combine Publishers. The following code was based on the very enlightening article from John O’Reilly. It will help us achieve it and handle the Publisher‘s lifecycle on the iOS side.

We start by creating an extension function that returns a background Job given a Flow:

iosMain

Next, inside iosApp, we need to create a Description that will hold the Flow and Job instances to manage the subscribe and cancel logic for us:

Bridge between Flow and Publisher

All items received by the Flow will be forward to the subscriber. Also, when Flow‘s onComplete is called the subscriber will also complete. According cancel() will be invoked and it will clear the subscriber and cancel the job.

If you remember, our MVI architecture is tied to the viewModelScope which means that when the ViewModel is cleared so it will the Flow and the Publisher.

Lifecycle handled ✅

Before continue to the next step, let’s add this handy extension:

The ObservableObject

The final step of this migration is exposing the UI State as a Published variable. To do so we’ll create a wrapper class that conforms with the ObservableObject protocol. That class will contain an instance from the shared ViewModel to expose it’s state and public methods:

ObservableObject wrapper

The following extension will become also quite handy:

ObservableObject extension

And to consume states in the View:

Now that we have the @Published var state at our disposal to be consumed we can choose to do it as a StateObject or ObservedObject. This example also illustrates two use-cases where we can query the state properties directly by viewModel.state.something or through a @State var when we need that property to behave like a State.

iOS consuming state changes ✅

The final step of this migration is completed.

In this article, we’ve learned how to migrate a platform working architecture into a KMM project in order to take advantage of its code-sharing philosophy. We’ve also deep-dived inside Orbit Multiplatform’s library swift-gradle-plugin and understood what classes are being generated, their purpose and how they work together.

I won’t be sharing Who’s Next!? project for now, but rest assured, I’ve created Expressus a Kotlin Multiplatform Coffee Machine:

This project contains all the logic discussed in both of my articles and includes a little bonus 😉, take a look for yourself.

As always, I hope you find this article useful, thanks for reading.

Leave a Comment