Experimenting with Composable Presenters in Kotlin Multiplatform

Using Circuit, Molecule and SKIE

Professor in lab

Introduction

This article provides an example of using a Slack Circuit style Composable Presenter with native views (SwiftUI/Jetpack Compose) for iOS and Android.

The example uses CashApp’s Molecule CashApp’s Molecule for the Composable Presenter written in Kotlin Multiplatform, with Touchlab’s SKIE providing an elegant way of consuming these in SwiftUI.

The Composable Presenter provides a Unidirectional Data Flow (UDF) architecture ‘ala ‘a ‘Circuit’ and simplifies the UI business logic code to be simpler due to using a Composable function via Molecule.

Slack’s Circuit inspired this experiment, which I first discovered when watching the video Modern Compose Architecture with Circuit by Zac Sweers and Kieran Elliott.

CashApp’s Broadway architecture inspired the Circuit architecture; a video on this is Benoit Quenaudon: architecture at Scale (DroidconNYC2022)

What's the Problem?

I recently started working on a project to build an app for iOS and Android using Kotlin Multiplatform. The app uses the Amity Social Cloud, which provides Chat, Social and Video SDKs. These SDKs offer either a lower-level SDK or a UIKit, which provides entire screen views for social, chat and video features. They use Swift and UIKit on iOS, Kotlin, and XML views on Android.

Amity is open source and offers certain levels of customisation; at this stage, we are using entire screens, but we may need to either customise these or create new views using existing code, such as the repositories.

I initially wanted to use a Kotlin Multiplatform framework such as Decompose as it supports native and shared UI. I decided against using a KMP framework as I had concerns about future integration issues with using the Amity screens. I didn’t want to paint myself into a corner, so I looked to see if I could use a lighter-weight approach to the architecture. The Amity SDKs use a Model-View-ViewModel (MVVM) architecture, so something similar would be a good approach.

Additionally, at this stage, I’m looking to use native views primarily, but I am open to using some shared UI via Compose Multiplatform for secondary screens; the Alpha status of this on iOS does put me off a little if I’m honest.

Slack's Circuit

While watching the video Modern Compose Architecture with Circuit by Zac Sweers and Kieran Elliott from KotlinConf, the architecture resonated with me, Android and the JVM (desktop) were the only supported platforms at the time. I hoped to see if there was a way to use Circuit with a native view on iOS using SwiftUI. I was happy to see preliminary support for iOS targets added to Circuit in May 2023 and the Counter example updated with an iOS implementation in SwiftUI.

The counter-example app runs on iOS with SwiftUI, which was encouraging; however, digging around in the GitHub repo, I found a PR by Rick Clephas SwiftUi sample/boilerplate, demonstrating an approach to using SwiftUI. The discussion on this PR ends with the suggestion that Circuit should be Compose Multiplatform only.
There is also this issue Figure out iOS story with UI, which explains how the current Circuit Counter example can run on iOS and ends by asking if SKIE could be used with the SwiftUI integration.

The upshot of this is that it leads me to be wary of using Circuit directly with SwiftUI, the focus with regards iOS support is somewhat unclear. Circuit is used in Slack by the Android team but not by their iOS team as far as I can tell, so it’s unclear if iOS will be adequately supported, and using it presents the risk of future changes that will not work with iOS SwiftUI. I also felt that making Circuit work with SwiftUI was a little clunky and could be improved using SKIE to simplify things.

Update 10/01/2024 – The Circuit Counter sample is now updated to use SKIE which is excellent! Looks like this was being done while I was writing the blog post!

Exploring Composable Presenters using Molecule

The core part of Circuit I was interested in being able to use is the UDF architecture using a Composable Presenter. Using Molecule allows us to transform a composable method into a Flow or StateFlow with Kotlin Multiplatform to use the Compose runtime on non-Compose platforms. Molecule also enables the presenter to return an initial state value, which the Compose UI on Android requires.

