Slide 1

Slide 1 text

REACT, MEET COMPOSE LELAND RICHARDSON @intelligibabble

Slide 2

Slide 2 text

What is Compose? Let’s first start by going over what Compose is at a high level before diving in.

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

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.

Slide 5

Slide 5 text

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!

Slide 6

Slide 6 text

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.

Slide 7

Slide 7 text

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.

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

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.

Slide 10

Slide 10 text

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!

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

Programming Model ==="" The programming model between React and Compose is actually quite similar.

Slide 14

Slide 14 text

Execution Model !==$$ The execution model, however, is very different.

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text


 
 
 
 @Composable fun Button( text: String, onPress: () ->& Unit ) { Touchable(onPress=onPress) { View { Text(text=text) } } } 
 
 
 
 function Button({ text, onPress }) { return ( ) } 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.

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

@Composable fun Counter() { val count = state { 0 } Button( text="Count: ${count.value}" onPress={ count.value += 1 } ) } function Counter() { const [count, setCount] = useState(0); return ( 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.

Slide 20

Slide 20 text

@Composable fun Counter() { val (count, setCount) = state { 0 } Button( text="Count: ${count}" onPress={ setCount(count + 1) } ) } function Counter() { const [count, setCount] = useState(0); return ( setCount(count + 1)} /> } You can even use Kotlin’s restructuring syntax and it will look even more similar!

Slide 21

Slide 21 text

Execution Model Let’s dive more into the execution model of Compose, which we are starting to see is quite different from React’s.

Slide 22

Slide 22 text

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.

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

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.

Slide 26

Slide 26 text

EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0 1 2

Slide 27

Slide 27 text

EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY Gap Gap Buffer 0 1 2 3

Slide 28

Slide 28 text

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.

Slide 29

Slide 29 text

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…

Slide 30

Slide 30 text

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…

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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.

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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.

Slide 38

Slide 38 text

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.

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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.

Slide 41

Slide 41 text

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.

Slide 42

Slide 42 text

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.

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

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.

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

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.

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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.

Slide 68

Slide 68 text

@Composable fun App(items: List, 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.

Slide 69

Slide 69 text

@Composable fun App(items: List, 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.

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

... EMPTY EMPTY EMPTY EMPTY ... EMPTY EMPTY EMPTY @Composable fun App(items: List, 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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items: List, 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.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items: List, 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.

Slide 76

Slide 76 text

... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items: List, 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.

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

... EMPTY EMPTY EMPTY EMPTY ... EMPTY @Composable fun App(items: List, 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.

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

class State(var value: T) fun 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.

Slide 82

Slide 82 text

class State(var value: T) @Composable fun state(initial: () ->& T) = memo { State(initial()) } Positional Memoization To “safely” affect the slot table, the state and memo functions need to be @Composable

Slide 83

Slide 83 text

@Composable fun App(items: List) { 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

Slide 84

Slide 84 text

Component 
 ≈
 Composable Function As we have seem, a React Component is roughly equivalent to a composable function in Compose.

Slide 85

Slide 85 text

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.

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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.

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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.

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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.

Slide 95

Slide 95 text

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.

Slide 96

Slide 96 text

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.

Slide 97

Slide 97 text

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.

Slide 98

Slide 98 text

Thank You @intelligibabble