Molecule: Build a StateFlow stream using Jetpack Compose
Let’s look at a snippet from Jetpack Compose:
@Composable
fun MessageCard(message: Message) {
Column {
Text(text = message.author)
Text(text = message.body)
}
}
The most interesting part of this code is that it is actually reactive. Its reactiveness is
abstracted away by the @Composable
annotation and delegated variables. For instance, say the parameter message
is delegated to a
MutableState
.
When message
is updated, the method MessageCard
’s current snapshot state will become dirty and
the next time the clock
ticks, the method will emit and it will update the two text views in place. The equivalent in RxJava
or Kotlin flows could be that message: Message
is a Observable<Message>
or Flow<Message>
, and
that MessageCard()
returns Observable<View>
or Flow<View>
.
On top of that, the androix.compose.runtime
provides APIs to consume Kotlin flows within
composable methods, such as StateFlow<T>.collectAsState: State<T>
.
This means we can write code as such:
@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)
}
}
No complex chained RxJava or Kotlin flow operators. The abstraction offered by Compose allows us to
write imperative and reactive code with simple logic such as if/else
, when
statements, or for
loops.
Now, the question is: How am I going to use my composable method in my non-composable environment?
And the answer is: Molecule!
Back in November 2021, Jake wrote about Molecule on this blog
when it was still under development. A few weeks ago, we released 0.4.0
which we are using in
production with great satisfaction.
Molecule does one thing: it transforms a composable method into a Flow
or a StateFlow
via the
following APIs:
/**
* Create a [Flow] which will continually recompose `body` to produce a stream of [T] values
* when collected.
*/
fun <T> moleculeFlow(
clock: RecompositionClock,
body: @Composable () -> T,
): Flow<T>
/**
* Launch a coroutine into this [CoroutineScope] which will continually recompose `body`
* to produce a [StateFlow] stream of [T] values.
*/
fun <T> CoroutineScope.launchMolecule(
clock: RecompositionClock,
body: @Composable () -> T,
): StateFlow<T>
Molecule lets you pass two clocks for two different recomposition behavior:
ContextClock
will delegate to theMonotonicFrameClock
provided by the callingCoroutineContext
.Immediate
will make the resulting flow emits eagerly every time the snapshot state is invalidated.
Writing imperative code without losing reactiveness is amazing. At Cash App, our new presenters are
written as composable methods. They are simple to write, simple to review, and simple to test!
Have a look at our README to get
started!