[MUSIC PLAYING] LELAND RICHARDSON: Hello, my
name is Leland Richardson. I'm a software engineer on
the Android UI Tool Kit team. More specifically, I work on the
Compose Runtime and the Compose Compiler. So earlier this year,
we open-sourced Compose at Google I/O. And
since then, we've been developing it
out in the open, as part of the Android
Open Source Project. As app developers,
the expectations around UI development
have really grown. Today, we can't
really build an app and meet the user's expectations
without having a polished user interface, animation, motion. All of these things
are things that didn't exist 10 years ago as
an expectation from the user. So Compose, we believe,
is a modern UI toolkit that really sets
app developers up for success in
this new landscape. So today, what I
want to talk about is, what problems specifically
does Compose solve? What were the reasons behind
some of our design decisions? And how can that help
you as an app developer? Additionally, I want to
talk about the mental model of Compose. I want to talk about
how you should think about code that you
write in Compose-- how you should shape your APIs. And then finally, I
want to talk about some of the implementation details
and how Compose actually works under the hood and explain
what's actually happening here. So let's get started. What problems does
Compose solve? And really, to frame this,
what I want to talk about today is separation of concerns. Separation of concerns is a
well-known software design principle. It's one of the
fundamental things that we learn as app developers. And really, it's an
age-old kind of thing. More than 40 years ago,
when separation of concerns was originally postulated,
it was actually framed in terms of
two other words-- coupling and cohesion. And so what I want
to do today is I want to talk about separation
of concerns in terms of this, because I think it's a
little bit more concrete and can help us understand
exactly what we're talking about. So abstractly,
when we write code, we think of our application
in terms of modules. And we might think of our module
in terms of multiple units. So our application has
several of these modules. And between them, we can
think of these dependencies as coupling. Basically, there's ways in
which parts of one module influence the other. And one way to
think of this is-- if I make a change to
some code somewhere, how many other
changes to other files am I going to have to make? And that's coupling. And in general,
what we want to do is we want to reduce
coupling as much as possible. And sometimes, coupling
is actually implicit. There's a dependency
that we're relying on or something that we're relying
on that's not actually very clear, but something
breaks because of a change that happens somewhere else. On the other hand,
we have cohesion. And cohesion is really about
how the units inside of a given module belong to one another. They're related to one another. And cohesion is generally
seen as a good thing. And so one way to think of this
is that separation and concerns is really all about
grouping as much related code together as possible
so that our code can be maintainable over
time and really scale as our application grows. So framing this in terms of
something more familiar to you, let's talk about a kind
of common situation. We have a view model. Here on the left. And then we also
have an XML layout. And the view model is really
providing data to this layout. We have a view that we need
to populate with something. And it turns out,
there's actually a lot of dependencies hidden here. There's a lot of
coupling between the view model and the layout. And one of the more familiar
ways that you can see this is through findViewByID. What we're doing is we're trying
to understand what the XML layout is actually
defining, finding specific elements into it,
and piping data through it. We might even, more
subtly, depend on things that happen in layout XML. We actually might rely
on a certain structure that was defined there. And so we have to keep
these things in sync as our application grows. And really, our
application can grow a lot. These layout XMLs
get very, very large. We have very large
complicated UIs. And they're dynamic too. So sometimes, one element
might leave the view hierarchy at runtime but not
statically, and this leads to null reference
exceptions and things like that. So one of the
fundamental things here is that we have a view
model defined in Kotlin. And then our layout
XML is defined in XML. And so because of this
difference in language, there's actually a
forced line of separation here, even though the view
model and the layout XML can sometimes be very,
very intimately related. In other words, they're
very tightly coupled. So what if we started to define
the layout, the structure of our UI, in the same language? What if we chose Kotlin? Now because we're in
the same language, some of these dependencies might
start to become more explicit. And even more, we can start
to refactor some code and move things over to where they
belong and actually reduce some of that coupling and
increase some of the cohesion. Now, some of you
might be thinking about what I'm saying here
and be a little bit skeptical. Am I saying that we should
mix Logic with our UI? Well, here's the thing. As framework authors, we
actually can't perfectly separate your concerns for you. This is something that
only, really, you can do. You have parts of your logic
that will not escape the UI. They are part of your UI. And we actually
can't prevent that. But what we can do is
provide you with tools to make that separation easier. And so I'm here today
to try and convince you that that tools is a
Composable function. And actually, this
might sound a little bit less controversial than it is. If you take away the Composable
part, it's just a function. And a function is something
that you've been using, probably for a long time, to
separate concerns elsewhere in your code. And the skills that
you have acquired to do that type of
refactoring and writing reliable, maintainable,
clean code-- those same skills apply
to Composable functions. So today, I want to
talk about the anatomy of a Composable
function a little bit and try to help
you understand how to think about these things. So here's an example of
a Composable function. And it receives
data as parameters. We have this app data
class that comes in. And we want to think
of the parameters that come into a Composable function,
really, as a mutable data. It's data that-- the
Composable function really shouldn't be changing. We should just be treating
it as a transform function of that data. Now because of that,
we can use any code that we want to, in Kotlin,
to take that data, create, drive data from it and then use
that and describe our hierarchy here in this function. And this means that we call
other Composable functions. And those invocations represent
the UI in our hierarchy. Also, we're able to use all of
the language level primitives that Kotlin already has in
order to do things dynamically. So we can if statements and
for loops for control flow and dealing with the
more complicated logic that our UI might have. And then finally, I
want to point out here that we're leveraging Kotlin's
trailing lambda syntax. So Body here is a
composable function that has a composable
lambda as a parameter. And that ends up implying some
sort of hierarchy or structure. And so Body is something that
wraps these set of items here. So you've probably heard us
say the word "declarative." "Declarative" is a buzzword. But it's an important one. And I want to describe
what we mean by that. And usually, when we
talk about declarative, we're talking about
it in contrast to imperative programming. So let's look at an example
to understand this more. What if we had a UI, like a mail
or a chat application, where we have an Unread Messages icon. And so if there are no messages,
we render a blank envelope. If there are some messages,
we put some paper in it. And maybe we're a
little bit cutesy. And if there are over
100 or something, we show some fire
and stuff like that. So with an imperative
interface, we might write an update
count function something like this, where what we do
is we get in the new count. And we go through
and we figure out how we're supposed
to poke at this UI in order to make it
reflect the proper state. And actually, there's a
lot of corner cases here. And this logic isn't
easy, even though it's a relatively simple example. And so if you take this
logic and, instead, write it in a declarative
interface, you might end up with something like this. And so here what we're
doing is we're saying, OK, if the count's
over 99, show fire. If the count's
over 0, show paper. If the count's over 0, render
a badge with this count. And that is what I mean when we
talk about a declarative API. And if you want to think about
it-- so as a UI developer, the things you need
to think about-- one-- given this data,
what UI do I want to show? How do I respond to events
and make my UI interactive? And then here's
the critical thing. We no longer need to think about
how our UI changes over time. What happens is, when
we get in the data, we show what it
should look like. We show what the next state is. And then the framework controls
how to get from one state into the other. And so now we no longer
need to think about it. And that's the critical piece. So describe the UI based
on the provided parameters. And understand that the
Composable function, it's one function definition. But it describes all possible
states of your UI in one place. It's locally defined. And that leads into what
we mean by composition. So with a name like Compose
and an annotation called Composable, it seems
like composition is an important concept here. So I want to talk
more about that. And really, one of the things
that we're talking about here is that our model of
composition differs from the model of composition
that Inheritance follows. And they're both
types of compositions. So what we're talking about
here is a different type. So let's go through an
example for this as well. Let's say we have a view. And we want to create an input. And so we use View
as our base class. And then we want
a ValidatedInput, and so we create a subclass
of input to do that. And we want a DateInput. And we want to use the
validation of a date, and so we subclass
ValidatedInput here as well. But then we run into a problem. When we want to create a date
range input, we have two dates. So we want to validate
two dates separately. So maybe we want to
subclass DateInput, but there are two of them. So we can't really do that. And so we run into this
limitation around inheritance that we have to have one
parent that we inherit from. In Compose, the
problem is simpler. So when we create
our ValidatedInput, we just call Input in
the body of our function. And we can decorate it with
something for validation. Then when we create
a data input, we end up calling
ValidatedInput as well. And now, when we run into
the date range input, we no longer have a problem. It's just two calls. And so there is no
single parent that we compose onto in Compose's
composition model. And that solves this problem. Another type of
composition problem is what I would
call containment. So we want to have this fancy
box, which is a view that decorates other views. And we might have
some other views here, like Story and EditForm. And then we want to make a fancy
story and a fancy edit form. But what do we do? Do we inherit from FancyBox? Or do we inherit from Story? It's unclear because,
again, we need one parent for that inheritance chain. And so Compose handles
this really well. We have a Composable
lambda as children. And that allows us
to define something that wraps another thing. So now when we want
to create FancyStory, we just call Story inside of
the children of FancyBox-- same with FancyEditForm. And this is Compose's
composition model. Another thing that Compose
accomplishes really well is encapsulation. This is what you
should be thinking about when you make public
APIs of composable functions. And the public API
of a composable is really the set of
parameters that it receives. And those are given to it, so it
doesn't have control over them. They're just provided as data. On the other hand, a composable
can manage and create state. And then it passes that state
along with, potentially, some data that it received
down to other composable as parameters. Now, because it's
managing that state-- Adam talked about
this yesterday-- if you want to make a
change to that state, you can allow your
children composables to signal that change up
towards you via callbacks. And finally, I want to
talk about something called Recomposition. And this is basically our way
of saying that any Composable function has this
special ability to get re-invoked at any time. And so what this means is that,
if you have this very large Composable hierarchy-- what happens is, when parts
of your hierarchy change, you don't want to have to
reinvent the entire hierarchy. And so Composable
functions are sort of restartable in this way. And you can actually
leverage this to do some pretty powerful things. So here's a Bind
function that is maybe something you would see
today in Android development. So we have a live data that we
want to subscribe our view to. And so to do that, we end up
calling the Observe method with a lifecycle owner. And then we pass in this lambda. And that lambda is going to
get called every single time the live data updates. And when that
happens, we might want to go and update our views. With Compose, we can
actually kind of invert this relationship. So in Compose, we would have
a similar Messages Composable. And it would
receive a live data. And here, we call
Compose's Observe method. And Observe does
two things here. First, what it does is
it unwraps that live data and returns the current
value as its return value. And that means you can
use it in the surrounding body of the function. But it also does something else. It implicitly--
well, it subscribes that live data to the Composable
that it's being unwrapped in. And so that means that,
instead of providing a lambda, you just now know that this
Composable function will recompose every time
live data changes. Looking at a simpler
example of this, let's imagine that we have
a simple counter composable. And so here we introduce a piece
of state, which is our count. And State is as a
function in Compose that returns an instance
of this state class. And the state class is
annotated with @Model. And what @Model does
is it means that every property of that class-- now the reads and writes to
that property are observable. And so what Compose does
is, when you're executing your Composable function, if
you read one of these model instances, Compose
will automatically subscribe the surrounding
scope to writes to that model. So that means that this
example is self-contained. We have a counter that will
get re-composed every time the value of that
model is changed. OK, so we just talked
about a lot of capabilities that Composable functions have. Let's start talking about how
it's actually implemented. Before we do that, I'm
going to get some water. OK-- small disclaimer-- everything I'm about to say
is an implementation detail. And it's subject to change. In fact, it's very
likely to change. But it's fun to talk about. But the important thing
that I want to say is that understanding this is
not required to use Compose. What I'm trying to
do is to satisfy your intellectual
curiosity here. And also, if you really
want to dive into this and understand what's happening,
this is a good primer. So we see this @Composable
annotation in a lot of slides. What is it actually doing? I want to make an
important point here, which is that Compose is
not an annotation processor. How Compose works is through
a Kotlin compiler plugin. And we work in the
tight checking phase and in the code generation
phase of Kotlin. So there's no annotation
processing happening. The annotation here is
actually more closely related to a language keyword. So I'm going to describe it
in terms of an analogy, which is the "suspend" keyword. Kotlin's suspend keyword
operates on function types. This means that you can have
a function declaration that's a suspend. We can have a lambda. We can have a type. Compose works in the same way. We can alter function types. And the important point
here is that, when you annotate a function
type with that composable, you're changing that type. So the same function type
without the annotation is not compatible with the
type with the annotation. It's a different type. Additionally, suspend
requires a calling context. This means that you can only
call Suspend functions inside of another Suspend function. Composable works
the exact same way. And this is because
there's a calling context object that we
need to thread through all of the invocations. And so I'm going to talk
about what that object is. What is this calling
context thing that we're passing around? And why do we need to do it? Well, the implementation
of this object actually has some
data structures in it that are very closely related
to an existing data structure called the "gap buffer." Most of you probably aren't
familiar with gap buffers. But if you work with text
editors, you might know them. They're commonly used there. So to describe what
a gap buffer is-- a gap buffer really
implements a list. It's a collection interface. And it has a current
index or cursor. And the way we implement this
is with a flat array in memory. And so that flat array
is necessarily larger than the collection of
data that it represents. And so the space in that
array that's unused we refer to as the gap. Now, as we execute our
Composable hierarchy, we can appeal to
this data structure. And we can insert
things into it. And so you can think of the
cursor as your current point of execution in your hierarchy. And so as we go
through execution, we can insert items, insert
another one, insert items. So now, let's imagine that we're
done executing the hierarchy. At some point, we're
going to go and we're going to re-compose something. And so we're going to reset
the cursor to the top. And then we're going to go
through execution again. And at this point, we're
able to do a few things. We can look at the
data that's there. And we can do
nothing, if we decide. We can update the value. Or we can decide that the
structure of the UI is changed. And then we want
to make an insert. So this is the important thing. At this point, what we
do is we move the gap to the current position. And now, we're able to
make inserts at that point. So we keep going,
keep making inserts. Now, the important thing to
understand about this data structure is that all of the
operations that we just talked about-- get, move,
insert, delete-- all of those are
constant time operations, except for moving the gap. Moving the gap is
the expensive thing. So the reason we chose
this data structure is because we're making a bet. The bet is that UIs, on
average, don't actually change structure very much. When we have dynamic
UIs, they change in terms of the values that are there. But they don't actually
change in structure. And when they do, they
typically change in big chunks. And so doing this gap move at
that time is a good trade-off. OK-- so let's look
at this example. We have the counter here. And this is the code
that we would write. This is the example
from earlier. Well, let's see what
the compiler does. So when we see this Composable
annotation, what we do is we actually insert additional
parameters into this function. And so we pass in this
Composer object through. And that Composer object
is what kind of contains this gap buffer thing. You also might hear me
refer to it as a slot table. Just think of it
as the same thing. And so we also insert some calls
in the body of this Composable. So we're going to call
this composer.start method. And we're going to
pass it in this key. And I'm going to talk
about that in a second. Another thing that
we're doing is we're passing that
Composer object into all of the composable
invocations that are in the body of this function. So we're threading it through. And we have these keys here. So these are these
arbitrary looking integers. But the way to think
about this correctly is that this represents, like,
a hash of the source position that this call site represents. So this is sort of
unique to each call site. So when we go through the
execution of this composable, we go through and we call Start. And Start inserts a group
object into the slot table. We go through. We call State. State inserts its
own group object. And then the value that state
returns is a state instance. That also stores that
into the slot table. Then we move onto Button. Button is going to
store a group as well. And then it's going to store
each of its parameters. And Button might have this
arbitrary implementation. We don't really know. And it's going to also use the
slot table during that time. And when it's done, we're going
to then call composer.end. And so you can see here
that this data structure is holding all of these objects
from this whole composition. And it's sort of the entire
tree in execution order. It's like a depth first
traversal of the tree. Now, all of those group
objects that we just saw-- what are they there for? They're taking up a
lot of space, right? So actually, those
group objects are really important to manage the moves
and the inserts that might happen with the dynamic UI. But we're a compiler. So we actually know what
code looks like that changes the structure of your UI. So we can conditionally
insert those groups. And most of the time, we
find that we don't actually need them. So we don't actually have
to insert that many groups into the slot table. To show an example of
a case where we do, let's look at some
conditional logic here. So here's a Composable. It has this getData function
that returns some result and renders a loading composable
in one case and a header and a body in another case. So here, we see that we're
inserting separate keys for the first branch
of the if statement and the second branch. And when we go
through and execute it-- let's say, the first time
this runs, the result is null. And so then we go and we
run the loading screen. Now the second time
we run it, let's pretend that feed item
is the result here. So it's not a null. And at this point, we're going
to go into the second branch of the if statement. And so this is where the
interesting thing happens. At this point, we
call composer.start. And it has a group with key 456. And it sees that the group
in the slot table of 123 doesn't match. So now it knows that the UI
has changed in structure. So what we do is we move the gap
to the current cursor position. And then we extend
the gap across the UI that was there before. So we kind of get rid of it. And now we insert the new
UI, the header and the body. And so one way to look at
this is the overhead of the if statement, in this case,
was a single slot entry in the slot table. And it was this group. And by just inserting this
single group, what this does is this allows us to
have arbitrary control flow in our UI and allows
us to manage it and appeal to this cache-like
data structure, while we move through
the execution of the UI. And so this concept
is something that we call Positional Memoization. And Positional Memoization
is a new thing. But this is the concept
that Compose is built from, from the ground up. And I want to talk
about what this means. So memoization is kind
of a fancy sounding word. Normally, we have
global memoization. And what memoization
means is that we are caching the
result of a function based on the inputs
of that function. So an example of
positional memoization here might be-- we
have this computation that we're doing instead
of a Composable function. We're taking in some
string items and a query. And we're performing some sort
of a filter operation on it. We can wrap this calculation
in a call to memo. And we can pass in items
and query to that call. And so memo is
something that knows how to appeal to the slot table. And what it does is
it looks at items. And there's nothing there. This is the first
time we're running it. So all it does is just store it. And we look at query. We store that. And then we run the calculation. And we store the result.
And then we pass it back. So that was fine. But the second
time we execute it, that's when the
interesting thing happens. So when we execute
it again, memo goes and looks at the new
values that are being passed in and compares them
with the old values. And if neither of them
have changed, then we can skip the
calculation and just return the previous result. And so
that's positional memoization. But the interesting thing here
is that it was really cheap. We only had to store
one previous invocation. And this calculation could
happen all over your UI. And you're storing
it positionally. So it only stores it
for that location. And this is the signature
of the Memo function. Memo here can take
any number of inputs and then some sort of
calculation function. But there is an interesting
degenerate case here, which is when there
are zero inputs. One of the things we can
do is we can deliberately misuse this API. We can memoize an intentionally
impure calculation, like, say, math.random. And if you were doing this
with global memoization, this would make no sense. But with positional
memoization, it ends up taking a new semantic. So here we have math.random,
which is memoized. And we store in this value x. And for every time we use App
in our composable hierarchy, there will be a new math.random
value that's returned there. But every single time that
composable re-composes, it will be the same
math.random return value. So what this gives rise
to is a persistence. And that persistence ends
up giving rise to state. And this is what the State
function actually is. State is just a call to memo
around the State constructor. And so what that
means is that you'll get the same instance the state
across every invocation of it. And that's what we want. So let's move into
talking about the way that we store the parameters
to Composable functions. So here, we have a
Google Composable, which takes in a number. This is kind of a silly example. But we're calling an
address composable. And we're just rendering
an address here. So we're calling a few
text nodes underneath it. When we look at how
this data ends up getting stored in
the slot table, we end up seeing
some redundancies. So the Mountain
View in California that we added in the
address invocation ends up getting stored
again in the underlying text invocations. So it turns out
that we can actually get rid of this redundancy
by adding another parameter to Composable functions
at the compiler level. So here we have the
static parameter. And this is a bit field that
indicates whether or not a given parameter is known
by the runtime to not change. And if it's known to
not change, then there's no need for us to store it. So we can see here in
this Google example, we can pass a bit field that
says, none of these parameters are ever changing. And then in Address, we
can do the same thing and pass it into text. Now all of this bit wise
logic is hard to read. It's confusing. And there's no intention
of you understanding this. Compilers are good at this. Humans aren't. And this is exactly
the kind of thing you want a compiler doing. So let's go back to our
top level example here. And we can see this
redundant information that we no longer need to store. But additionally, there
are all these values that are constant here. These are static values. It turns out, we don't
need to store them either. And so this entire hierarchy
is actually purely determined by this one number parameter. And that ends up being the only
value that we need to store. But we can actually go further. So we can generate
code that understands that number is the only
thing that's going to change. And so what we can say is,
well, if number hasn't changed, don't bother doing
anything else. Just skip this invocation. And the composer
knows exactly how far to fast forward the
execution to resume exactly where it needs to. So the final concept
I want to talk about is explaining how this
recomposition happens that we talked about earlier. So going back to this
counter, the generated code that we would create
for this counter has a composer start and
end, like we've seen. And what I mentioned earlier
is that, whenever we execute Counter, the runtime
understands that when I call count that value, I'm
reading the property of an app model instance. And so at runtime, what happens
is whenever we call end, we optionally return a value. And then we can call an update
scope method on that value, basically, with a lambda
that tells the runtime, here's how to restart this
Composable if you need to. And so this is sort of
equivalent to that lambda that that live data would
be receiving otherwise. And the reason that this
question mark is here-- the reason that
this is nullable-- is because, if we don't
actually read any model objects during the
execution of Counter, then there's absolutely no reason
to teach the runtime how to update this, because we
know it never will update. So some closing thoughts-- I want to emphasize again
that this is still really, really early and, more
importantly, that this isn't production ready. And we really mean that. This is a really
big undertaking. And we're really still exploring
a lot of these options. And a lot of the things
I just talked about are pretty new
explorations on our side. And so I think it's
pretty interesting. But just know that a
lot of these things are still happening. And we think there are some
really, really powerful ways that we can really gain
some performance in a world where we have
Composable functions. But we're still exploring that. The other thing I
want to point out is that everything
we just talked about that's being
done by the compiler is required for correctness. And there's an
interesting oversight where code that uses
@Composable and things like that compiles just fine with
the old Kotlin compiler. There are some blogs
out on the internet that say, hey, you don't
need Android Studio for-- and all that. They're wrong. [LAUGHTER] So what I ask is that
you all be careful. This was an oversight
on our part, and we're exploring how to
make this less of a foot gun. But really, your
code is not correct unless you're using
this compiler, basically, a new version of
Android Studio 4 or higher. So just be very
careful with that. And then finally, I want to give
a shout out to the Kotlin Lang slack channel. There's a Compose channel
in that community. It's very active. A lot of folks from
the Compose team are on there, including myself. And if you do want
to get involved and learn more about these
things or even help out, that's a great place to start. Also, another shout
out-- we do a lot of UX studies around Compose
and APIs we're choosing. And we're really looking for
more volunteers to help us out. So upstairs at the
Compose sandbox, there's a way to sign
up if you're interested. And that's it. Thank you. [APPLAUSE] [MUSIC PLAYING]