Save 37% off PRO during our Black Friday Sale! »

Hosting states in Compose

Hosting states in Compose

C6e1201f51c1ff186edba69f98476f15?s=128

Dinorah Tovar

October 21, 2021
Tweet

Transcript

  1. Hosting states in Compose Dinorah Tovar Google Developer Expert Platform

    Mobile Engineer 
 @ konfío.mx @ddinorahtovar @ddinorahtovar
  2. But first, lifecycles

  3. Everything has some type of lifecycle @ddinorahtovar 
 General Application

    Activity Fragment •Defines an object that has an Android Lifecycle Classes
  4. @Composable @ddinorahtovar •Composable functions are fast, idempotent, and the “remember”

    • Stateless functions •New data -> Recomposition (redraw if needed and only for the parts that are required)
  5. @Composable @ddinorahtovar •Composable functions works different, they emit data @Composable

    fun view(data: Int) data: Int Composition emit
  6. States and recomposition

  7. States @ddinorahtovar •Are, pretty much, any value that can change

    over time. Name How States A composable has to explicitly be told the new state in order for it to update accordingly. Why When the state is updated, we go over a process of Recomposition
  8. States @ddinorahtovar •Holds a value (Can be read and updated)

    interface State<T : Any?> •The current RecomposeScope will be subscribed to changes of that value
  9. States @ddinorahtovar interface MutableState<T> : State<T> { override var value:

    T } •mutableStateOf creates an observable MutableState<T>, which is an observable type integrated with the compose runtime.
  10. Init composition vs recomposition @ddinorahtovar First Interaction Composition Insert “A”

    Insert “B” Second Interaction Re-composition
  11. Compose @ddinorahtovar •The composition is the state maintained by the

    memory Composition 
 Slot Table Composer Compose Runtime
  12. Compose Runtime @ddinorahtovar Composition 
 Compose Runtime Slot Table Composer

    •Fundamental building blocks of Compose's programming model and state management, and core runtime for the Compose Compiler Plugin to target. •Handle everything for the states
  13. Slot table @ddinorahtovar Composition 
 Slot Table Composer Compose Runtime

    •Current state of the Composition •Updates with every composition •Gap Buffer - data structure •Positional Memorization
  14. Slot table @ddinorahtovar Unused Used B Used A Internal array

    Gap Collection Emit Composition 
 Slot Table Composer Compose Runtime Collect B Collect A
  15. Composer @ddinorahtovar Composition 
 Composer Compose Runtime Slot Table fun

    MultiMeasureLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit, measurePolicy: MeasurePolicy ) { val materialized = currentComposer.materialize(modifier) val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current ReusableComposeNode<LayoutNode, Applier<Any>>( factory = LayoutNode.Constructor, update = { set(materialized, ComposeUiNode.SetModifier) set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) @Suppress("DEPRECATION") init { this.canMultiMeasure = true } }, content = content ) }
  16. Composer @ddinorahtovar Composition 
 Composer Compose Runtime Slot Table fun

    MultiMeasureLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit, measurePolicy: MeasurePolicy ) { val materialized = currentComposer.materialize(modifier) val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current ReusableComposeNode<LayoutNode, Applier<Any>>( factory = LayoutNode.Constructor, update = { set(materialized, ComposeUiNode.SetModifier) set(measurePolicy, ComposeUiNode.SetMeasurePolicy) set(density, ComposeUiNode.SetDensity) set(layoutDirection, ComposeUiNode.SetLayoutDirection) @Suppress("DEPRECATION") init { this.canMultiMeasure = true } }, content = content ) }
  17. Composer @ddinorahtovar Composition 
 Composer Compose Runtime Slot Table @Composable

    inline fun <T : Any?, reified E : Applier<*>> ReusableComposeNode( noinline factory: () -> T, update: @DisallowComposableCalls Updater<T>.() -> Unit, content: @Composable () -> Unit ) { if (currentComposer.applier !is E) invalidApplier() currentComposer.startReusableNode() if (currentComposer.inserting) { currentComposer.createNode(factory) } else { currentComposer.useNode() } currentComposer.disableReusing() Updater<T>(currentComposer).update() currentComposer.enableReusing() content()
  18. Recompose and Remember

  19. Scopes for recomposition @ddinorahtovar •The composition is the state maintained

    by the memory 🧠
  20. Remember 🧠 @ddinorahtovar •@Composable fun can store a single object

    in memory by using the remember composable •Remember the value produced by calculation •calculation will only be evaluated during the composition •Recomposition will always return the value produced by composition
  21. Remember @ddinorahtovar @Composable fun View() { val value = remember

    { mutableStateOf("") } var someValue by remember { mutableStateOf("") } val (someOtherValue, setValue) = remember { mutableStateOf("") } }
  22. Remember @ddinorahtovar @Composable inline fun <T> remember(calculation: @DisallowComposableCalls () ->

    T): T = currentComposer.cache(false, calculation) @Composable fun View() { var variable by remember { mutableStateOf("") } }
  23. Remember @ddinorahtovar •Remember is the one that conserve the state,

    not the @Composable fun @Composable fun view(data: Int) data: Int Composition emit remember by { }
  24. Remember @ddinorahtovar •So this save us from activity recreation? •Short

    answer: No
  25. RememberSaveable @ddinorahtovar @Composabke fun View() { var variable = rememberSaveable

    { mutableStateOf("") } }
  26. RememberSaveable @ddinorahtovar @Composable fun <T : Any> rememberSaveable( vararg inputs:

    Any?, saver: Saver<T, out Any> = autoSaver(), key: String? = null, init: () -> T ): T { val finalKey = if (!key.isNullOrEmpty()) { key } else { currentCompositeKeyHash.toString() } @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>) val registry = LocalSaveableStateRegistry.current val value = remember(*inputs) { val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) } restored ?: init() }
  27. RememberSaveable @ddinorahtovar val registry = LocalSaveableStateRegistry.current val value = remember(*inputs)

    { val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) } restored ?: init() } val saverHolder = remember { mutableStateOf(saver) } saverHolder.value = saver if (registry != null) { DisposableEffect(registry, finalKey, value) { val valueProvider = { with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) } } registry.requireCanBeSaved(valueProvider()) val entry = registry.registerProvider(finalKey, valueProvider) onDispose { entry.unregister() } } } return value }
  28. State hosting @ddinorahtovar •Is a pattern that to move all

    the state variables to the top-level composable function and pass them as arguments to each composable function based on the requirement 
 Decoupling @Composable fun View() @Composable fun View() @Composable fun ViewContent()
  29. State Hosting @ddinorahtovar @Composable fun View(){ var variable by remember

    { mutableStateOf("") } val onValueChange = { text : String-> variable = text } Component(name= variable, onValueChange= onValueChange ) } @Composable fun ViewContent(name : String, onValueChange : (String) -> Unit ){ // Your view }
  30. State hosting @ddinorahtovar 
 Decoupling @Composable fun View() @Composable fun

    View() @Composable fun ViewContent() Unidirectional Data Flows
  31. State Holders @ddinorahtovar •Manage complex state UI elements. •They own

    UI elements' state and UI logic. •State holders are compoundable •ViewModels, Objects or pretty much anything 😅
  32. State Holders @ddinorahtovar @Composable State Holder ViewModel/ Presenters Access to

    
 Logic UI component state UI component Composition
  33. State Holders @ddinorahtovar @Composable are in charge of emi tt

    ing UI elements State holder only contains the UI logic and state
  34. StateFlow and SharedFlow collection

  35. SharedFlow and StateFlow @ddinorahtovar

  36. SharedFlow and StateFlow collection •What do we really need? @ddinorahtovar

    •Launch and Run in specific stage •Cancel when is not longer in a stage •Automatic relaunch and cancellation
  37. SharedFlow and StateFlow collection @ddinorahtovar Safe collection Using a lib

    
 repeatOnLifecycle() flowWithLifecycle() •It’s on alpha, but it works implementation: “androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02"
  38. repeatOnLifecycle() @ddinorahtovar Safe collection Using a lib 
 repeatOnLifecycle() flowWithLifecycle()

    private fun setObservables() = lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiHomeState.collect { // Safe collect } } }
  39. Under the hood of: repeatOnLifecycle() @ddinorahtovar public suspend fun Lifecycle.repeatOnLifecycle(

    state: Lifecycle.State, block: suspend CoroutineScope.() - > Unit ) { require(state ! == Lifecycle.State.INITIALIZED) { "repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state." } if (currentState = == Lifecycle.State.DESTROYED) { return } coroutineScope { withContext(Dispatchers.Main.immediate) { if (currentState === Lifecycle.State.DESTROYED) return@withContext var launchedJob: Job? = null var observer: LifecycleEventObserver? = null } } }
  40. Under the hood of: repeatOnLifecycle() @ddinorahtovar public suspend fun Lifecycle.repeatOnLifecycle(

    state: Lifecycle.State, block: suspend CoroutineScope.() - > Unit ) { require(state ! == Lifecycle.State.INITIALIZED) { "repeatOnLifecycle cannot start work with the INITIALIZED lifecycle state." } if (currentState = == Lifecycle.State.DESTROYED) { return } coroutineScope { withContext(Dispatchers.Main.immediate) { if (currentState === Lifecycle.State.DESTROYED) return@withContext var launchedJob: Job? = null var observer: LifecycleEventObserver? = null } } }
  41. Under the hood of: repeatOnLifecycle() @ddinorahtovar try { suspendCancellableCoroutine<Unit> {

    cont -> val startWorkEvent = Lifecycle.Event.upTo(state) val cancelWorkEvent = Lifecycle.Event.downFrom(state) observer = LifecycleEventObserver { _, event -> if (event == startWorkEvent) { launchedJob = this@coroutineScope.launch(block = block) return@LifecycleEventObserver } if (event == cancelWorkEvent) { launchedJob ?. cancel() launchedJob = null } if (event == Lifecycle.Event.ON_DESTROY) { cont.resume(Unit) } } this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver) } } finally { ?.
  42. == launchedJob = this@coroutineScope.launch(block = block) return@LifecycleEventObserver } if (event

    == cancelWorkEvent) { launchedJob ? . cancel() launchedJob = null } if (event == Lifecycle.Event.ON_DESTROY) { cont.resume(Unit) } } this@repeatOnLifecycle.addObserver(observer as LifecycleEventObserver) } } finally { launchedJob ?. cancel() observer ?. let { this@repeatOnLifecycle.removeObserver(it) } } Under the hood of: repeatOnLifecycle() @ddinorahtovar
  43. repeatOnLifecycle() on fragments @ddinorahtovar Safe collection Using a lib 


    repeatOnLifecycle() flowWithLifecycle() private fun setObservables() = viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiHomeState.collect { // Safe collect } } }
  44. flowWithLifecycle() @ddinorahtovar lifecycleScope.launch { viewModel.state .flowWithLifecycle(this, Lifecycle.State.STARTED).collect { // Safe

    collect } } Safe collection Using a lib 
 repeatOnLifecycle() flowWithLifecycle()
  45. Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class)

    public fun <T> Flow<T>.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow<T> = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState) { this@flowWithLifecycle.collect { send(it) } } close() }
  46. Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class)

    public fun <T> Flow<T>.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow<T> = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState) { this@flowWithLifecycle.collect { send(it) } } close() } Cold flow •With elements that are sent to a SendChannel with a block -> ProducerScope. •It allows elements to be produced by code that is running in a different context or concurrently
  47. Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class)

    public fun <T> Flow<T>.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow<T> = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState) { this@flowWithLifecycle.collect { send(it) } } close() }
  48. Collecting flows in @Composables @ddinorahtovar @Composable fun View(someFlow: Flow<Int>) {

    val lifecycleOwner = LocalLifecycleOwner.current val flowWithLifecycleAware = remember(someFlow, lifecycleOwner) { someFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) } val yourValueThatChange by flowWithLifecycleAware.collectAsState() // Your Composable component }
  49. Give it a try!

  50. Hosting states in Compose Dinorah Tovar Google Developer Expert Platform

    Mobile Engineer 
 @ konfío.mx @ddinorahtovar @ddinorahtovar