ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

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.

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

Migration

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 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:

iosMain

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:

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. 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:

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.

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.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Guilherme Delgado

Software Engineer at BlissApplications 🇵🇹 • github.com/GuilhE • When I’m not coding I’m 🧗🏽or 🏄‍♂️

No responses yet

Write a response