Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Hosting states in Compose

Hosting states in Compose

Dinorah Tovar

October 21, 2021
Tweet

More Decks by Dinorah Tovar

Other Decks in Technology

Transcript

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

    Mobile Engineer 
 @ konfío.mx @ddinorahtovar @ddinorahtovar
  2. Everything has some type of lifecycle @ddinorahtovar 
 General Application

    Activity Fragment •Defines an object that has an Android Lifecycle Classes
  3. @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)
  4. 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
  5. 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
  6. 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.
  7. Compose @ddinorahtovar •The composition is the state maintained by the

    memory Composition 
 Slot Table Composer Compose Runtime
  8. 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
  9. Slot table @ddinorahtovar Composition 
 Slot Table Composer Compose Runtime

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

    Gap Collection Emit Composition 
 Slot Table Composer Compose Runtime Collect B Collect A
  11. 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 ) }
  12. 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 ) }
  13. 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()
  14. 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
  15. Remember @ddinorahtovar @Composable fun View() { val value = remember

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

    T): T = currentComposer.cache(false, calculation) @Composable fun View() { var variable by remember { mutableStateOf("") } }
  17. 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 { }
  18. 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() }
  19. 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 }
  20. 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()
  21. 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 }
  22. State hosting @ddinorahtovar 
 Decoupling @Composable fun View() @Composable fun

    View() @Composable fun ViewContent() Unidirectional Data Flows
  23. 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 😅
  24. State Holders @ddinorahtovar @Composable are in charge of emi tt

    ing UI elements State holder only contains the UI logic and state
  25. 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
  26. 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"
  27. repeatOnLifecycle() @ddinorahtovar Safe collection Using a lib 
 repeatOnLifecycle() flowWithLifecycle()

    private fun setObservables() = lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiHomeState.collect { // Safe collect } } }
  28. 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 } } }
  29. 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 } } }
  30. 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 = [email protected](block = block) return@LifecycleEventObserver } if (event == cancelWorkEvent) { launchedJob ?. cancel() launchedJob = null } if (event == Lifecycle.Event.ON_DESTROY) { cont.resume(Unit) } } [email protected](observer as LifecycleEventObserver) } } finally { ?.
  31. == launchedJob = [email protected](block = block) return@LifecycleEventObserver } if (event

    == cancelWorkEvent) { launchedJob ? . cancel() launchedJob = null } if (event == Lifecycle.Event.ON_DESTROY) { cont.resume(Unit) } } [email protected](observer as LifecycleEventObserver) } } finally { launchedJob ?. cancel() observer ?. let { [email protected](it) } } Under the hood of: repeatOnLifecycle() @ddinorahtovar
  32. 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 } } }
  33. 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) { [email protected] { send(it) } } close() }
  34. 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) { [email protected] { 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
  35. 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) { [email protected] { send(it) } } close() }
  36. 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 }
  37. Hosting states in Compose Dinorah Tovar Google Developer Expert Platform

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