Molecule Documentation

repository·trunk·Indexed 24 days ago

https://github.com/cashapp/molecule

A library for building StateFlow or Flow streams using the Jetpack Compose runtime and compiler without a UI layer. Molecule allows developers to replace complex reactive stream operators with imperative @Composable functions, utilizing features like remember and LaunchedEffect to manage state and emit it as a stream.

Tokens
1.1K
Snippets
5
Records
6
Agent score
31%

What's inside Molecule

  1. What is Molecule and how does it work?

    trunk

    Molecule is a library that allows you to build StateFlow or Flow streams using Jetpack Compose logic without requiring a UI node tree. It glues Compose's state management to kotlinx.coroutines flows.

    Instead of using complex reactive operators (like combine) to merge multiple data sources, you can write imperative @Composable functions that use Compose features like remember, LaunchedEffect, and collectAsState. Molecule then runs these composables and emits the resulting state as a stream.

    @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)
      }
    }
    
    // Run the presenter to get a StateFlow
    val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) {
      ProfilePresenter(userFlow, balanceFlow)
    }
  2. Configure RecompositionMode (Frame Clocks)

    trunk

    Molecule requires a clock to know when to trigger recompositions. You must specify a RecompositionMode:

    • RecompositionMode.ContextClock: Behaves like Jetpack Compose UI. It looks for a MonotonicFrameClock in the coroutineContext. If none is found, it throws an exception. This is ideal for Android apps using AndroidUiDispatcher.Main.
    • RecompositionMode.Immediate: Constructs an immediate clock that produces a frame whenever the enclosing flow is ready to emit an item. This is useful for unit testing, running off the main thread, or when no MonotonicFrameClock is available.
  3. Install Molecule

    trunk

    Molecule requires the JetBrains Kotlin Compose plugin to be applied to any module that calls launchMolecule or defines @Composable functions for use with Molecule.

    Add the molecule-runtime dependency to your project:

    dependencies {
      implementation("app.cash.molecule:molecule-runtime:2.2.0")
    }
  4. Test Molecule flows with Turbine

    trunk

    To test Molecule, use moleculeFlow(mode = Immediate) combined with the Turbine library. This allows you to treat the Molecule output as a standard Flow for assertions.

    Note for Android modules on JVM: If unit testing in an Android module on the JVM, ensure your AGP config has testOptions.unitTests.returnDefaultValues = true.

    @Test fun counter() = runTest {
      moleculeFlow(RecompositionMode.Immediate) {
        Counter()
      }.test {
        assertEquals(0, awaitItem())
        assertEquals(1, awaitItem())
        assertEquals(2, awaitItem())
        cancel()
      }
    }
  5. Create a regular Flow with moleculeFlow

    trunk

    If you need a standard Flow instead of a StateFlow, use moleculeFlow. This is useful for scenarios where you don't need the 'always has a value' guarantee of StateFlow or when using RecompositionMode.Immediate.

    val models: Flow<ProfileModel> = moleculeFlow(mode = Immediate) {
      ProfilePresenter(userFlow, balanceFlow)
    }
  6. Create a StateFlow with launchMolecule

    trunk

    Use launchMolecule to run a @Composable function within a CoroutineScope. This returns a StateFlow that emits the value returned by the composable whenever it recomposes.

    fun CoroutineScope.launchCounter(): StateFlow<Int> = launchMolecule(mode = ContextClock) {
      var count by remember { mutableStateOf(0) }
    
      LaunchedEffect(Unit) {
        while (true) {
          delay(1_000)
          count++
        }
      }
    
      count
    }