Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building StateFlows with Jetpack Compose

Mohit S
September 02, 2022

Building StateFlows with Jetpack Compose

Mohit S

September 02, 2022
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Building State Flows with Compose • How to setup and

    use Molecule • Design Patterns • Testing with Molecule
  2. Building State Flows with Compose • How to setup and

    use Molecule • Design Patterns • Testing with Molecule • Molecule Internals
  3. @Composable fun ProfileScreen() { val user by userFlow.subscribeAsState(null) val balance

    by balanceFlow.subscribeAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } }
  4. @Composable fun ProfileScreen() { val user by userFlow.subscribeAsState(null) val balance

    by balanceFlow.subscribeAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } }
  5. @Composable fun ProfileScreen() { val user by userFlow.subscribeAsState(null) val balance

    by balanceFlow.subscribeAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } } Undesired coupling
  6. @Composable fun ProfileScreen() { val user by userFlow.subscribeAsState(null) val balance

    by balanceFlow.subscribeAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } } Not Reusable
  7. class ProfilePresenter(val db: Db) { fun transform(events: Flow<Event>) : Flow<ProfileModel>

    { } } sealed interface ProfileModel { object Loading : ProfileModel data class Data( val name: String, val balance: Long, ): ProfileModel }
  8. class ProfilePresenter(val db: Db) { fun transform(events: Flow<Event>) : Flow<ProfileModel>

    { return combine( db.users().onStart { emit(null) }, db.balances().onStart { emit(0L) }, ) { user, balance -> } } }
  9. class ProfilePresenter(val db: Db) { fun transform(events: Flow<Event>) : Flow<ProfileModel>

    { return combine( db.users().onStart { emit(null) }, db.balances().onStart { emit(0L) }, ) { user, balance -> } } } Can get complex if we add more streams
  10. @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) } }
  11. @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) } }
  12. @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) } }
  13. val scope = CoroutineScope(Main) val models: StateFlow<ProfileModel> = scope.launchMolecule( 


    clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) }
  14. val scope = CoroutineScope(Main) val models: StateFlow<ProfileModel> = scope.launchMolecule( 


    clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) }
  15. val scope = CoroutineScope(Main) val models: StateFlow<ProfileModel> = scope.launchMolecule( 


    clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) }
  16. val scope = CoroutineScope(Main) 
 scope.launchMolecule( 
 clock = RecompositionClock.ContextClock,

    
 emitter = { value -> } 
 ) { ProfilePresenter(events, randomService) }
  17. val scope = CoroutineScope(Main) 
 scope.launchMolecule( 
 clock = RecompositionClock.ContextClock,

    
 emitter = { value -> } 
 ) { ProfilePresenter(events, randomService) } Will not respect back-pressure
  18. val scope = CoroutineScope(Main) 
 val models: StateFlow<ProfileModel> = scope.launchMolecule(

    
 clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) } Frame Clock
  19. val scope = CoroutineScope(Main) 
 
 fun <T> CoroutineScope.launchMolecule( ...

    ) { with(this + clockContext) { 
 // Pass context element to Recomposer } }
  20. interface MonotonicFrameClock : CoroutineContext.Element { suspend fun withFrameNanos(onFrame): R override

    val key: CoroutineContext.Key <*> get() = Key companion object Key : CoroutineContext.Key<MonotonicFrameClock> }
  21. Context Clock • Use MonotonicFrameClock from calling Coroutine 
 


    Context • Use Molecule with built in Android Frame Clock
  22. fun <T> CoroutineScope.launchMolecule( ... ) { val clockContext = when

    (clock) { RecompositionClock.ContextClock -> EmptyCoroutineContext RecompositionClock.Immediate -> GatedFrameClock(this) } ... }
  23. fun <T> CoroutineScope.launchMolecule( ... ) { val clockContext = when

    (clock) { RecompositionClock.ContextClock -> EmptyCoroutineContext RecompositionClock.Immediate -> GatedFrameClock(this) } ... }
  24. var value = 0 
 scope.launchMolecule(RecompositionClock.ContextClock, { value = it

    }) { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(100) count ++ } } count }
  25. scope.launchMolecule( ... ) { var count by remember { mutableStateOf(0)

    } LaunchedEffect(Unit) { while (true) { delay(100) count ++ } } count }
  26. scope.launchMolecule( ... ) { var count by remember { mutableStateOf(0)

    } LaunchedEffect(Unit) { while (true) { delay(100) count ++ } } count }
  27. Context Clock • Use MonotonicFrameClock from calling Coroutine 
 


    Context • Use Molecule with built in Android Frame Clock
  28. Immediate class GatedFrameClock(scope: CoroutineScope) : MonotonicFrameClock { 
 val frameSends

    = Channel<Unit>(CONFLATED) init { scope.launch { for (send in frameSends) sendFrame() } } 
 ... }
  29. Immediate class GatedFrameClock(scope: CoroutineScope) : MonotonicFrameClock { 
 val clock

    = BroadcastFrameClock { if (isRunning) frameSends.trySend(Unit).getOrThrow() } 
 ... }
  30. val scope = CoroutineScope(Main) 
 val models: StateFlow<ProfileModel> = scope.launchMolecule(

    
 clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) }
  31. class ProfilePresenter(val repo: Repo): MoleculePresenter< ... > { @Composable override

    fun models(events: Flow<ProfileEvent>): ProfileModel { } }
  32. @Composable fun models(events: Flow<ProfileEvent>): ProfileModel { var data by remember

    { mutableStateOf<ProfileData?>(null) } LaunchedEffect(Unit) { data = repo.getProfileData() } }
  33. @Composable fun models(events: Flow<ProfileEvent>): ProfileModel { 
 ... return if

    (data == null) { ProfileModel.Loading } else { ProfileModel.Success(data) } }
  34. class ProfilePresenter( 
 val component1Presenter: Component1Presenter, 
 ...
 ): MoleculePresenter

    { @Composable override fun models(events: Flow<ProfileEvent>): ProfileModel { } }
  35. class ProfilePresenter( ... ): MoleculePresenter { @Composable override fun models(events:

    Flow<ProfileEvent>): ProfileModel { val model = component1Presenter.models(events) } }
  36. Turbine suspend fun <T> Flow<T>.test( validate: suspend ReceiveTurbine<T>.() -> Unit,

    ) { coroutineScope { collectTurbineIn(this).apply { } } }
  37. Turbine fun <T> Flow<T>.collectTurbineIn(scope: CoroutineScope): Turbine<T> { 
 lateinit var

    channel: Channel<T> val job = scope.launch(start = UNDISPATCHED) { channel = collectIntoChannel(this) } return ChannelTurbine(channel, job) }
  38. Turbine fun <T> Flow<T>.collectTurbineIn(scope: CoroutineScope): Turbine<T> { 
 lateinit var

    channel: Channel<T> val job = scope.launch(start = UNDISPATCHED) { channel = collectIntoChannel(this) } return ChannelTurbine(channel, job) }
  39. Turbine fun <T> Flow<T>.collectTurbineIn(scope: CoroutineScope): Turbine<T> { 
 lateinit var

    channel: Channel<T> val job = scope.launch(start = UNDISPATCHED) { channel = collectIntoChannel(this) } return ChannelTurbine(channel, job) }
  40. Turbine val channel = Turbine<Int>() listOf(1, 2, 3).forEach { channel.add(it)

    } channel.skipItems(2) assertEquals(3, channel.awaitItem())
  41. val channel = Turbine<Int>() listOf(1, 2, 3).forEach { channel.add(it) }

    channel.skipItems(2) assertEquals(3, channel.awaitItem()) Turbine
  42. Turbine val channel = Turbine<Int>() listOf(1, 2, 3).forEach { channel.add(it)

    } channel.skipItems(2) assertEquals(3, channel.awaitItem())
  43. Molecule Testing @Test fun `should get success model`() = runBlocking

    { launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { } }
  44. Molecule Testing @Test fun `should get success model`() = runBlocking

    { launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { assertEquals(ProfileModel.Loading, awaitItem()) 
 ... } }
  45. Molecule Testing @Test fun `should get success model`() = runBlocking

    { 
 val events = MutableSharedFlow<Events>() 
 launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { } }
  46. Molecule Testing @Test fun `should get success model`() = runBlocking

    { launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { 
 events.emit(EditProfile) } }
  47. Molecule Testing @Test fun `should get success model`() = runBlocking

    { launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { 
 events.emit(EditProfile) 
 // Assertions } }
  48. Compose Internals class Foo { fun bar() { 
 ...


    } } Code Parsing Analysis 
 & 
 Resolve IR
  49. @Composable fun Greeting(name: String, $composer: Composer <*> ) { Text(name,

    $composer) } Calling context is passed along Compose Internals
  50. @Composable inline fun Layout( ... ) { 
 ReusableComposeNode<ComposeUiNode, Applier<Any

    >> ( update = { set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) ... }, ) } Composition Teaching the runtime
  51. @Composable inline fun ResuableComposeNode( ... ) { 
 ... currentComposer.startReusableNode()

    
 ...
 currentComposer.createNode(factory) } Composition Delegate to composer
  52. @Composable inline fun ResuableComposeNode( ... ) { 
 ... currentComposer.startReusableNode()

    
 ...
 currentComposer.createNode(factory) } Composition Changes List Change(…) Change(…) Change(…)
  53. Composition internal typealias Change = ( applier: Applier <*> ,

    slots: SlotWriter, rememberManager: RememberManager ) -> Unit
  54. Composition class UiApplier(…): AbstractApplier { 
 fun insertBottomUp( ... )

    fun insertTopDown( ... ) fun move( ... ) fun onClear( ... ) fun onEndChanges( ... ) 
 fun remove( ... ) } Applier in Compose UI
  55. Entry Points for Client Libraries • Create a recomposer •

    Specify Applier • Call setContent with composable function
  56. val contextWithClock = currentThreadContext + 
 (pausableClock ?: EmptyCoroutineContext) 


    
 val recomposer = Recomposer(contextWithClock) 
 Compose UI Example
  57. fun launchMolecule(clock: RecompositionClock, body: @Composable () -> T) { with(this

    + clockContext) { val recomposer = Recomposer(coroutineContext) val composition = Composition(UnitApplier, recomposer) launch(start = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } } } Molecule Internals
  58. fun launchMolecule( ... ) { with(this + clockContext) { val

    recomposer = Recomposer(coroutineContext) val composition = Composition(UnitApplier, recomposer) launch(start = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } } } Molecule Internals
  59. Recomposer • Creates coroutine Job • Applies changes for recomposition

    using context • Cancels composition or recompositions when shutdown
  60. fun launchMolecule( ... ) { with(this + clockContext) { val

    recomposer = Recomposer(coroutineContext) val composition = Composition(UnitApplier, recomposer) launch(start = UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } } } Molecule Internals
  61. fun launchMolecule(@Composable body: () -> T) { with(this + clockContext)

    { 
 ... composition.setContent { body() } } } Molecule Internals
  62. fun launchMolecule(emitter: (value: T) -> Unit) { with(this + clockContext)

    { 
 ... composition.setContent { emitter(body()) } } } Molecule Internals
  63. Building State Flows with Compose • How to setup and

    use Molecule • Design Patterns • Testing with Molecule • Molecule Internals