React, Meet Compose

React, Meet Compose

Jetpack Compose is a new declarative UI framework that is being developed in the open for Android. It has a very similar programming model to React, and this talk will dive deep into the internals of both to explain the similarities and differences in the architectures of both, and how React Native might be able to leverage some of this technology long term.

Talk Recording: https://www.youtube.com/watch?v=4EFjDSijAZU

Fc5aa209594b8c9c16af3c217d5c0f2a?s=128

Leland Richardson

July 12, 2019
Tweet

Transcript

  1. REACT, MEET COMPOSE LELAND RICHARDSON @intelligibabble

  2. What is Compose? Let’s first start by going over what

    Compose is at a high level before diving in.
  3. What is Compose? Declarative UI Runtime Compose is a declarative

    component-based UI runtime for Android. Much of its design has been inspired by other declarative UI frameworks such as React, Flutter, etc.
  4. What is Compose? Compiler Plugin It mis more than just

    a runtime. It’s a compiler as well! We work as a Kotlin Compiler plugin.
 This means that to write apps with Compose, you need to use Kotlin. For those not familiar, Kotlin is now the primary language of the Android platform, as of Google I/O 2019.
  5. What is Compose? Built by Team Compose is being built

    by the Android UI Toolkit team. This is the same team that owns View.java!
  6. What is Compose? Complete UI Toolkit It also contains a

    complete ground-up rewrite of Android’s UI toolkit. This is a *big* undertaking.
 Compose runtime can target views, but the android toolkit is being completely rewritten to target a declarative component-based programming model.
 Not going to talk a ton about the specifics of this today, but there’s a lot here that is really exciting. Future talk maybe.
  7. What is Compose? Unbundled from OS This whole project is

    being built in user-space as a library unbundled from the underlying Android operating system. 
 This means we can iterate out of band from OS releases, and you don’t have to wait for OEMs to target new features in apps.
  8. What is Compose? Open Source It’s also open source! Google

    is developing it in the open. You can see progress, understand inner workings, contribute, etc.
  9. What is Compose? “pre”-Alpha Compose (as of July 2019) is

    *not* ready for production use. It is not ready to be used or relied upon. Lots of fundamental changes are still taking place, and many parts of the runtime and surrounding toolkit are either not done, or haven’t reached maturity.
  10. So naturally, I’m really excited about this because I work

    on it. But here we are at a React conference, and I’m talking about this Android thing. Some of you might be thinking: Should I even care about this? Is this relevant to me? After all, I just told you you couldn’t even use it if you wanted to!
  11. This is an exciting time! That’s fair. But let’s take

    a step back to look at the landscape of mobile UI development… In just the last few months, both Google and Apple have both publicly announced *declarative*, *component-based*, UI frameworks that represent *fundamental* shifts in the 1st-party UI frameworks on Android and iOS. Declarative UI has spread. We have passed the inflection point. It is becoming THE way to write UI. This is really exciting. It means that there will be less impedance mismatch between platforms/frameworks in the future. There is lots of innovation in this space happening, and lots of potential for collaboration, cross-pollination of ideas.
  12. React, meet Compose OK. So that presents the motivation for

    my talk. I’m not trying to convince you to use Compose.
 
 Instead, I want to talk about some of the similarities of Compose and React, but also some of the key differences between the two. This will be a technical deep dive, not a surface-level introduction. If you’re familiar with React, some of the differences may surprise you. Understand that none of these differences were introduced without a significant amount of thought, and may represent entirely new ways to do certain things.
  13. Programming Model ==="" The programming model between React and Compose

    is actually quite similar.
  14. Execution Model !==$$ The execution model, however, is very different.

  15. @Composable fun Button( text: String, onPress: () ->& Unit )

    { // ... } function Button({ text, onPress }) { // ... } Both React and Compose have the notion of a “Component” as the building block, which in both cases is defined as a function. In Compose, a @Composable annotation is required to treat the function as a component. In React, props come in as a single object with named properties. In Compose, props are just normal parameters since Kotlin has named arguments.
  16. 
 
 
 
 @Composable fun Button( text: String, onPress:

    () ->& Unit ) { Touchable(onPress=onPress) { View { Text(text=text) } } } 
 
 
 
 function Button({ text, onPress }) { return ( <Touchable onPress={onPress}> <View> <Text text={text} /> </View> </Touchable> ) } Notice that the Compose component does not return anything! We will show why this is later on in the talk.
 As you can see here, Compose does not have a JSX-like syntax. The analogous thing is to just call a composable function. You can think of a JSX element and the invocation of a composable function as equivalent.
  17. @Composable fun Button( text: String, onPress: () ->& Unit )

    { Touchable(onPress=onPress) { View { Text(text=text) } } } @Composable fun Touchable( // ..., children: @Composable () ->& Unit ) { // ... children() // ... } @Composable fun View( // ..., children: @Composable () ->& Unit ) { // ... children() // ... } The hierarchy/structure of Compose is achieved using curly-braces after the argument list of a function call. In Kotlin, these curly braces form a lambda, and are implicitly passed to the call that precedes it. This is called “Trailing Lambda Syntax”. This means that for a component to accept children, it should add a @Composable lambda parameter.
  18. [ // ... onPress, // ... text, // ... ]

    { type: Touchable, props: { onPress: onPress, children: [ { type: View, props: { children: [ { type: Text, props: { text: text } } ] } } ] } } In React, components return a “Virtual DOM” data structure. React uses this to understand UI structure, perform reconciliation and perform updates. 
 Compose is pretty different. Compose’s compiler uses the execution of other composable functions to understand the structure of the UI, and stores values along the way into an array-like data structure that is implicitly passed into each composable.
  19. @Composable fun Counter() { val count = state { 0

    } Button( text="Count: ${count.value}" onPress={ count.value += 1 } ) } function Counter() { const [count, setCount] = useState(0); return <Button text={`Count: ${count}`} onPress={() =>( setCount(count + 1)} /> } Using state (and other things) in Compose and React is also quite similar. React has hooks such as `useState`, and Compose has similar functions. These two examples are equivalent.
  20. @Composable fun Counter() { val (count, setCount) = state {

    0 } Button( text="Count: ${count}" onPress={ setCount(count + 1) } ) } function Counter() { const [count, setCount] = useState(0); return <Button text={`Count: ${count}`} onPress={() =>( setCount(count + 1)} /> } You can even use Kotlin’s restructuring syntax and it will look even more similar!
  21. Execution Model Let’s dive more into the execution model of

    Compose, which we are starting to see is quite different from React’s.
  22. Gap Buffer Compose uses a data structure commonly known as

    a “Gap Buffer”. Gap Buffers are traditionally used as efficient text buffers inside of text editors. In Compose, we use it slightly differently, and call this data structure a “Slot Table”, but the core principles and tradeoffs are the same.
  23. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY

    Gap Gap Buffer A Gap Buffer represents a flattened tree of objects. It is backed by a flat contiguous array in memory that is larger than the size of the tree. The space left over is referred to as the “gap”. When the gap runs out, we must resize the array. We always have a “current index”, which is where a current operation can be conducted. This is represented by the arrow on the left.
  24. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap

    Gap Buffer 0 When we want to insert something, we just insert it into the array and increment the index.
  25. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap

    Buffer 0 1 We continue doing this, and the gap gets smaller as we go.
  26. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer

    0 1 2
  27. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0

    1 2 3
  28. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0

    1 2 3 When we reset the index, we can go over the filled in tree again. If we don’t want to update an item, we can just increment the index.
  29. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0

    1 2 3 We can update a value at a specific index in constant time…
  30. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0

    1 2 3 If we want to insert a new item at that index, we want to move the gap to start at that index…
  31. EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0

    1 2 3 Then, again, inserts are cheap constant-time operations
  32. EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0 1

    3 4 2
  33. EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0 1 4

    5 2 3 For a Gap Buffer, all operations (insert, update, delete) are constant time, except for the moving of the gap. We only need to move the gap when the structure of the tree changes. The bet that we are making by using this data structure is that for UIs, the structure of the UI doesn’t actually change very often, but we update values (without changing structure) a lot.
  34. @Composable fun Counter() { val count = state { 0

    } Button( text="Count: ${count.value}" onPress={ count.value += 1 } ) } To understand how we use this data structure, we need to look at what the compose compiler actually does. This composable function represents the code a user would actually write. But the compiler will turn this code into something slightly different.
  35. @Composable fun Counter($composer: Composer, $key: Int) { val count =

    state { 0 } Button( text="Count: ${count.value}" onPress={ count.value += 1 } ) } The @Composable annotation implies that a contextual object, which we call the Composer, is passed into the function. This object is what uses the gap buffer.
  36. @Composable fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count

    = state { 0 } Button( text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } For this example, we can imagine the compiler inserting start/stop methods into the body of every composable. We will see later that we don’t actually do this, but this is a good way to build up the understanding.
  37. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } As you can see, the compiler takes care of passiong the $composer object through to the other composable functions that are called inside of the body.
  38. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } Additionally, we have these “key” integers that we are passing in to each call, and we can see that one was also passed into counter. A good mental model is to imagine that this key represents the a hash of the position in source code of the call itself.
  39. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Let’s walk through the execution of Counter and how it affects the Slot Table
  40. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Group($key) The start(…) call adds a “Group” to the slot table.
  41. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY EMPTY Group($key) Group(123) EMPTY Then we call state. The state call also adds a Group itself, with the key we passed it.
  42. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY EMPTY Group($key) Group(123) State(0) State will also cause a “State” object to get created which holds on to the initial value of 0 that we passed it.
  43. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY EMPTY Group($key) Group(123) State(0) Next we call Button.
  44. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY EMPTY Group($key) Group(123) State(0) Group(456) Just like the others, it also adds a Group.
  45. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY EMPTY Group($key) Group(123) State(0) Group(456) "Count: 0” It then stores each of its parameters into the slot table as well.
  46. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... EMPTY EMPTY Group($key) Group(123) State(0) Group(456) "Count: 0” { … }
  47. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... Group($key) Group(123) State(0) Group(456) "Count: 0” { … } Button(…) And then the implementation of Button could do a bunch of other things. It’s not shown here, so we’re just going to represent it here with this squiggly line.
  48. fun Counter($composer: Composer, $key: Int) { $composer.start($key) val count =

    state($composer, 123) { 0 } Button($composer, 456, text="Count: ${count.value}" onPress={ count.value += 1 } ) $composer.end() } ... ... Group($key) Group(123) State(0) Group(456) "Count: 0” { … } Button(…) And then we are done.
  49. ... ... Group($key) Group(123) State(0) Group(456) "Count: 0” { …

    } Button(…) state(…) Counter(…) Button(…) We can see that the composer essentially has an array that represents the depth first traversal of the entire hierarchy. You might be wondering what these Groups and source positions are. These are needed to handle insertions, deletions, and moves properly when we re-execute the function. Completely unnecessary in most cases. Most components don’t have conditional logic or control flow, and when they don’t, we don’t need these groups…
  50. ... ... State(0) "Count: 0” { … } Button(…) state(…)

    Counter(…) Button(…) Which means we can get rid of them. This ends up reducing the size of the Slot Table considerably. But when there *is* conditional logic, we *do* need to have these groups…
  51. @Composable fun App() { val result = getData() if (result

    ==* null) { Loading(...) } else { Header(result) Body(result) } } So to examine how that works, let’s walk through a component that *does* have control flow. Here we have an App component that loads some data, and depending on the result, shows a loading component or a header and a body.
  52. fun App($composer: Composer) { val result = getData() if (result

    ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } Desugaring the code to see what the compiler outputs, we see that now we don’t need a key to be passed in to App, just a composer. Instead, we put start/end calls with source position keys around each basic block in the function
  53. ... fun App($composer: Composer) { val result = getData() if

    (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = null EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY EMPTY Desugaring the code to see what the compiler outputs, we see that now we don’t need a key to be passed in to App, just a composer. Instead, we put start/end calls with source position keys around each basic block in the function
  54. ... fun App($composer: Composer) { val result = getData() if

    (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = null EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY Group(123) The first time the function is executed, result is null, so we go down the if branch. To do so, we add a Group(123).
  55. ... fun App($composer: Composer) { val result = getData() if

    (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = null EMPTY EMPTY EMPTY EMPTY ... EMPTY Group(123) Loading(…)
  56. ... fun App($composer: Composer) { val result = getData() if

    (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = null EMPTY EMPTY EMPTY EMPTY ... Group(123) Loading(…) ...
  57. ... Group(123) ... Loading(…) fun App($composer: Composer) { val result

    = getData() if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } EMPTY EMPTY EMPTY Gap EMPTY ... App(…)
  58. ... Group(123) ... Loading(…) fun App($composer: Composer) { val result

    = getData() if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... The second time we execute the function, result is not null.
  59. ... Group(123) ... Loading(…) fun App($composer: Composer) { val result

    = getData() if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... Once we get here, we end up comparing “456” with “123” and see that they aren’t equal. This tells the runtime that we need to replace the current group.
  60. ... Group(123) ... Loading(…) fun App($composer: Composer) { val result

    = getData() if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... To do so, we move the gap to the current position…
  61. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY …and remove the current group.
  62. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY Group(456) Now we continue executing like we would have, inserting the correct Group(456)
  63. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY EMPTY EMPTY ... Group(456) Header(…)
  64. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY ... Group(456) Header(…) Body(…)
  65. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY ... Group(456) Header(…) Body(…)
  66. ... ... fun App($composer: Composer) { val result = getData()

    if (result ==* null) { $composer.start(123) Loading(...) $composer.end() } else { $composer.start(456) Header(result) Body(result) $composer.end() } } // result = FeedItem(...) EMPTY EMPTY ... Group(456) Header(…) Body(…) App(…) As we can see the effective overhead of the if statement is a single slot in the Slot Table. This allows us to have arbitrary control-flow in our code while enabling memoization of arbitrary code at the position of execution in the hierarchy.
  67. Positional Memoization This concept of memoization against a gap buffer

    with control flow creating groups based on source- position is something we have decided to call “Positional Memoization”. The entire compose runtime is essentially built up from this concept.
  68. @Composable fun App(items: List<String>, query: String) { val results =

    items.filter { it.matches(query) } // ... } Positional Memoization “Memoization” is basically a fancy word for “caching the result of a function based on the inputs of that function”.
 
 Consider the following App component. Here we are doing some computational work inside of the function’s body. We are filtering a list of items based on a string query.
  69. @Composable fun App(items: List<String>, query: String) { val results =

    memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ... EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY EMPTY Positional Memoization What we can do is surround this computation in a call to a `memo` function. The result of the filter call can be considered a pure function of its inputs: items and query.
  70. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY @Composable

    fun App(items: List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } EMPTY Positional Memoization
  71. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY @Composable

    fun App(items: List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] Positional Memoization Memo will add all of the inputs to to the slot table
  72. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY @Composable fun

    App(items: List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" Positional Memoization
  73. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization Since this is the first time we are executing the function, we have to run the calculation. Before returning it, memo will store the result in the slot table alongside the inputs.
  74. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization
  75. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization The next time App is executed, the composer has the data from the previous execution stored.
  76. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization This means we can compare the inputs passed in with the previous values and see if they have changed at all.
  77. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization
  78. ... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items:

    List<String>, query: String) { val results = memo(items, query) { 
 items.filter { it.matches(query) }
 } // ... } ["a", "b", "c"] "b" ["b"] Positional Memoization Assuming they haven’t changed, memo can avoid running the calculation and instead just return the previous results that were stored in the slot table.
  79. fun <T> memo(vararg inputs: Any?, fn: () ->& T): T

    Positional Memoization This memo function has a shape like this. An arbitrary number of inputs, and a calculation function.
  80. @Composable fun App() { val x = memo { Math.random()

    } // ... } Positional Memoization One of the interesting things that you can do is memoize a calculation that isn’t pure. Here we are memoizing a call to math.random, but now the random result that we get will actually be persisted across subsequent calls to App. The semantics of memo has changed in this case.
  81. class State<T>(var value: T) fun <T> state(initial: () ->& T)

    = memo { State(initial()) } Positional Memoization Normally an under-specified or impure memoization is just…. wrong. But here, it actually has meaning. The core realization here is this:
 
 An under-specified positional memoization is equivalent to persistence. Persistence gives rise to state.
 
 Thus, our state function is just a special case of memo.
  82. class State<T>(var value: T) @Composable fun <T> state(initial: () ->&

    T) = memo { State(initial()) } Positional Memoization To “safely” affect the slot table, the state and memo functions need to be @Composable
  83. @Composable fun App(items: List<FeedItem>) { for (item in items) {

    val selected = state { false } FeedItem( item = item, selected = selected.value ) } } Positional Memoization This essentially allows us to do something that React can’t do: we can have “hooks” inside of control flow! The mechanism that allows us to do this is the same one that allows for components to be inside of control flow
  84. Component 
 ≈
 Composable Function As we have seem, a

    React Component is roughly equivalent to a composable function in Compose.
  85. Hook 
 ≈
 Composable Function But the interesting thing is

    that React’s hooks are *also* equivalent to Compose’s composable functions.
 
 Interestingly, Compose’s execution model is actually much closer to React’s execution model for Hooks than it is to React’s execution model for components.
  86. @Composable fun Google( number: Int ) { Address( number=number, street="Amphitheatre

    Pkwy", city="Mountain View", state="CA" zip="94043" } @Composable fun Address( number: Int, street: String, city: String, state: String, zip: String ) { Text("$number $street") Text(city) Text(", ") Text(state) Text(" ") Text(zip) } ... ... "Mountain View" "Mountain View" ", " "CA" "CA" " " "94043" ... Let’s imagine we have an Address and Google component like shown here. We see that since the parameters get stored in the Slot Table, there is some redundancy when we pass props through to other components. Let’s see if we can remove this redundancy.
  87. fun Google( $composer: Composer, number: Int ) { Address( $composer,

    number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" ) } Here we have the desugared version of the Google component.
  88. fun Google( $composer: Composer, $static: Int, number: Int ) {

    Address( $composer, 0b01111 or ($static and 0b1), number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" ) } We can pass an additional bit field in to each composable function denoting whether or not the compiler can determine at the call site that the parameters will never change. If they never change, we don’t need to store them. In this case, we know that the last 4 parameters to address are all static, and the first one, number, is static if my parent told me that it was static
  89. fun Address( $composer: Composer, number: Int, street: String, city: String,

    state: String, zip: String ) { Text($composer,"$number $street") Text($composer, city) Text($composer, ", ") Text($composer, state) Text($composer, " ") Text($composer, zip) } Likewise, we can pass that information through from Address into Text
  90. fun Address( $composer: Composer, $static: Int, number: Int, street: String,

    city: String, state: String, zip: String ) { Text($composer, 0b0, "$number $street") Text($composer, ($static and 0b100) shr 2, city) Text($composer, 0b1, ", ") Text($composer, ($static and 0b1000) shr 3, state) Text($composer, 0b1, " ") Text($composer, ($static and 0b10000) shr 4, zip) } The bitwise logic isn’t important here. Compilers are good at this. Humans aren’t.
  91. ... ... @Composable fun Google( number: Int ) { Address(

    number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" } @Composable fun Address( number: Int, street: String, city: String, state: String, zip: String ) { Text("$number $street") Text(city) Text(", ") Text(state) Text(" ") Text(zip) } "Mountain View" "Mountain View" ", " "CA" "CA" " " "94043" ... Redundant! So then we can know at runtime whether or not we need to store and compare the data. This means we can get rid of it!
  92. ... ... @Composable fun Google( number: Int ) { Address(

    number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" } @Composable fun Address( number: Int, street: String, city: String, state: String, zip: String ) { Text("$number $street") Text(city) Text(", ") Text(state) Text(" ") Text(zip) } "Mountain View" ", " "CA" " " "94043" ... Static! Additionally, the parameters we are left with are the ones that were statically defined. It turns out we can get rid of these too since we know at compile time that they will never change!
  93. ... @Composable fun Google( number: Int ) { Address( number=number,

    street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" } @Composable fun Address( number: Int, street: String, city: String, state: String, zip: String ) { Text("$number $street") Text(city) Text(", ") Text(state) Text(" ") Text(zip) } ... 1600 "1600 Amphitheatre Pkwy" So basically, the number and string template end up being the only things we need to actually store in the composer in this case.
  94. fun Google( $composer: Composer, number: Int ) { Address( $composer,

    number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" ) } Furthermore, we can see that if the number parameter is the same across executions, the entire sub-hierarchy will be the same.
  95. fun Google( $composer: Composer, number: Int ) { if (number

    ==* $composer.next()) { Address( $composer, number=number, street="Amphitheatre Pkwy", city="Mountain View", state="CA" zip="94043" ) } else { $composer.skip() } } shouldComponentUpdate? We can actually generate code that will check to see if the number has changed, and if it hasn’t, just skip the execution of Address entirely. This is equivalent to shouldComponentUpdate, but the compiler can codegen only where it determines it will be beneficial and correct.
  96. React Native + Compose? Having React target composables directly may

    require some fundamental rethinking and changes to React and/or React Native. BUT, the target itself matches the declarative programming model much more closely, so there might be some benefits.
 
 I believe some more fundamental interop to the Slot Table directly might be possible. I don’t know exactly what it would look like, but hopefully this talk helps give people a starting point.
  97. Landscape is evolving The mobile UI development landscape is changing.

    Quickly. I encourage people to embrace it. Be curious. Investigate what we are doing that’s different.
 
 Google and Apple are literally investing engineer centuries into these platforms. React is a source of inspiration, but we also have a clean slate and 6+ years of hindsight that may cause us to make fundamentally different decisions in certain areas.
 
 Somehow, at the same time, everything is both the same and different.
  98. Thank You @intelligibabble