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

Chicago Roboto: What does Recomposition mean to your app?

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
Tweet

More Decks by Aida Issayeva

Other Decks in Programming

Transcript

  1. What does Recomposition
    mean to your app?
    Aida Issayeva
    August 2, 2022
    Chicago Roboto
    @aida_isay
    Android GDE

    View Slide

  2. About me
    🤖 Android GDE
    󰠁 Sr. Software Engineer
    󰠅 WomenTechmakers'
    ambassador
    🐦 @aida_isay
    🌐 cupsofcode.com
    📽 @aida_isay

    View Slide

  3. 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

    View Slide

  4. Jetpack Compose paradigm
    UI widgets are functions
    They are stateless
    Different arguments, passed to
    the functions, trigger updates

    View Slide

  5. SlotTable
    Android OS
    Composer
    Applier
    UI Node Tree
    State Snapshot System
    Change List
    Applier

    View Slide

  6. Deep into the
    foundation
    02

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  13. 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

    View Slide

  14. is the scheduler for performing recomposition and
    applying updates to one or more Compositions.
    Recomposer

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. State(“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(false)
    State(“Android”)
    State(true)
    State(false)

    View Slide

  18. 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

    View Slide

  19. Composition
    is a process of executing
    composable functions

    View Slide

  20. Composition
    Composer
    SlotTable
    Change
    List
    Manager Update
    State

    View Slide

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

    View Slide

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

    View Slide

  23. 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

    View Slide

  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)
    C
    A
    R
    D
    C
    O
    L
    U
    M
    N

    View Slide

  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”
    C
    A
    R
    D
    C
    O
    L
    U
    M
    N

    View Slide

  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

    View Slide

  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)
    C
    A
    R
    D
    C
    O
    L
    U
    M
    N
    B
    U
    T
    T
    O
    N
    { ... }

    View Slide

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

    View Slide

  29. 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

    View Slide

  30. Applier
    is an interface for materializing
    UI tree

    View Slide

  31. 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

    View Slide

  32. Layout phase steps
    Column
    LayoutNode
    Button
    LayoutNode
    Text
    LayoutNode
    Text
    LayoutNode
    Root Node
    1. Add child nodes bottom-up to the
    root node

    View Slide

  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
    C
    O
    N
    S
    T
    R
    A
    I
    N
    T
    S

    View Slide

  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
    C
    O
    N
    S
    T
    R
    A
    I
    N
    T
    S
    M
    E
    A
    S
    U
    R
    E
    D
    S
    I
    Z
    E

    View Slide

  35. 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

    View Slide

  36. 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

    View Slide

  37. Final result

    View Slide

  38. What is
    recomposition?
    03

    View Slide

  39. Recomposition
    is a re-execution of composable functions in
    response to state changes

    View Slide

  40. 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

    View Slide

  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

    View Slide

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

    View Slide

  43. 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

    View Slide

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

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

  47. Smart
    recomposition
    04

    View Slide

  48. Inputs are stable
    Smart recomposition is an intelligent way to skip the
    recomposition of composable functions when their

    View Slide

  49. 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

    View Slide

  50. 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

    View Slide

  51. Stable property types
    Primitive types:
    Int, Boolean, Long, Char, Float, etc
    String
    Annotated with @Stable

    View Slide

  52. What about custom
    data types?

    View Slide

  53. Inferring Class stability
    1
    Visit each
    eligible class
    2
    Annotate with
    @StabilityInferred
    3
    Help Compiler
    to determine

    View Slide

  54. Exceptions
    Enum Enum Entry
    Interface Annotation
    Companion Object
    Inline Anonymous Object
    Expect Element
    Inner class

    View Slide

  55. Tools to
    detect & debug
    recomposition
    05

    View Slide

  56. 1. Compose Compiler Metrics
    {
    "skippableComposables" : 64,
    "restartableComposables" :
    76,
    "readonlyComposables" : 0,
    "totalComposables" : 76
    }
    Full analysis

    Release build

    Skippable fun

    View Slide

  57. 2. Recompose Highlighter
    Visual appeal

    Nested functions

    Labor-consuming

    View Slide

  58. 3. Logging Modifier
    Too much noise

    Easy to add

    Labor-consuming

    View Slide

  59. № of compositions

    № of skips

    No manual add

    Canary build

    JC v1.2.0-alpha03+

    4. Recomposition counter in Android Studio EE

    View Slide

  60. Practical tips to
    leverage smart
    recomposition
    06

    View Slide

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

    View Slide

  62. 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) {
    LazyColumn(
    modifier = Modifier
    .recompositionCounter("lazyColumn")
    ) {
    items(
    items = fruits,
    key = { it.name }
    ) { fruit ->
    Item(fruit)
    }
    }
    }

    View Slide

  63. 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)
    }
    }

    View Slide

  64. 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
    ) {
    . . .
    }
    }

    View Slide

  65. 5. Utilize `derivedStateOf()`
    /**
    * 👍 : Only BottomBar gets recomposed whenever
    hideBottomBar's calculated state changes
    */
    @Composable
    private fun GoodGreetingsTip5(list: List) {
    val listState = rememberLazyListState()
    val hideBottomBar = remember {
    derivedStateOf { listState.isScrollInProgress }
    }
    Scaffold(bottomBar = {
    AnimatedVisibility(visible = !hideBottomBar.value)
    {
    BottomAppBar(content = { BottomNavigationItems
    ()})
    }
    }, content = {
    LazyColumn(state = listState) {
    . . .
    }
    })

    View Slide

  66. 6. Tips worth repeating:
    Logic out of
    composables
    Execution in
    any order &
    parallel
    Side-effect
    free
    ViewModels Callbacks
    Optimization

    View Slide

  67. 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

    View Slide

  68. 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

    View Slide

  69. THANK YOU
    www.cupsofcode.com
    Code | Live | Grow
    @aida_isay

    View Slide