Open-source project
cashapp/molecule avatar
cashapp/molecule

cashapp/molecule: running Jetpack Compose without a screen to produce a StateFlow

Build a StateFlow stream using Jetpack Compose

2,231 stars117 forksKotlinApache-2.0

At a glance

What is it?
Molecule is an Apache-2.0 Kotlin library from Cash App that runs a composable function headlessly and exposes its return value as a StateFlow or Flow, which lets presenters be written in plain Compose state code instead of reactive stream ceremony. The clock parameter is the part a new user has to understand before anything produces a value.
Who is it for?
Molecule is the right tool if you are a Kotlin shop that already uses Compose, if your presenters currently drown in combine and onStart calls, and if you can accept a dependency on the Kotlin Compose compiler plugin in the modules that define your presenters.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 28, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole library in five lines of counter code

Molecule's entire premise fits in one function. You annotate a composable function, hand it to a launcher, and receive a StateFlow of whatever the function returned:

kotlin
fun CoroutineScope.launchCounter(): StateFlow<Int> = launchMolecule(mode = ContextClock) {
  var count by remember { mutableStateOf(0) }

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

  count
}

There is a deliberate joke in the README about whether Jetpack Compose is a UI toolkit for Android, and the answer given is that Compose is really a general purpose compiler and runtime for state tracking and tree node and property manipulation, usable on any platform Kotlin supports, with any tree, for any state. Molecule is described as the glue that connects that runtime to kotlinx.coroutines flows so the tree can be omitted entirely. The project's own image makes the same point as a meme about Molecule not being a framework.

The counter example is worth reading twice because it demonstrates the payoff. The increment happens inside a LaunchedEffect with a one second delay, and the returned value is a plain mutable state read, not a flow operator. There is no stream to build, no combine to wire, and no emission to thread through. The result is that the logic reads like the imperative code a Kotlin developer would write by hand, while the type signature remains a StateFlow that any view layer can collect.

That is the entire trick. Everything after this section is about the conditions under which it works.

What problem it solves: stream ceremony that scales badly

The motivation section is unusually specific, and it explains why a team would adopt a library this small. Cash App's presenter objects traditionally expose a single stream of display models through Kotlin coroutine Flow or RxJava Observable, and the complaint is not that this pattern is wrong but that combining reactive streams scales non-linearly: the more sources you join and the more complex the logic, the less readable the result becomes.

The example given is a profile screen combining a user stream and a balance stream into a sealed interface with a Loading state and a Data state. Written with combine and onStart, it is working code that nobody enjoys maintaining. The claim is that the ceremony grows faster than the logic does.

The second problem is subtler and, in the project's view, an architectural violation. Compose UI requires an initial value whenever you collect a Flow or an Observable, so the display layer has to supply a default. But the display layer does not know the model, and the presenter layer owns it, so the view is being made responsible for a decision it is not in a position to make. A null default and a zero default both leak a guess about the domain into the layer that should know nothing about it.

Molecule's answer to both is the same move. Return a StateFlow instead of a Flow, and the initial value is part of the value rather than an argument the caller has to invent. The view layer then collects with no default at all, which is a small change in the signature and a large change in where the knowledge lives.

A presenter as a composable function

The replacement for the combine block is a composable function that returns the model rather than emitting it. Same inputs, same sealed interface, no stream operators:

kotlin
@Composable
fun ProfilePresenter(
  userFlow: Flow<User>,
  balanceFlow: Flow<Long>,
): ProfileModel {
  val user by userFlow.collectAsState(null)
  val balance by balanceFlow.collectAsState(0L)

  return if (user == null) {
    Loading
  } else {
    Data(user.name, balance)
  }
}

Read carefully, this is the same logic as the version before it, and that is the point worth dwelling on. The two placeholder values are still there, but they now live in the presenter, which is the layer that knows what a null user and a zero balance mean. The view layer gets to be honest about not knowing.

The wiring is then a single line, launching the composable into a scope and collecting its output:

kotlin
val userFlow = db.users()
val balanceFlow = db.balances()
val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) {
  ProfilePresenter(userFlow, balanceFlow)
}

And the view becomes trivial, collecting a StateFlow with no initial value argument at all:

kotlin
@Composable
fun Profile(models: StateFlow<ProfileModel>) {
  val model by models.collectAsState()
  when (model) {
    is Loading -> Text("Loading…")
    is Data -> Text("${model.name} - ${model.balance}")
  }
}

A coroutine runs the presenter function and shares what it returns through the StateFlow. That is the entire contract, and the reason this library can be so small is that Compose's runtime is doing the state tracking rather than a hand written stream.

Flow, not just StateFlow, for output with no single current value

StateFlow requires a value at all times, which is right for a screen that always has something to show. Some presenters are not shaped that way, so the library also produces ordinary Flows through a differently named entry point:

kotlin
val userFlow = db.users()
val balanceFlow = db.balances()
val models: Flow<ProfileModel> = moleculeFlow(mode = Immediate) {
  ProfilePresenter(userFlow, balanceFlow)
}

The counter example works the same way with a different mode:

kotlin
fun counter(): Flow<Int> = moleculeFlow(mode = Immediate) {
  var count by remember { mutableStateOf(0) }

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

  count
}

The naming difference is worth internalising because it is the only clue about which one you want. launchMolecule produces a StateFlow and needs a CoroutineScope, since a StateFlow has a lifetime and an owner. moleculeFlow produces a Flow and does not, since a cold Flow has no state to keep alive. If you find yourself passing a scope to a presenter that should be a pure function, or hunting for a scope you do not have for a one-shot collector, that is usually the other function.

