A stable, multiplatform Molecule 1.0

Molecule is a Compose-based library which we announced two years ago for managing application state. I’m excited to announce that today we are releasing version 1.0, its first stable version!

In the time since the original post, Molecule has gained two major features:

  1. Support for Kotlin multiplatform targets (JVM, JS, and native) in addition to just Android.
  2. An immediate recomposition mode saving you from needing to supply a frame clock.

How are these useful? Let’s look at an example!

A pure Compose UI app may separate state-producing composables from UI-rendering composables.

@Composable fun counter(start: Int = 0): Int {
  var count by remember { mutableStateOf(start) }

  LaunchedEffect(Unit) {
    while (true) {
      delay(1.seconds)
      count++
    }
  }

  return count
}

@Composable fun CounterText(value: Int) {
  Text("$value", fontSize = 20.sp)
}

Now CounterText() can easily be used with @Preview and/or Paparazzi snapshot testing. The state-producing counter() can be even more powerful as used alongside features of Molecule.

Increasing reuse

Migrating a large, View-based Android app to Compose UI takes a very long time. Instead of keeping state logic written in libraries like RxJava during the UI rewrite, logic can be migrated to Compose early and exposed to Views as a StateFlow.

fun CoroutineScope.launchCounter(start: Int = 0): StateFlow<Int> {
  return launchMolecule(mode = ContextClock) {
    counter(start)
  }
}

With Compose running outside the context of Compose UI and producing plain data, other destinations like notifications, widgets, and more can become the target of your output.

Separating the clock

It doesn’t always make sense to recompose counter() at the rate of the UI framework when using it more like a presenter. Molecule’s “immediate” recomposition mode triggers whenever there are new changes to produce.

fun countFlow(start: Int = 0): Flow<Int> {
  return moleculeFlow(mode = Immediate) {
    counter(start)
  }
}

This Flow will produce a new value every second, since that’s when the internal timer updates the internal count and notifies Compose of pending state change.

Simplifying testing

The logic of counter() exposed as a Flow can now be unit tested with Turbine.

@Test fun counts() = runTest {
  countFlow().test {
    assertEquals(0, awaitItem())
    assertEquals(1, awaitItem())
    assertEquals(2, awaitItem())
  }
}
@Test fun countStart() = runTest {
  countFlow(start = 3).test {
    assertEquals(3, awaitItem())
    assertEquals(4, awaitItem())
  }
}

And these unit tests will run on the JVM, because…

Multiplatform usage

Molecule runs on every Kotlin multiplatform target supported by the JetBrains Compose runtime. So in addition to enabling unit tests on the JVM, your counter() can now be run on platforms like iOS targeting SwiftUI or the web targeting the DOM.

suspend fun main() {
  val count = document.getElementById("count")
  countFlow().collect { value ->
    count.innerText = "$value"
  }
}

Whether it’s a 100% Compose UI Android app, a Kotlin multiplatform project with many targets, or something in-between, Molecule is here to help you manage state using Compose.

This post is part of Cash App’s Summer of Kotlin Multiplatform series.