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. Mohit Sarveiya Building State Flows with Compose @heyitsmohit

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

    use Molecule
  3. Building State Flows with Compose • How to setup and

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

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

    use Molecule • Design Patterns • Testing with Molecule • Molecule Internals
  6. Building State Flows with Compose

  7. Building State Flows with Compose

  8. Compose vs Compose UI

  9. Compose • General purpose tool for managing tree of nodes.

  10. Compose • General purpose tool for managing tree of nodes.

  11. Compose • General purpose tool for managing tree of nodes.

    Node can be anything
  12. Compose • Compose Runtime • Kotlin Complier Plugin • State

    Snapshot System
  13. Compose UI • UI Toolkit Views

  14. Molecule @Composable fun MoleculePresenter( events: Flow<Event>, ): Model

  15. Molecule @Composable fun MoleculePresenter( events: Flow<Event>, ): Model Compose Runtime

  16. Molecule @Composable fun MoleculePresenter( events: Flow<Event>, ): Model Compose Runtime

    State Flow 
 
 Flow
  17. val userFlow = db.userObservable() val balanceFlow = db.balanceObservable()

  18. @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") } }
  19. @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") } }
  20. @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
  21. @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
  22. class ProfilePresenter(val db: Db) { fun transform(events: Flow<Event>) : Flow<ProfileModel>

    { } }
  23. 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 }
  24. class ProfilePresenter(val db: Db) { fun transform(events: Flow<Event>) : Flow<ProfileModel>

    { }
  25. 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 -> } } }
  26. 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
  27. Molecule @Composable fun MoleculePresenter( ... ): Model Compose Runtime State

    Flow 
 
 Flow
  28. @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) } }
  29. @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) } }
  30. @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) } }
  31. val scope = CoroutineScope(Main)

  32. val scope = CoroutineScope(Main) scope.launchMolecule( 
 clock = RecompositionClock.ContextClock 


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


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


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


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

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

    
 emitter = { value -> } 
 ) { ProfilePresenter(events, randomService) } Will not respect back-pressure
  38. val models: Flow<ProfileModel> = moleculeFlow( 
 clock = RecompositionClock.ContextClock 


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

    
 clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) } Frame Clock
  40. Composable Recomposition Enter the composition Leave the composition

  41. Composable Wait for next frame Enter the composition Leave the

    composition
  42. Composable Wait for next frame Enter the composition Leave the

    composition Monotonic Frame Clock
  43. Monotonic 
 Frame Clock Pull Based System Pull Signal

  44. Monotonic 
 Frame Clock Pull Based System Pull Signal Choreographer

    Performs Pull for UI
  45. val scope = CoroutineScope(Main) 
 
 fun <T> CoroutineScope.launchMolecule( ...

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

    val key: CoroutineContext.Key <*> get() = Key companion object Key : CoroutineContext.Key<MonotonicFrameClock> }
  47. Recomposition Clocks • Context clock • Immediate

  48. Context Clock • Use MonotonicFrameClock from calling Coroutine 
 


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

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

    (clock) { RecompositionClock.ContextClock -> EmptyCoroutineContext RecompositionClock.Immediate -> GatedFrameClock(this) } ... }
  51. val dispatcher = TestCoroutineDispatcher() val clock = BroadcastFrameClock() 
 Frame

    clock provided by Compose
  52. val dispatcher = TestCoroutineDispatcher() val clock = BroadcastFrameClock() 
 val

    scope = CoroutineScope(dispatcher + clock)
  53. var value = 0 
 scope.launchMolecule(RecompositionClock.ContextClock, { value = it

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

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

    } LaunchedEffect(Unit) { while (true) { delay(100) count ++ } } count }
  56. dispatcher.advanceTimeBy(100) assertEquals(1, value)

  57. scope.launchMolecule(RecompositionClock.ContextClock) { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit)

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

    { while (true) { delay(100) count ++ } } count }
  59. assertEquals(0, value) Initial Composition

  60. assertEquals(0, value) 
 dispatcher.advanceTimeBy(100) 
 


  61. scope.launchMolecule(RecompositionClock.ContextClock) { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit)

    { while (true) { delay(100) count ++ } } count }
  62. assertEquals(0, value) 
 dispatcher.advanceTimeBy(100) 
 
 assertEquals(1, value)

  63. val dispatcher = TestCoroutineDispatcher() val clock = BroadcastFrameClock() 
 val

    scope = CoroutineScope(dispatcher + clock)
  64. assertEquals(0, value) 
 dispatcher.advanceTimeBy(100) 
 
 clock.sendFrame(0) 
 
 assertEquals(1,

    value)
  65. Context Clock • Use MonotonicFrameClock from calling Coroutine 
 


    Context • Use Molecule with built in Android Frame Clock
  66. Recomposition Clocks • Context clock • Immediate

  67. Gated Frame Clock • Request for a frame immediately succeeds

    • Can be stopped
  68. Immediate class GatedFrameClock(scope: CoroutineScope) : MonotonicFrameClock { 
 val frameSends

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

    = BroadcastFrameClock { if (isRunning) frameSends.trySend(Unit).getOrThrow() } 
 ... }
  70. Recomposition Clocks • Context clock • Immediate

  71. val scope = CoroutineScope(Main) 
 val models: StateFlow<ProfileModel> = scope.launchMolecule(

    
 clock = RecompositionClock.ContextClock 
 ) { ProfilePresenter(events, randomService) }
  72. Launching Molecules • Create coroutine scope • Specify frame clock

    • Provide Composable function
  73. Design Patterns

  74. View

  75. View Presenter Events

  76. View Presenter Events Model

  77. View Presenter Events Model Remote Service Result

  78. interface MoleculePresenter<Event, Model> { @Composable fun models(events: Flow<Event>): Model }

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

    ProfileModel { } }
  80. class ProfilePresenter(val repo: Repo): MoleculePresenter< ... > { @Composable override

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

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

    (data == null) { ProfileModel.Loading } else { ProfileModel.Success(data) } }
  83. val models = scope.launchMolecule( ... ) { presenter.models(events) } Events

    Flow
  84. sealed interface ProfileEvents { object Edit: ProfileEvents }

  85. Channel Event Presenter

  86. @Composable fun models(events: Flow<ProfileEvent>): ProfileModel { LaunchedEffect(events) { events.collect {

    when (it) { Events.Edit -> { ... } } } } }
  87. @Composable fun models(events: Flow<ProfileEvent>): ProfileModel { CollectEffect(events) { when (it)

    { Events.ClickedSubmit -> { ... } } } }
  88. View Presenter Events Model Remote Service Result

  89. Molecule Presenter • Separation of concerns • Composite presenters

  90. Component 1 Component 2 Component 3

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

    ProfileModel { } } Can get complex
  92. class Component1Presenter: MoleculePresenter<Event, Model> { @Composable override fun models(events: Flow<Event>):

    Model { } }
  93. class ProfilePresenter( 
 val component1Presenter: Component1Presenter, 
 ...
 ): MoleculePresenter

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

    Flow<ProfileEvent>): ProfileModel { val model = component1Presenter.models(events) } }
  95. View Presenter Events Model Remote Service Result

  96. Testing

  97. val models: StateFlow<ProfileModel> = scope.launchMolecule( 
 clock = RecompositionClock.ContextClock 


    ) { ProfilePresenter(randomService) } Testing
  98. Testing • Turbine • Molecule Testing Dependency [Deprecated]

  99. Turbine • Queue based testing • Use to be an

    extension in SQL Delight
  100. None
  101. Turbine interface ReceiveTurbine<T> { fun awaitEvent(): Event<T> fun awaitError(): Throwable

    suspend fun awaitItem(): T ... }
  102. Turbine flow { emit("one") emit("two") }.test { 
 assertEquals("one", awaitItem())

    
 assertEquals("two", awaitItem()) }
  103. Turbine flow { emit("one") emit("two") }.test { 
 assertEquals("one", awaitItem())

    
 assertEquals("two", awaitItem()) }
  104. Turbine suspend fun <T> Flow<T>.test( validate: suspend ReceiveTurbine<T>.() -> Unit,

    ) { coroutineScope { collectTurbineIn(this).apply { } } }
  105. 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) }
  106. 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) }
  107. 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) }
  108. Turbine class ChannelTurbine<T>( channel: Channel<T> = Channel(UNLIMITED) ) : Turbine<T>

    { suspend fun awaitItem(): T = channel.awaitItem() }
  109. Turbine val channel = Turbine<Int>() listOf(1, 2, 3).forEach { channel.add(it)

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

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

    } channel.skipItems(2) assertEquals(3, channel.awaitItem())
  112. val models: StateFlow<ProfileModel> = scope.launchMolecule( 
 clock = RecompositionClock.ContextClock 


    ) { profilePresenter.models(events) } Testing
  113. Testing Cases • Verify correct models are created • Verify

    side effects from events
  114. Testing • runBlocking • Immediate Frame Clock • Use Turbine

  115. Molecule Testing @Test fun `should get success model`() = runBlocking

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

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

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

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

    { launchMolecule(RecompositionClock.Immediate) { profilePresenter.models(events) }.test { 
 events.emit(EditProfile) 
 // Assertions } }
  120. Testing • runBlocking • Immediate Frame Clock • Use Turbine

  121. Molecule Internals

  122. val scope = CoroutineScope(Main) 
 
 fun <T> CoroutineScope.launchMolecule( ...

    ) { // Setup Recomposer }
  123. Internals • Compose Compiler & Runtime • How Molecule hooks

    into Compose Runtime
  124. Compose Components • Composition • Recomposer • Applier

  125. Compose Internals class Foo { fun bar() { 
 ...


    } } Code Parsing Analysis 
 & 
 Resolve IR
  126. @Composable fun Greeting(name: String) { Text(name) } Compose Internals

  127. @Composable fun Greeting(name: String) { Text(name) } Creates Deferred Change

    Compose Internals
  128. @Composable fun Greeting(name: String, $composer: Composer <*> ) { Text(name)

    } Connects to Compose Runtime Compose Internals
  129. @Composable fun Greeting(name: String, $composer: Composer <*> ) { Text(name,

    $composer) } Calling context is passed along Compose Internals
  130. suspend fun getData(payload: Payload) { ... } Compose Internals

  131. suspend fun getData(payload: Payload, $cont: Continuation) { anotherMethod($cont) } Compose

    Internals
  132. @Composable fun Greeting(name: String) { Text(name) } Creates Deferred Change

    Composition
  133. @Composable inline fun Layout( ... ) { 
 ReusableComposeNode<ComposeUiNode, Applier<Any

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

    
 ...
 currentComposer.createNode(factory) } Composition Delegate to composer
  135. Composition @Composable inline fun remember( ... ) { 
 ...

    currentComposer.cache( ... ) 
 }
  136. @Composable inline fun ResuableComposeNode( ... ) { 
 ... currentComposer.startReusableNode()

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

    slots: SlotWriter, rememberManager: RememberManager ) -> Unit
  138. Composition Changes List Change(…) Change(…) Change(…) Slot table Applier

  139. Composition class UiApplier(…): AbstractApplier { 
 fun insertBottomUp( ... )

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

    Specify Applier • Call setContent with composable function
  141. Recomposer • Trigger recompositions • Determine which thread to compose

    or recompose on
  142. val contextWithClock = currentThreadContext + 
 (pausableClock ?: EmptyCoroutineContext) 


    
 val recomposer = Recomposer(contextWithClock) 
 Compose UI Example
  143. 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
  144. fun launchMolecule( ... ) { with(this + clockContext) { val

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

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

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

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

    { 
 ... composition.setContent { emitter(body()) } } } Molecule Internals
  149. Molecule Internals var flow: MutableStateFlow<T>? = null launchMolecule( emitter =

    { value -> outputFlow.value = value }, ... )
  150. val models: StateFlow<ProfileModel> = scope.launchMolecule(…) { profilePresenter.models(events) } models.collect {

    ... }
  151. Building State Flows with Compose • How to setup and

    use Molecule • Design Patterns • Testing with Molecule • Molecule Internals
  152. Thank You! www.codingwithmohit.com @heyitsmohit