Note also that the mode argument differs between the two examples, and it is not incidental. The clock choice is part of the API surface, which is the subject of the next section and the single most common source of confusion for someone evaluating this library.

Every API forces a clock decision, and that is the real cost

Jetpack Compose recomposes on frames. It waits for the next frame before beginning its work, and it depends on a MonotonicFrameClock in its CoroutineContext to know when a new frame is sent. Molecule is Compose running underneath, so it inherits that dependency: values are not produced until a frame is sent and recomposition occurs.

On Android that is invisible, because a frame is always coming. Everywhere else it is the central design question. A unit test has no frame clock. A background sync job has no frame clock. A server handling a request has no frame clock. The library does not paper over this, and the documentation is direct about the consequence: unlike Jetpack Compose, Molecule will sometimes be run in circumstances that do not provide a MonotonicFrameClock, so all Molecule APIs require you to state your preferred clock behaviour.

The mode parameter is where that decision lives. The counter example uses ContextClock, which behaves like Compose does on a screen by fishing the MonotonicFrameClock out of the calling coroutine context. The Flow examples use Immediate, which does not wait for a frame. Choosing between them is choosing what drives your updates: the display pipeline, or something else.

This is the trade a team is making, and it deserves more than a passing mention. If your presenter is feeding a screen, frame clock is right and the coupling to the display is intentional, because it means your data layer updates exactly when the screen can show them. If your presenter is feeding a background worker, you need the non waiting mode, and you should verify that the ordering guarantees you relied on in your old stream version still hold, because a recomposition driven loop is not the same as a flow operator chain and the documentation does not promise equivalence. Treat the mode as a decision to write down rather than a parameter to copy from an example.

The Compose compiler plugin is a hard requirement, and snapshots live elsewhere

Molecule is described as a library for Compose, and it relies on JetBrains' Kotlin Compose compiler plugin being present. Any module that calls launchMolecule or defines composable functions for Molecule's benefit must apply that plugin. This is not a soft requirement and it is not confined to the module that depends on the library: presenter modules and the app module both need it, which is exactly the set of modules a Gradle convention plugin or a shared build convention should cover for you.

Adding the dependency itself is unremarkable once the plugin is in place:

groovy
dependencies {
  implementation("app.cash.molecule:molecule-runtime:2.2.0")
}

Development snapshots are published separately, to the Central Portal Snapshots repository, and they are versioned as a snapshot of the next minor rather than as a nightly, which is a friendlier scheme than most projects use:

groovy
repositories {
  mavenCentral()
  maven {
    url "https://central.sonatype.com/repository/maven-snapshots/"
  }
}

dependencies {
  implementation("app.cash.molecule:molecule-runtime:2.3.0-SNAPSHOT")
}

The repository layout tells you where the code actually lives. There is a `molecule-runtime` module, and beside it a `sample` module and a `sample-viewmodel` module, which is the pairing that makes sense for this library since the whole point is the boundary between a presenter and a view. There is a Kotlin JS store directory, so the library is compiled for the browser as well as for the JVM and Android, and that is consistent with the claim that Compose is a general purpose runtime rather than an Android UI toolkit. A Kotlin Multiplatform audience is the group this design serves best, and the documentation site is versioned, with a 2.x path and a latest path, which is a small thing that tells you they expect people to be on different versions for a while.

Editorial conclusion

Molecule is the right tool if you are a Kotlin shop that already uses Compose, if your presenters currently drown in combine and onStart calls, and if you can accept a dependency on the Kotlin Compose compiler plugin in the modules that define your presenters. It is the wrong tool if you are on plain JVM Kotlin without Compose, since the compiler plugin requirement is not optional and every module defining a composable must apply it, and it is also wrong if you need output that ticks on a wall clock rather than on a frame, because every API demands a clock decision and a background service has no frames to wait for. Before adopting it, write one presenter in the imperative style and check whether the recomposition clock gives you the update cadence you actually need, since that single choice determines whether your data layer or your screen is the thing being scheduled.

Frequently asked questions

What does cashapp/molecule actually do?

It runs a Jetpack Compose function without a UI and exposes the value it returns as a StateFlow or a Flow. That lets a presenter be written with ordinary Compose state code instead of reactive stream operators.

When should I use launchMolecule versus moleculeFlow?

launchMolecule produces a StateFlow and requires a CoroutineScope, which suits a presenter whose model always has a current value. moleculeFlow produces a plain Flow and needs no scope, which suits cold collections such as background work or tests.

What are the RecompositionMode options and why do they matter?

Every Molecule API requires a clock choice because the library is Compose underneath and normally waits for a frame. RecompositionMode.ContextClock behaves like Compose on a screen by taking the MonotonicFrameClock from the calling coroutine context, while other modes exist for contexts such as tests and background work that never send a frame.

Do I need the Kotlin Compose compiler plugin to use Molecule?

Yes. Any module that calls launchMolecule or defines composable functions for Molecule must have the JetBrains Kotlin Compose plugin applied, which normally means a shared Gradle convention so the app and presenter modules are covered together.

How do I add Molecule to a Gradle project and where do snapshots live?

Add app.cash.molecule:molecule-runtime as a normal implementation dependency. Development builds are published to the Central Portal Snapshots repository, so a consuming project must declare that repository separately to use a snapshot version.

Official sources

  1. cashapp/molecule on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cashapp-molecule.svg)](https://hysenlabs.com/projects/cashapp-molecule)