Writing Composable functions, in turn, allows us to write simplified business logic within our presenters, as we no longer need to use the Flow operators such as map and flatmap to return the state in a Flow or StateFlow.

We can test the logic in the presenter using Turbine; I still need to explore this.

I found these CashApp articles very useful:-

The state of managing state (with Compose) – Jake Wharton introduces Molecule.

Molecule: Build a StateFlow stream using Jetpack Compose – Benoît Quenaudon shows how to use Molecule and details the different clocks

A stable, multiplatform Molecule 1.0 – Jake Wharton shares the first stable version of Molecule with Kotlin Multiplatform and Immediate clock support

Where's the example?

The example app source code accompanying this article is available in the ComposablePresenterCounter repo. A screenshot of the app showing the two counter views is below.

Key Points

I built this example starting from the Slack Circuit Counter example, directly re-using the views, presenters and state/events, please see the Circuit repo & counter example.

The project was initially built from the JetBrains KMP web wizard, using-

  • Koin for DI
  • Cashapp’s Molecule to output a StateFlow from a Composable Presenter
  • Touchlab’s SKIE to consume the Presenter’s StateFlow in SwiftUI

The app has a single ‘CounterViewModel’, which is used as a container to host two styles of composable presenter:-

  • A Slack Circuit Style presenter with an event sink in the state object
  • A presenter with separate event sink and state – this is how most other examples I have seen are structured, so it was worthwhile to present both approaches.
  • The CounterViewModel inherits a KaMPKit-inspired ‘MoleculeviewModel’, which sets the MonotonicFrameClock for each platform – the DisplayLinkClock on iOS and AndroidUiFrameClock on Android.
class CounterViewModel : MoleculeViewModel() {
    val circuitCounterPresenter = moleculeScope.launchMolecule(mode = RecompositionMode.ContextClock) {
        CircuitCounterPresenter()
    }

    // Events have a capacity large enough to handle simultaneous UI events, but
    // small enough to surface issues if they get backed up for some reason.
    private val events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 20)

    val counterPresenter = moleculeScope.launchMolecule(mode = RecompositionMode.ContextClock) {
        CounterPresenter(events)
    }

    fun take(event: UiEvent) {
        if (!events.tryEmit(event)) {
            error("Event buffer overflow.")
        }
    }
}

The example app has two counter views, one for each Presenter style.

ViewModel hosting presenters

The approach of using a MoleculeViewModel to host the presenter and set the correct scope to use to LaunchMolecule is based on the example Sample MoleculeViewModel and Building Shared UI’s across Platforms with Compose. Effectively, the ViewModel becomes a container or shell for the presenters.

The common definition of MoleculeViewModel.

/**
 * Base class for Molecule ViewModels based on KaMPKit.
 * The molecule scope is used to setup the MonotonicFrameClock for each platform
 */
expect open class MoleculeViewModel(scope: CoroutineScope? = null) {
    val viewModelScope: CoroutineScope
    val moleculeScope: CoroutineScope

    protected open fun onCleared()
    protected open fun performSetup()
}

The Android implementation.

actual open class MoleculeViewModel actual constructor(scope: CoroutineScope?) : AndroidXViewModel() {
    actual val viewModelScope: CoroutineScope = scope ?: androidXViewModelScope
    actual val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main)

    init {
        viewModelScope.launch {
            performSetup()
        }
    }

    actual override fun onCleared() {
        super.onCleared()
    }

    protected actual open fun performSetup() {
        /* Perform any setup here rather than in init block */
    }
}

The iOS implementation.

actual open class MoleculeViewModel actual constructor(scope: CoroutineScope?) {
    actual val viewModelScope = scope ?: MainScope()
    actual val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + DisplayLinkClock)

    /**
     * Override this to do any cleanup immediately before the internal [CoroutineScope][kotlinx.coroutines.CoroutineScope]
     * is cancelled in [clear]
     */
    protected actual open fun onCleared() {
        /* Default No-Op */
    }

    /**
     * Cancels the internal [CoroutineScope][kotlinx.coroutines.CoroutineScope]. After this is called, the ViewModel should
     * no longer be used.
     */
    fun clear() {
        onCleared()
        viewModelScope.cancel()
    }

    protected actual open fun performSetup() {
        /* Overridden where needed */
    }
}

