Share
Embed
Chicago Roboto: What does Recomposition mean to your app?
PRESENTED AT:
Chicago Roboto 2022
https://chicagoroboto.com/speaker/aida-issayeva/
DATE:
August 2, 2022 04:00 PM - 04:45 PM
ROOM:
Room 1
DESCRIPTION:
You’ve heard a lot that Jetpack Compose is a declarative UI toolkit and it recomposes only the components that changed. But what does it exactly mean? How does it apply not only in the scale of one composable function but the entire app? Well, it turns out there is more than meets the eye when it comes to recomposition, and in this talk, we will deep dive into its benefits, costs, and gotchas. You’ll learn some practical tips to efficiently build functions in Jetpack Compose.
MORE TALKS & ARTICLES FROM ME: https://cupsofcode.com/talks/
Aida Issayeva
August 02, 2022
Transcript
69 slides
-
What does Recomposition mean to your app? Aida Issayeva August
2, 2022 Chicago Roboto @aida_isay Android GDE -
About me 🤖 Android GDE Sr. Software Engineer
WomenTechmakers' ambassador 🐦 @aida_isay 🌐 cupsofcode.com 📽 @aida_isay -
TODAY’S AGENDA 02 What is Recomposition? 03 Smart Recomposition 04
Tools to detect & debug 05 Practical tips 01 Deep into the foundation 06 Jetpack Compose Paradigm -
Jetpack Compose paradigm UI widgets are functions They are stateless
Different arguments, passed to the functions, trigger updates -
SlotTable Android OS Composer Applier UI Node Tree State Snapshot
System Change List Applier -
Compose Phases 1 Composition Calls composable functions and creates a
description of the UI 2 Layout Measures the layout elements and their children and places them on the screen 3 Drawing Draws the UI elements to the screen -
From Android OS: • Context • Configuration • Lifecycle •
Keyboard state • SavedStateInstance • etc CompositionLocals Entrance to Jetpack Compose class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... setContent { Card() } } } -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list -
is the scheduler for performing recomposition and applying updates to
one or more Compositions. Recomposer -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list WindowRecomposer: LifecycleAware recomposer -
Android Lifecycle & Compose viewTreeLifecycleOwner .lifecycle.addObserver( object : LifecycleEventObserver {
override fun onStateChanged (lifecycleOwner: LifecycleOwner , event: Lifecycle .Event) { val self = this when (event) { Lifecycle.Event.ON_CREATE -> runRecomposeScope .launch(start = CoroutineStart .UNDISPATCHED) { try { recomposer.runRecomposeAndApplyChanges() } finally { lifecycleOwner .lifecycle.removeObserver( self) } } Lifecycle .Event.ON_START -> pausableClock ?.resume() Lifecycle .Event.ON_STOP -> pausableClock ?.pause() Lifecycle.Event.ON_DESTROY -> { recomposer.cancel() } Lifecycle .Event.ON_PAUSE -> { // Nothing } Lifecycle .Event.ON_RESUME -> { // Nothing } Lifecycle .Event.ON_ANY -> { // Nothing } } } } ) Android OS State Snapshot System -
State<String>(“Android”) State Snapshot System @Composable fun StateSnapshotSystem() { val name
= remember { mutableStateOf(Android) } val isImportant = remember { mutableStateOf(false) } Column { Checkbox(checked = isImportant.value, onCheckedChange = { isImportant.value = !isImportant.value }) Text(text = name.value, modifier = Modifier.clickable { name.value = swapName(name.value) } ) } } State<Boolean>(false) State<String>(“Android”) State<Boolean>(true) State<Boolean>(false) -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } ….. -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. C A R D -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. Group(456) C A R D C O L U M N -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. Group(456) “Compose” C A R D C O L U M N -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. Group(456) “Compose” Group(789) C A R D C O L U M N B U T T O N -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. Group(456) “Compose” Group(789) C A R D C O L U M N B U T T O N { ... } -
Composer & SlotTable @Composable fun Card() { Column { Text(
text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } Group(123) ….. Group(456) “Compose” Group(789) “Click me” C A R D C O L U M N B U T T O N { ... } -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list UiApplier: Applier implementation WindowRecomposer: LifecycleAware recomposer -
Layout phase steps Column LayoutNode Button LayoutNode Text LayoutNode Text
LayoutNode Root Node 1. Add child nodes bottom-up to the root node -
Layout phase steps Column LayoutNode Button LayoutNode Text LayoutNode Text
LayoutNode Root Node 1. Add child nodes bottom-up to the root node 2. Measure the child nodes C O N S T R A I N T S -
Layout phase steps Column LayoutNode Button LayoutNode Text LayoutNode Text
LayoutNode Root Node 1. Add child nodes bottom-up to the root node 2. Measure the child nodes 3. Define the size of a parent node C O N S T R A I N T S M E A S U R E D S I Z E -
Layout phase steps Column LayoutNode Button LayoutNode Text LayoutNode Text
LayoutNode Root Node 1. Add child nodes bottom-up to the root node 2. Measure the child nodes 3. Define the size of a parent node 4. Place the child nodes on screen C O N S T R A I N T S M E A S U R E D S I Z E -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list UiApplier: Applier implementation WindowRecomposer: LifecycleAware recomposer -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list UiApplier: Applier implementation WindowRecomposer: LifecycleAware recomposer -
Recomposition in SlotTable: example 1 @Composable fun Card() { val
counter = remember { mutableStateOf(0) } Column { Text(text = "Compose") Button(onClick = { counter.value++ }) { Text(text = "Click me $counter") } } } Group(456) ….. Group(789) “Compose” Group(101) “Click me 0” C A R D C O L U M N B U T T O N { ... } Group(123) State(0) R E M E M B E R -
Recomposition in SlotTable: example 1 @Composable fun Card() { val
counter = remember { mutableStateOf(0) } Column { Text(text = "Compose") Button(onClick = { counter.value++ }) { Text(text = "Click me $counter") } } } Group(456) ….. Group(789) “Compose” Group(101) “Click me 0” C A R D C O L U M N B U T T O N { ... } Group(123) State(0) R E M E M B E R -
Recomposition in SlotTable: example 1 @Composable fun Card() { val
counter = remember { mutableStateOf(0) } Column { Text(text = "Compose") Button(onClick = { counter.value++ }) { Text(text = "Click me $counter") } } } Group(456) ….. Group(789) “Compose” Group(101) “Click me 1” C A R D C O L U M N B U T T O N { ... } Group(123) State(1) R E M E M B E R -
Recomposition in SlotTable: example 2 @Composable fun Card() { val
counter = remember { mutableStateOf(0) } Column { Text(text = "Compose $counter") Button(onClick = { counter.value++ }) { Text(text = "Click me") } } } Group(456) ….. Group(789) “Compose 0” Group(101) “Click me” C A R D C O L U M N B U T T O N { ... } Group(123) State(0) R E M E M B E R -
Recomposition in SlotTable: example 2 @Composable fun Card() { val
counter = remember { mutableStateOf(0) } Column { Text(text = "Compose $counter") Button(onClick = { counter.value++ }) { Text(text = "Click me") } } } Group(456) ….. Group(789) “Compose 1” Group(101) “Click me” C A R D C O L U M N B U T T O N { ... } Group(123) State(1) R E M E M B E R -
Compose Phases 1 Composition Calls composable functions and creates a
description of the UI 2 Layout Measures the layout elements and their children and places them on the screen 3 Drawing Draws the UI elements to the screen -
Inputs are stable Smart recomposition is an intelligent way to
skip the recomposition of composable functions when their -
UI Compiler Runtime 1 Composition 2 Layout 3 Drawing Composition:
1. SlotTable 2. Composer 3. Changes List Applier: UI materializer Recomposer: 1. State Snapshot System 2. Compositions - list UiApplier: Applier implementation WindowRecomposer: LifecycleAware recomposer -
Stable type contract rules The result of `equals()` function is
the same All public properties are stable Public property is changed - composition is notified Rule 1 Rule 3 Rule 2 -
Stable property types Primitive types: Int, Boolean, Long, Char, Float,
etc String Annotated with @Stable -
Inferring Class stability 1 Visit each eligible class 2 Annotate
with @StabilityInferred 3 Help Compiler to determine -
Exceptions Enum Enum Entry Interface Annotation Companion Object Inline Anonymous
Object Expect Element Inner class -
1. Compose Compiler Metrics { "skippableComposables" : 64, "restartableComposables" :
76, "readonlyComposables" : 0, "totalComposables" : 76 } Full analysis ✅ Release build ❗ Skippable fun ✅ -
3. Logging Modifier Too much noise ❗ Easy to add
✅ Labor-consuming ❗ -
№ of compositions ✅ № of skips ✅ No manual
add ✅ Canary build ❗ JC v1.2.0-alpha03+ ❗ 4. Recomposition counter in Android Studio EE -
1. Break down composable functions /** * 👍 : only
associated column with changes gets recomposed. */ @Composable fun GoodGreetingsTip1() { Column { Greeting("Android", 1, Gray200) Greeting("Compose", 2, Brown200) } } @Composable fun Greeting(name: String, columnNumber: Int, background: Color) { Column { . . . } } -
2. Use the key composable /** * 👍 : the
key is set, and when item in the middle is removed, only LazyColumn gets recomposed * and not its children */ @Composable fun GoodGreetingsTip2(fruits: List<String>) { LazyColumn( modifier = Modifier .recompositionCounter("lazyColumn") ) { items( items = fruits, key = { it.name } ) { fruit -> Item(fruit) } } } -
3. Read the state value at the lowest composable function
/** * 👍 : only inner column and its children get recomposed, because the read happens * in one of the children. */ @Composable fun GoodGreetingsTip3(cardInfo: () -> CardInfo) { Column { UpperRow() GoodRow(cardInfo = cardInfo) } } @Composable private fun GoodRow(cardInfo: () -> CardInfo) { Row { GoodInnerColumn(cardInfo = cardInfo) } } -
4. Use modifier lambdas /** * 👍: With the receiver
lambda function (Modifier.offset{}) in place, this composable function doesn't get recomposed. */ @Composable private fun GoodGreetingsTip4(yAxis: () -> Int) { Row( modifier = Modifier .offset { IntOffset(y = yAxis(), x = 0) }, verticalAlignment = Alignment.Bottom ) { . . . } } -
5. Utilize `derivedStateOf()` /** * 👍 : Only BottomBar gets
recomposed whenever hideBottomBar's calculated state changes */ @Composable private fun GoodGreetingsTip5(list: List<FruitRow>) { val listState = rememberLazyListState() val hideBottomBar = remember { derivedStateOf { listState.isScrollInProgress } } Scaffold(bottomBar = { AnimatedVisibility(visible = !hideBottomBar.value) { BottomAppBar(content = { BottomNavigationItems ()}) } }, content = { LazyColumn(state = listState) { . . . } }) -
6. Tips worth repeating: Logic out of composables Execution in
any order & parallel Side-effect free ViewModels Callbacks Optimization -
Recap UI widgets are composable functions 3 Compose Phases UI
widgets go through are Composition, Layout, Draw Recomposition is an expected process 3 Compose Layers Jetpack Compose uses to create composable functions: Compiler, Runtime, UI Practical tips should be used to avoid common recomposition pitfalls Smart recomposition should be leveraged as much as possible to teach Compiler -
Resources 1. Jetpack Compose Internals 2. Jetpack Compose source code
3. Understanding Jetpack Compose — part 1 of 2 4. Understanding Jetpack Compose — part 2 of 2 5. Recomposition made easy 6. RecomposeHighlighter code snippet 7. Compose Compiler Metrics 8. A historical introduction to the Compose reactive state model 9. What is “donut-hole” skipping in Jetpack Compose? 10. Compose performance 11. Composable Metrics 12. Thinking in Compose 13. Jetpack Compose Phases 14. Jetpack Compose Lifecycle 15. Compose Tooling Recomposition tips code samples