Kotlin Multiplatform — sharing the UI State management

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 (KMP) universe, where one can benefit from a common source, containing MVI+FSM, so 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 KMP, how to setup a project, how to create common code, how to request platform specific implementations (expect/actual), and have read my previous article.
Platform prerequisites
Android:
Jetpack Compose and Flow (Job).
iOS:
SwiftUI and Combine (Publishers & Subscription).
Common prerequisites
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.
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.
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.
Who’s Next?!
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.


Migration
Android UI’s it’s already done, we don’t need to change it.
The next steps are:
- Sharing the FSM+MVI architecture and the ViewModel;
- Handle Flow’s and Publisher’s lifecycles;
- Consume state changes on iOS.
All the FSM and MVI code will be moved to the commonMain folder inside the 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”:
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:
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 Flows, but 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
:
Next, inside iosApp, we need to create a Subscription that will hold the Flow
and Job
instances to manage the subscribe and cancel logic for us:
All items received by the Flow
will be forward to the subscriber
. Also, when Flow
‘s onComplete
is called the subscriber
will also complete. Consequently 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:
The following extension will become also quite handy:
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.
Conclusions
In this article, we’ve learned how to migrate a platform working architecture into a KMP 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.
Edit 2024:
Who’s Next!? is now available and it has more targets 🎊
Note: The architecture of this project diverges slightly from the original design outlined in the Expressus sample. While it still employs FSM and Flows, it opts for KMP-ObservableViewModel in place of Orbit Multiplatform and the Publisher logic. This adjustment significantly streamlines the process, making it more efficient and manageable.
🎉 Featured in Android Weekly #515 & Kotlin Weekly #299