FrameClock

The Circuit counter-example for iOS uses the Immediate clock see the file SwiftSupport. The Immediate clock will make the resulting flow emit eagerly every time the snapshot state is invalidated, see Molecule: Build a StateFlow stream using Jetpack Compose.

Most of the examples I found also used the Immediate clock. However, a search yielded this Kotlin Slack conversation, which pointed me in the right direction for the DisplayLinkClock in Molecule which is an implementation of the MonotonicFrameClock on iOS. I also came across this slide in Building Shared UI’s across Platforms with Compose.

Internally, the DisplayLinkClock uses the CADisplayLink to implement the MonotonicFrameClock interface.

A MonotonicFrameClock ensures the recomposition is tied to the display refresh cycles rather than the constantly recomposing Immediate clock.

The Molecule readme has a good explanation of how Molecule works with the FrameClock.

The code snippets above of the MoleculeViewModel show how the moleculeScope is set for the different FrameClocks on each platform.

Presenter styles

I have used two styles of presenter, one being the Circuit style with the event sink in the returned state object and the other which I found used in the Molecule docs and most other Molecule examples, for example, in the article The state of managing state (with Compose).

There is a recent discussion on the state designs and event sink in the Circuit GitHub repo: Prototype different state designs.

The Circuit-style presenter with the event sink in the state object is taken from the Circuit repo:-

@Composable
fun CircuitCounterPresenter(): CircuitCounterScreen.State {
    var count by remember { mutableStateOf(0) }

    return CircuitCounterScreen.State(count) { event ->
        when (event) {
            is CircuitCounterScreen.CounterEvent.Increment -> count++
            is CircuitCounterScreen.CounterEvent.Decrement -> count--
        }
    }
}

interface CircuitCounterScreen : Screen {
    data class State(
        val count: Int,
        val eventSink: (CounterEvent) -> Unit = {},
    ) : CircuitUiState

    sealed interface CounterEvent : CircuitUiEvent {
        data object Increment : CounterEvent

        data object Decrement : CounterEvent
    }
}

The more common style of presenter with a separate events Flow can be seen here, this is based on the Molecule example. The take function in the CounterViewModel is called from the View to emit events on the Flow.

@Composable
fun CounterPresenter(events: Flow<UiEvent>): CounterScreen.State {
    var count by remember { mutableStateOf(0) }

    // Handle UI events.
    LaunchedEffect(Unit) {
        events.collect { event ->
            when (event) {
                is CounterScreen.CounterEvent.Increment -> count++
                is CounterScreen.CounterEvent.Decrement -> count--
            }
        }
    }

    return CounterScreen.State(count = count)
}

interface CounterScreen : Screen {
    data class State(
        val count: Int,
    ) : UiState

    sealed interface CounterEvent : UiEvent {
        data object Increment : CounterEvent

        data object Decrement : CounterEvent
    }
}

Consuming the Presenters StateFlow on iOS using SKIE

Consuming the presenter’s StateFlow using SKIE makes for an elegant solution, and the SwiftCircuitCounterPresenter & SwiftCounterPresenter wrapper pulls the CounterViewModel from the KoinDI and consumes the presenter’s StateFlow.

The SwiftCircuitPresenter can be seen here.

class SwiftCircuitCounterPresenter: ObservableObject {
    
    var viewModel: CounterViewModel = KotlinDependencies.shared.getCounterViewModel()
    
    @Published
    private(set) var state: CircuitCounterScreenState? = nil

    @MainActor
    func activate() async {
        for await state in viewModel.circuitCounterPresenter {
           
            self.state = state
        }
    }
}

