Slide 1

Slide 1 text

Hosting states in Compose Dinorah Tovar Google Developer Expert Platform Mobile Engineer 
 @ konfío.mx @ddinorahtovar @ddinorahtovar

Slide 2

Slide 2 text

But first, lifecycles

Slide 3

Slide 3 text

Everything has some type of lifecycle @ddinorahtovar 
 General Application Activity Fragment •Defines an object that has an Android Lifecycle Classes

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

@Composable @ddinorahtovar •Composable functions works different, they emit data @Composable fun view(data: Int) data: Int Composition emit

Slide 6

Slide 6 text

States and recomposition

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

States @ddinorahtovar •Holds a value (Can be read and updated) interface State •The current RecomposeScope will be subscribed to changes of that value

Slide 9

Slide 9 text

States @ddinorahtovar interface MutableState : State { override var value: T } •mutableStateOf creates an observable MutableState, which is an observable type integrated with the compose runtime.

Slide 10

Slide 10 text

Init composition vs recomposition @ddinorahtovar First Interaction Composition Insert “A” Insert “B” Second Interaction Re-composition

Slide 11

Slide 11 text

Compose @ddinorahtovar •The composition is the state maintained by the memory Composition 
 Slot Table Composer Compose Runtime

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Slot table @ddinorahtovar Composition 
 Slot Table Composer Compose Runtime •Current state of the Composition •Updates with every composition •Gap Buffer - data structure •Positional Memorization

Slide 14

Slide 14 text

Slot table @ddinorahtovar Unused Used B Used A Internal array Gap Collection Emit Composition 
 Slot Table Composer Compose Runtime Collect B Collect A

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Composer @ddinorahtovar Composition 
 Composer Compose Runtime Slot Table @Composable inline fun > ReusableComposeNode( noinline factory: () -> T, update: @DisallowComposableCalls Updater.() -> Unit, content: @Composable () -> Unit ) { if (currentComposer.applier !is E) invalidApplier() currentComposer.startReusableNode() if (currentComposer.inserting) { currentComposer.createNode(factory) } else { currentComposer.useNode() } currentComposer.disableReusing() Updater(currentComposer).update() currentComposer.enableReusing() content()

Slide 18

Slide 18 text

Recompose and Remember

Slide 19

Slide 19 text

Scopes for recomposition @ddinorahtovar •The composition is the state maintained by the memory 🧠

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Remember @ddinorahtovar @Composable fun View() { val value = remember { mutableStateOf("") } var someValue by remember { mutableStateOf("") } val (someOtherValue, setValue) = remember { mutableStateOf("") } }

Slide 22

Slide 22 text

Remember @ddinorahtovar @Composable inline fun remember(calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation) @Composable fun View() { var variable by remember { mutableStateOf("") } }

Slide 23

Slide 23 text

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 { }

Slide 24

Slide 24 text

Remember @ddinorahtovar •So this save us from activity recreation? •Short answer: No

Slide 25

Slide 25 text

RememberSaveable @ddinorahtovar @Composabke fun View() { var variable = rememberSaveable { mutableStateOf("") } }

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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 }

Slide 28

Slide 28 text

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()

Slide 29

Slide 29 text

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 }

Slide 30

Slide 30 text

State hosting @ddinorahtovar 
 Decoupling @Composable fun View() @Composable fun View() @Composable fun ViewContent() Unidirectional Data Flows

Slide 31

Slide 31 text

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 😅

Slide 32

Slide 32 text

State Holders @ddinorahtovar @Composable State Holder ViewModel/ Presenters Access to 
 Logic UI component state UI component Composition

Slide 33

Slide 33 text

State Holders @ddinorahtovar @Composable are in charge of emi tt ing UI elements State holder only contains the UI logic and state

Slide 34

Slide 34 text

StateFlow and SharedFlow collection

Slide 35

Slide 35 text

SharedFlow and StateFlow @ddinorahtovar

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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"

Slide 38

Slide 38 text

repeatOnLifecycle() @ddinorahtovar Safe collection Using a lib 
 repeatOnLifecycle() flowWithLifecycle() private fun setObservables() = lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiHomeState.collect { // Safe collect } } }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Under the hood of: repeatOnLifecycle() @ddinorahtovar try { suspendCancellableCoroutine { 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 { ?.

Slide 42

Slide 42 text

== 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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

flowWithLifecycle() @ddinorahtovar lifecycleScope.launch { viewModel.state .flowWithLifecycle(this, Lifecycle.State.STARTED).collect { // Safe collect } } Safe collection Using a lib 
 repeatOnLifecycle() flowWithLifecycle()

Slide 45

Slide 45 text

Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class) public fun Flow.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState) { [email protected] { send(it) } } close() }

Slide 46

Slide 46 text

Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class) public fun Flow.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow = 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

Slide 47

Slide 47 text

Under the hood of: flowWithLifecycle() @ddinorahtovar @OptIn(ExperimentalCoroutinesApi : : class) public fun Flow.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow = callbackFlow { lifecycle.repeatOnLifecycle(minActiveState) { [email protected] { send(it) } } close() }

Slide 48

Slide 48 text

Collecting flows in @Composables @ddinorahtovar @Composable fun View(someFlow: Flow) { val lifecycleOwner = LocalLifecycleOwner.current val flowWithLifecycleAware = remember(someFlow, lifecycleOwner) { someFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) } val yourValueThatChange by flowWithLifecycleAware.collectAsState() // Your Composable component }

Slide 49

Slide 49 text

Give it a try!

Slide 50

Slide 50 text

Hosting states in Compose Dinorah Tovar Google Developer Expert Platform Mobile Engineer 
 @ konfío.mx @ddinorahtovar @ddinorahtovar