Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon NYC: What does Recomposition mean to your app?

Aida Issayeva
September 02, 2022

Droidcon NYC: What does Recomposition mean to your app?

NOTES:
Includes practical tips 6 and 7

PRESENTED AT:
Droidcon New York 2022

https://nyc.droidcon.com/aida-issayeva/

TALK TITLE:
What does Recomposition mean to your app?

ROOM:
Hopper

DATE:
September 2, 2022

TIME:
13:20 > 40 min

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

September 02, 2022
Tweet

More Decks by Aida Issayeva

Other Decks in Programming

Transcript

  1. What does Recomposition mean to your app? Aida Issayeva Senior

    Software Engineer Android GDE @aida_isay
  2. 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
  3. Jetpack Compose paradigm UI widgets are functions They are stateless

    Different arguments, passed to the functions, trigger updates
  4. SlotTable Android OS Composer Applier UI Node Tree State Snapshot

    System Change List Applier
  5. Deep into the foundation 02

  6. 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
  7. Compose Layers UI Compiler Runtime 1 Composition 2 Layout 3

    Drawing
  8. Compose Layers UI Compiler Runtime 1 Composition 2 Layout 3

    Drawing
  9. 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() } } }
  10. Compose Layers UI Compiler Runtime 1 Composition 2 Layout 3

    Drawing
  11. 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
  12. 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
  13. scheduler for performing recomposition and applying updates to one or

    more Compositions. Recomposer
  14. 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
  15. 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
  16. 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)
  17. 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
  18. Composition process of executing composable functions

  19. Composition Composer SlotTable Change List Manager Update State

  20. Composer & SlotTable @Composable fun Card() { Column { Text(

    text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } }
  21. Composer & SlotTable @Composable fun Card() { Column { Text(

    text = "Compose", fontSize = 24.sp, fontWeight = FontWeight.Bold ) Button(onClick = { }) { Text(text = "Click me") } } } …..
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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 { ... }
  27. 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 { ... }
  28. 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
  29. Applier interface for materializing UI tree

  30. 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
  31. Layout phase steps Column LayoutNode Button LayoutNode Text LayoutNode Text

    LayoutNode Root Node 1. Add child nodes bottom-up to the root node
  32. 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
  33. 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
  34. 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
  35. 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
  36. Final result

  37. What is recomposition? 03

  38. Recomposition re-execution of composable functions in response to state changes

  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. Smart recomposition 04

  47. Inputs are stable Smart recomposition is an intelligent way to

    skip the recomposition of composable functions when their
  48. 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
  49. 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
  50. Stable property types Primitive types: Int, Boolean, Long, Char, Float,

    etc String Annotated with @Stable
  51. What about custom data types?

  52. Inferring Class stability 1 Visit each eligible class 2 Annotate

    with @StabilityInferred 3 Help Compiler to determine
  53. Exceptions Enum Enum Entry Interface Annotation Companion Object Inline Anonymous

    Object Expect Element Inner class
  54. Tools to detect & debug recomposition 05

  55. 1. Compose Compiler Metrics { "skippableComposables" : 64, "restartableComposables" :

    76, "readonlyComposables" : 0, "totalComposables" : 76 } Full analysis ✅ Release build ❗ Skippable fun ✅
  56. 2. Recompose Highlighter Visual appeal ✅ Nested functions ❗ Labor-consuming

  57. 3. Logging Modifier Too much noise ❗ Easy to add

    ✅ Labor-consuming ❗
  58. № of compositions ✅ № of skips ✅ No manual

    add ✅ Canary build ❗ JC v1.2.0-alpha03+ ❗ 4. Recomposition counter in Android Studio EE
  59. Practical tips to leverage smart recomposition 06

  60. 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 { . . . } }
  61. 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) } } }
  62. 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) } }
  63. 4. Use modifier lambdas /** * 👍: With the receiver

    lambda function (Modifier.offset{}) in place, this composable function doesn't get recomposed. */ @Composable fun GoodGreetingsTip4(yAxis: () -> Int) { Row( modifier = Modifier .offset { IntOffset(y = yAxis(), x = 0) }, verticalAlignment = Alignment.Bottom ) { . . . } }
  64. 5. Utilize `derivedStateOf()` /** * 👍 : Only BottomBar gets

    recomposed whenever hideBottomBar's calculated state changes */ @Composable 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) { . . . } })
  65. 6. Declare properties as read-only /** * 👍: `name` property

    inside Fruit is declared as read-only. When Checkbox is toggled, the recomposition of Text UI widget is skipped */ data class Fruit(val name: String) @Composable fun GoodGreetingsTip6(fruit: Fruit) { var isChecked by remember { mutableStateOf(false) } Column { Text(text = fruit.name) Checkbox(checked = isChecked, onCheckedChange = { isChecked = !isChecked }) } }
  66. 7. Leverage KotlinX immutable collections /** * 👍: When it

    comes to reg collections, Compiler can't determine their stability, because they're interfaces and underlying implementation can be mutable, therefore it marks them unstable.When using kotlinX immutable collections, it's guaranteed they'll be immutable, meaning they will be inferred by the Compiler. */ @Composable fun GoodGreetingsTip7() { fruits: ImmutableList<FruitRow> = persistentListOf() Column { fruits.forEach { fruit -> Text(text = fruit.name) } } } in alpha
  67. 7. Tips worth repeating: Logic out of composables Execution in

    any order & parallel Side-effect free ViewModels Callbacks Optimization
  68. 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
  69. 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. Jetpack Compose Stability Explained 16. Compose Tooling Recomposition tips code samples
  70. THANK YOU www.cupsofcode.com Code | Live | Grow @aida_isay