And the CircuitCounterView which is unchanged from the original Circuit repo bar the line of code for the presenter and the event naming.

struct CircuitCounterView: View {
    
    @ObservedObject
    var presenter = SwiftCircuitCounterPresenter()

    var body: some View {
        NavigationView {
            VStack(alignment: .center) {
                Text("Count \(presenter.state?.count ?? 0)")
                    .font(.system(size: 36))
                HStack(spacing: 10) {
                    Button(action: {
                        presenter.state?.eventSink(CircuitCounterScreenCounterEventDecrement.shared)
                    }) {
                        Text("-")
                            .font(.system(size: 36, weight: .black, design: .monospaced))
                    }
                        .padding()
                        .foregroundColor(.white)
                        .background(Color.blue)
                    Button(action: {
                        presenter.state?.eventSink(CircuitCounterScreenCounterEventIncrement.shared)
                    }) {
                        Text("+")
                            .font(.system(size: 36, weight: .black, design: .monospaced))
                    }
                        .padding()
                        .foregroundColor(.white)
                        .background(Color.blue)
                }
            }
            .task {
                await presenter.activate()
            }
            .navigationBarTitle("Circuit")
        }
    }
}

Next Steps

Areas to look into further include state retention and restoration across compositions via the remember style functions and structured concurrency, so calls to repositories are cancelled when we navigate back, for example. I’ve focussed on a simple experiment to validate the concept.

Summary

I hope sharing this experiment was helpful. The Composable Presenter pattern is appealing, and the open-source work that the CashApp, Slack Circuit, and Touchlab teams have done is fantastic and allows us to truly ‘stand on the shoulders of giants’!

It would be great to see if Circuit can adopt SKIE for the presenters, enabling this scenario but using the core of Circuit, the composable presenters with native views.

If you are interested in discussing how Kotlin Multiplatform could help, you can reach me, Richard Woollcott, at [email protected]

Resources

While researching this topic, I found these articles helpful.

Molecule GitHub Repo

KaMPKit – great starter repo, I used the view model definitions from here.

The state of managing state (with Compose) – Jake Wharton introduces Molecule.

Molecule: Build a StateFlow stream using Jetpack Compose – Benoît Quenaudon shows how to use Molecule and details the different clocks

A stable, multiplatform Molecule 1.0 – Jake Wharton shares the first stable version of Molecule with Kotlin Multiplatform and Immediate clock support

Building Shared UI’s across Platforms with Compose talk by Mohit Sarveiya – useful slides covering using Molecule with Presenters, including the MonotonicFrameClock for iOS

Using Jetpack Compose with Square’s Molecule Library – Mohit shares a Kotlin Multiplatform Golf Scores app using Molecule.

Building Flows with Molecule – Mohit presents four videos covering building a Molecule Presenter, Recomposition Frame clock, Launching Molecule and Testing Molecules with Turbine.

Kotlin Multiplatform presenters (or ViewModels): the lean way – helpful article, which has a section on the Retained library https://github.com/marcellogalhardo/retained

Using Jetpack Compose with Square’s Molecule Library w/ Mohit Sarveiya.

Molecule: Build Powerful StateFlows with Jetpack Compose

Demystifying Molecule: Running Your Own Compositions for Fun and Profit – Ash Davies and Bill Phillips Droidcon video on Molecule

Molecule with PreCompose – has an example Presenter using rememberPresenter

Beyond MVVM: Hierarchical State Management with Molecule and Compose – uses Molecule and Renderings rather than presenters but applicable.

From ViewModel To Compose Presenter: The New Form Of State Management – demonstrates how to migrate from a ViewModel to a presenter and how to test this.

Molecule Presenter Kotlin Slack message

Lightning Talk / Circuit – Compose Driven Architecture / June 8th 2023 / Trevor Crawford

Please get in touch if you want to learn more about how Kotlin Multiplatform might help your development.