cashapp / molecule

Build a StateFlow stream using Jetpack Compose
https://cashapp.github.io/molecule/docs/1.x/
Apache License 2.0
1.79k stars 77 forks source link

State production suspension and caching with Molecule #271

Closed tunjid closed 12 months ago

tunjid commented 1 year ago

There are two questions in this:

  1. suspending a launched Molecule: CoroutineScope.launchMolecule implies in name and verifies in source that the launched Molecule will keep producing state while the launching Coroutine is active. In the case where there is no collector of the produced state, can there be a way to stop composing the Presenter?
  2. If the above can be accommodated, can there be an API to seed the last produced state to presenters? This is so that resuming collecting from the presenter does not overwrite the last produced state value.

Consider the ProfilePresenter example:

@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)
  }
}

The initial state is produced will always be Loading. In the case where Data has been produced and the ProfilePresenter is no longer being composed because the StateFlow is not being collected from, (maybe the owning screen has been placed in the back stack), I would like to prevent the last seen Data from immediately being overwritten by Loading as I return to the screen and the presenter produces state yet again.

i.e:

  1. Screen in focus. State: Loading.
  2. Data emitted. State: Data.
  3. Screen in back stack, presenter is no longer composing. State: Data.
  4. Screen back in focus, presenter is recomposed. Last seen Data state is overwritten. State: Loading.

The above can be worked around by using moleculeFlow with the Flow builder to allow seeding as the Flow is cold, and then creating a StateFlow with the right SharingStarted argument:

class SeededStateHolder {
    var seed: ProfileModel = Loading
    val profileStateFlow = flow {
        // Pass the seed to the presenter to prevent overwriting state
        emitAll(moleculeFlow(ProfilePresenter(seed, ....))
    }
    // update the seed on each emission
    .onEach { seed = it }
    .stateIn(...)
}

Though I wonder if I'm missing something more obvious. If not, a built in API that allows for it would be really nice.

jingibus commented 1 year ago

In the case where there is no collector of the produced state, can there be a way to stop composing the Presenter?

The StateFlow created by launchMolecule is fed by a coroutine residing in the calling CoroutineScope. This coroutine runs the composition. If you kill that coroutine (e.g. by cancelling the Job in that CoroutineScope), it will stop composing. But there is no way to restart composition in that case.

If the above can be accommodated, can there be an API to seed the last produced state to presenters? This is so that resuming collecting from the presenter does not overwrite the last produced state value.

If you want to stop composition and restart it, that means managing the StateFlow by hand instead of using Molecule's API. The approach you wrote is how you'd do that: use moleculeFlow to create a cold flow that does its own composition, and then use stateIn to share it into a StateFlow. stateIn provides various options for starting/stopping the driving coroutine depending on how many collectors it has.

tunjid commented 12 months ago

Gotcha, if I understand correctly:

I'll close the issue, thank you.