LELAND RICHARDSON: Hi. My name is Leland Richardson,
and I'm a software engineer on the Android Toolkit team. Today, I'm going to talk to
you about Jetpack Compose. So, if you haven't
heard about it, Jetpack Compose is our new
next generation UI toolkit. It uses a declarative
component-based paradigm for building your UIs
easily and quickly. It's written entirely
in Kotlin and embraces the style and ergonomics
of the Kotlin language. It allows for interop with
the existing view system, and it's built entirely
in user space, unbundled from the underlying
platform, which means you can take advantage
of feature improvements and bug fixes on your own time. And I'm very excited to say
that as of this morning, Compose is now
officially in alpha, ready for you to
try out yourself. So, Compose promotes
a programming model that's quite a bit different
from the existing model of building UI on Android. And while this has been met
with a ton of excitement, it can also be a
bit scary to know such a big shift is coming. So the goal of this
talk is to help people get familiar with
this new programming model. And to do that,
we're going to build a chat application, JetChat,
completely from scratch. The design we're using
for this is actually from one of our
official Compose sample apps we published this morning,
which you can find here. So, our designer hands us
this beautiful new mockup of this chat app. And the first thing
I'm going to do is break down this
mock into pieces I think of as modular components
or chunks of the design. From a high level view, I
think of the entire screen as one large component. And below that, the screen can
be broken into the top app bar the bottom area where you
compose a new message, and the center, where the center
holds the entire conversation. Thinking a bit smaller, we
might break these components into smaller pieces. So the conversation can be
broken up in the messages, and each message might have
a user photo component. And we can go
further and further, even having different
button types. In the case of Compose,
all of these components are represented as
composable functions that we refer to as Composables. So, for this talk, I'm
going to take a somewhat bottom-up approach and start
by building the chat message Composable. Here I'll talk about
some useful ways of thinking about how to compose
your app's UI out of Composable functions. And then I'll move on to
creating the bottom area to compose new
messages, where we'll get to understand a
little bit about how Compose manages state. And then last, will look at
tying all of these pieces together at the
top level and talk a bit about app architecture. So let's start off talking
about composition with this chat message Composable. The very first thing
I'm going to do is just write an empty
function annotated @Composable. We'll call it JetChat. With Android Studio, I can
add a preview annotation to Composable functions
in order to open up a live preview of just
that Composable inside IDE. You can see that here
on the right side. To start, I'll take some
of the text from the design and pass them into
this text Composable that Compose provides. By doing this, the
strings get rendered to the preview on the right. Now, the execution of
Composable functions ends up describing the actual
hierarchy of the elements that get rendered on screen. So calling these three text
Composables inside of JetChat results in a tree
with three text nodes. This is kind of what
we mean when we say that Compose is declarative. The body of the
function can be thought of as a description of what
the resulting UI looks like. But by default, there's
no layoff policy here. So they're all stacked
on top of one another. And we can add some structure
by using the row and column Composables to define how
these elements should layout with respect to one another. But everything here is
still completely unstyled. So we can add various
parameters to these Composables to style it like our mock. The mock also has the user's
photo on the left side, and I decided to break this
out into a separate Composable on its own, UserPhoto. Our API will give us user
images in the form of a URL, so we create the initial
API of this Composable as accepting the
URL of an image. We then take that
image URL and pass it into the CoilImage
Composable, which is a Composable that uses the
popular Kotlin image loading library Coil. And we've started by giving
the image a size of 38 dp. The result is this 38-db
square image on the right. But we wanted a round image. So to do this, we can
use the clip modifier and pass in a circle shape. This tells Compose to clip the
drawing along the circle shape. The design also had some spacing
in between the image's edge and the border, so we can add
some padding with the padding modifier and add a 2-dp border
with a color and a shape. So you can see here that all
of the styling that we just supplied was through this
modifier parameter here. The modifier parameter
is a parameter that all of UI-emitting
Composables will have. You can think of modifiers
as a chain of declarations or wrappings to apply
to a given element. In this case, there's border,
padding, clip, and size. And the order of these matter. So here, we applied the
border and then the padding. And in the preview, you
can see that the padding is inside the border. So for modifiers, the order
that you read it in code is the same order that it
ends up with on screen. In this example, we've
started this modifier chain by using this capital-M
Modifier object, which represents an empty
modifier that we started this chain with. If we want to allow the
consumers of the user photo Composable to add additional
modifiers to decorate it or position it,
we can allow that by adding a modifier
parameter to this function with the default value
of the empty modifier. You'll find that a lot of the
Composables provided by Compose follow this convention. And if you want to make your
Composables highly reusable, this is a good
convention to follow. So now, we have a pretty
reasonable user photo Composable. But our design actually had
a different border color for each image. And so to handle
this, we're going to pull out the border
color into a local variable, ring color, and set
it to a random color. In this case, we can
imagine random color might be implemented
something like this. So, this kind of works. We ended up getting a random
color for every user photo, but there's a problem. Composable functions might
be called again in order to update the state of the UI. Composable functions are
just like normal functions, but with a few
important differences. And this is one of them. The Compose runtime might
re-execute the Composable function after they've been
called the first time in order to update the current
state of the UI. We call this process
recomposition. So, if we go back to the
user photo implementation, we see that this
could be a problem. Every time the user photo
Composable re-executed, a new random color
will get generated. So, there is another aspect
of the Composable function that can help us here-- that Composable
functions have a memory. And what I mean by that is
that a Composable function has access to what happened the
last time it was called. And there's this primitive
function, remember, which helps you tap into this. And so, going back to our
user photo Composable, we're going to do the same
thing that we did before. We call randomColor,
except we're going to wrap it with
a call to remember. And so in this case,
nothing has changed here in terms of the type system. We're still initializing the
ring color variable as a color, but by wrapping it
in remember, we're saying to remember the first
result of this function. And then instead of re-executing
every time, just return us the remembered value. So, we can think of
the remembered value as being part of the
tree we're generating. A color is stored
for every user photo Composable while on the tree. So in a sense, it's
part of the tree, even though it's not
part of the rendered UI. OK. So, stepping back to our
chat message example, we're looking pretty good. Let's take a look at our
design to see how we compare. Here, we realized that
there's something we forgot. The design has messages with
clickable links and code blocks, but we're only
handling basic texts. So going back to
our Composable, we can take the text Composable
that we were using before and refactor this into
our own custom text component, ChatText. So, starting out ChatText
does nothing but call texts like it did before. But Compose actually
provides two overloads of the text Composable, one
that accepts a standard string and another that accepts
an annotated string. This is a compose
to find data type for handling more complex
text functionality. So, we can build a function
that takes in a string with some markup in it-- whether it's
HTML or Markdown or something custom-- and we can build
an annotated string from that using the
annotated string DSL that Compose provides. So now, in our
ChatText function, instead of passing
in a string to text, we can convert the string
into an annotated string and pass that into text. We see now that the
links and code blocks are showing up on the right. But we have a problem again. The parseAnnotated call
might be pretty expensive. So we want to make
sure that it doesn't run more than it needs to. But ChatText is a
Composable function and can get re-composed. And so again, we can use
the remember function. In this case, we're going to use
the first parameter remember. The remember function
will remember the result of a calculation, as long as
none of the provided input parameters have changed
from the values passed in during the
previous composition. So in a way, you can kind
of think of these parameters as the keys to a local cache. Going back to our
ChatText example, we can wrap the
parseAnnotate call with remember and pass in text
as a single input parameter. This means that we
only pay the cost of parsing the string if the
value of the text has changed, which is exactly what we wanted. Going back to our higher
level chat message Composable, our public API looks
something like this. At the moment, this
is all of the data we need to show a message
like this one on the right. However, looking
at our design, we see that there is some
more complicated variations of this component. For instance,
there can sometimes be multiple messages,
each in their own bubble. So instead of a string
like text parameter, we might accept a
list of strings. But then we see that
the content of a message isn't always a string. Sometimes we have these
stickers that a user can sent. So we then change
it to be a list of either text or stickers. And then we can
also have images, so we add a class for that. And then we have a feature
where you can like an image, so we add the likeCounts. And we need to handle the user
liking and unliking a message, so we add an
onLikeClick callback. And we might want
to handle the user clicking on the image
itself, and also long clicking, and clicking
on the user's photo. And so we might also want
to add read receipts. So add a callback for when
this message is on screen, and then we want to handle a
way for a user to edit messages and share a message. And so now, we see that this
relatively straightforward message component has actually
become pretty complex, and it's going to have a lot
of logic in its implementation. So at this point, we
might say this Composable is trying to do too much. I've seen some people new to
Compose a little bit stuck at this stage and not really
know how to structure the code to be any more maintainable. And there's a very
important concept here, and that's that
Composable functions can be composed together. This can mean a few
different things, but let's take a look at what
we can do in this example. The chat message Composable,
as it's defined now, needs to handle all of
the different variations of a message. And as a first step, if we
look at all the parameters that have to do with the
content of the message, we can see that this is
where a lot of the complexity comes from. And we can actually replace
all of these parameters with a single Composable
content lambda. This is really important. So, this allows us to make the
ChatMessage Composable simpler and also more flexible
at the same time. With this parameter, the
consumer of ChatMessage can essentially put
arbitrary content inside the body
of a chat message. Similarly, we can make
a ChatBubble component that just produces the
styling of the chat bubbles without having any knowledge
of what kind of things might go inside it. This allows us to
organize our hierarchy in a more understandable tree
of higher level components. And if you find
yourself frustrated at the number of
parameters you're needing to thread through
a given Composable, remember that you can use a
Composable lambda like this to let the consumers
of your Composable pass an arbitrary content. OK. So, now we're going to
move on to implementing the send message bar
and talk a little bit about state in Compose. To implement this,
we're going to need some sort of a text input. Compose provides a full
implementation of the materials design components, so the
first thing we might do is reach for one of
Materials TextFields. Both the filled text field
and the outline text field have quite different designs
from what we actually need for this chat app. So, your first thought might
be to try using one of these, but to tweak it until it
looks like what we want. But I'm going to urge you
to try not to do that. In addition to the
material design components, Compose provides a lot
of useful core primitives for building up user interfaces. All of the material
design components are built on top of
these primitives. And so in this case, Compose
provides a raw text field Composable that we can
use to get what we want. The TextField Composable has
two required parameters-- value, which is a string, and
onValueChange, which is a lambda that accepts a string. So to give it a try, we're going
to pass a string literal hello for the value of the text field. So this renders what we
want, but it doesn't really behave like we'd expect. When we go and type
into this text field, the value of the text field
won't actually change. And that's because the value
isn't an initial value. It's actually the value of
the TextField Composable. So, really what
we're saying here is that we want to bind
the value of the text field to a string constant. And another way to
think about this is that in a declarative
UI system, the code itself describes the UI. But we need to describe
what the UI looks like at any point in time,
not just the initial time. So, to have the text field
represent a dynamic value we need to introduce
a bit of state. And to do so, we would
do something like this. Var text by remember
mutableStateOf. This line has introduced a
few new concepts at once, so let's try and break it down. mutableStateOf is a function
that takes in a value of type T and returns a mutable state
instance of that type, initialized to that value. MutableState to type with a
writable value property of type T. And the key thing here is that
when the value property is written to, it will schedule a
recomposition of any Composable function that's
subscribed to it. And a Composable function get
subscribed to state instance any time the value property
is read during its execution. And so, going back
to our example, we can see that remember
mutableStateOf will return a mutable state of string. But we see this "by" keyword
here to the left if it. And this might not be
familiar to a lot of you. This "by" keyword is part of
the Kotlin language feature called property delegates. When you have a local
variable in a function, there are really only two
things you can do with it. You can get the value, or if
it's a var like text is here, you can assign it
or set the value. With property
delegates, Kotlin allows you to give different meaning
to the getting and setting of a local variable by
defining these get value and set value
operator functions. So now, if we want to
de-sugar this example, we could take away
the "by" keyword. And when we do this, the
type of the text variable goes from string to
mutable state of string. So, anywhere we get
the value of text before, we now need to replace
that with a call to get value. And similarly, we would replace
all assignments of the text variable with set value calls. And with state,
the implementation of get value and set value calls
are just the getter and setter of the value property. So this is equivalent to just
using the value property. And so, we see that using
state with the "by" keyword is equivalent to just
treating the state object's value as the value itself. So now, going back
to our example, we have a working
TextField, which will respond to
the keyboard input and update the value of
the text based on it. It's still completely
unstyled though, and it's missing a few features. So here we've added some
style to the text field, and also added this
clickable expand icon on the right side
of the text field. Additionally the design-- we
have wanted this text field have some placeholder text
when the value is empty. But the lower
level TextField API doesn't have any notion
of a placeholder. So we ended up
implementing one ourselves without too much effort. We do this by actually
conditionally calling a text Composable only if
the value of text is empty. So, this is a good
demonstration of how you can build dynamic UIs by
writing natural logic that depends on state. OK. So, now we're going
to move on and talk about how we can tie
in some of the things that we've built together
into something resembling an actual application. And along the way, we'll
talk about some architecture considerations we might
have when doing so. So, here's a
high-level hierarchy of the components that
make up our chat screen. What we want to do is find
the minimal representation of UI state that we can
implement our screen with. First up is our text field. The user needs to be able
to type up a new message, so we hold that
in the text state. Next, we have the input mode. The user can have
different modes of creating message, like
text, emoji, or an image. And the current mode
is reflected in the UI below the text field. Now, we need to think about
where the state needs to live. To do this, we first need to
think about where it's actually going to be used. The current text value
of the text field obviously needs to be
used by the text field, but we also want to
enable or disable the Send button based
on whether or not the value is empty or not. Additionally, when the user
clicks the Send button, we'll want to reset the
text value and input mode. So as a result, we need
to move or lift this state up the tree a little bit. As a rule of thumb,
we want to put state in the lowest part of
the tree where it's still accessible to all of
the things that need it, but no lower than that. And this is related
to the fact that we want to have a single source
of truth for all states. We never want to
have to synchronize two states that are meant
to represent the same thing. So the result is that we end
up with both the text and mode state in the
SendMessageBar Composable. And this allows for this state--
or some projection of it-- to be passed to the
things below it. So our SendMessageBar
Composable might end up looking something like this. We've got our text state, which
we use in the ChatTextField Composable. And then the Send button is
enabled based on whether or not the text is empty or not. And it gets cleared whenever
the Send button is clicked. We also have the
mode state, which gets passed into the
input options Composable and gets reset if the user
types into the text field. And then, we're also passing
in this ChatViewModel into the Composable. And whenever the user
clicks on the Send button, we end up calling
the send method. And so, this is a
good time to talk about separation of concerns. At this point, you can see
that some Composable functions can end up with a
lot of logic in them. And it's reasonable
to ask whether or not this is adhering to the spirit
of separation of concerns. Separation of
concerns is sometimes defined instead in terms
of coupling and cohesion. And I find this a
little bit easier to understand and identify. So, abstractly,
when we write code, we think of our application
in terms of modules. And we might think of our
module as a set of units. And we can think of dependencies
between these modules as coupling. One way to think of it is if
I make a change somewhere, how many other
changes am I going to have to make as a result? And in general, we want
to reduce or minimize the amount of coupling that
we have in our application. Sometimes coupling can
actually be implicit. There's some aspect of
a dependency somewhere that we're relying
on, but indirectly. And this means that when
we change something, something somewhere else
might break as a result. And sometimes this
can be even worse if we don't have the
tooling to identify when this would be the case. On the other hand,
we have cohesion. Cohesion is how units inside of
a given module belong to one, or how they're related
to one another. Cohesion is generally
seen as a good thing, so we want to maximize cohesion. Separation of concerns
can be thought of as grouping as much related
code together as possible and drawing the
lines of separation so that coupling is minimized. So, framing this in terms of
something more familiar to you, let's imagine we
have a layout XML file and a corresponding
activity file. In this case, the activity
is inflating the layout, handling some user interaction,
and things like that. And so it turns out
there's a fair amount of coupling between the two. The activity needs to know
some things about the layout. This can manifest
in a lot of ways, but one prime example of this is
something like Find view by id. And then sometimes it's quite
a bit more subtle than that. And we might be relying on
some of the exact structure of the layout implicitly. And as your application
grows, these dependencies grow, as well. And following best
practices, we'd want to pull as
much UI logic out of the activity as possible. And we might instead handle
this type of data binding inside of a view model instead. This doesn't really get rid
of any of this coupling. It just shifts it over
to the view model. At the end of the day,
the responsibilities of a view model are inherently
related to the layout, and so it must have
some knowledge of it. In other words, they're
tightly coupled. One of the fundamental
issues here is that UI-related code is
written in one language, and the layouts themselves are
defined in another language. And this creates a forced
line of separation. And because they're forced
to be in different languages, this leads to implicit coupling. So, the general
idea here is if we decided to define the
structure of our UI in the same language-- Kotlin-- then these
dependencies at least can become much more explicit. So if we do this, we now
have the full power of Kotlin to draw lines of separation
where they might make the most sense to us. This means that some of
the highly-coupled logic can be refactored and
moved, and the result is our code can has less
coupling and more cohesion. So, let's take a look at how
this might work in real code. Here is a bind function. You might see something like
this in typical Android code today. Here, we're subscribing
to a LiveData of messages, and we have a call to observe. So, for observe,
you need to pass in a lifecycle owner
and a lambda that runs wherever the data changes. This lambda is where you'd
end up executing some code to update the state of your UI. The situation in Compose is
similar, but in a lot of ways simpler. Here, we're leveraging this
observeAsState Composable call. And this is an extension
method on LiveData. This method essentially converts
the live data of some type into a state object
of the same type. So, this function
is for live data, but Compose provides
equivalent functions for libraries like Flow and
Rx, and rolling your own for your own data solution
would be easy to do. Going back to the
example, we see that we're leveraging
property delegates again with the "by" keyword. And the type of messages
here is list of messages. This line is equivalent to
LiveData's observe method. But because Composable
functions have a lifecycle and the
ability to re-compose, the Composable
function itself can act as both the lifecycle
owner and the function to execute whenever
the data changes. So we don't need to
specify either of them. This means we're free
to just use the messages value naturally inside of
the chat screen Composable. In this case, the live data
is passed in to the Composable as a parameter. Another way to do
this would be to have the view model that holds
the messages data itself and pass that in. Even better, since the
Composable functions imply a scope, you can get
the view model instance using the viewModel function
provided by Compose, instead of passing
it in as a parameter. This leverages a viewModel
provider under the hood and will provide you a
properly scoped viewModel for these higher-level
Composables without needing to pass
it in as a parameter. So now, we have a
viewModel and our data in our top-level
screen Composable. Let's try and actually build
the screen from this data. Since messages is just
a list of messages, we can start with something
super simple and just compose chat messages in a loop. This works, but for
really long conversation, it might be too expensive to
compose all of the messages when only a few of them
need to be visible. So today, we would likely
use a recycler view for this. In Compose, we actually have
a similar concept, LazyColumn. Just like it was
with a call for each, this allows us to define
a Composable lambda for the content of each item. It really is just this simple. But our chat screen is more
than just a list of messages. We have a top bar and a bottom
bar and more around the Chrome of the conversation. Compose also provides
a Scaffold Composable, which is a useful way to
set up the common structure of a top-level screen. Here, I just pass in
a Composable lambda to define the content for my
top app bar, my bottom app bar, and my content. And that's it. It's really exciting for
me to see UI development Android working like this. So much boilerplate
that just vanishes. So, when building
apps with Compose, I want to encourage people
to view Composable functions themselves not as part
of the Compose toolkit, but as an addition to
the Kotlin language. Composable functions are
like normal Kotlin functions, but with some
additional capabilities that really transform how
you build abstractions on top of them. Composable functions can
be re-invoked and have a corresponding lifecycle. This also means that
they have a memory. They represent something
that has been called before and can be called again. And you have the tools to
peek into that history. So in this talk,
we went into depth about how a chat app like
JetChat might come together. JetChat is an official
Compose sample app that was released this morning. And there are four more sample
apps for you to look through. Looking through samples
can be a great way to understand this new
programming model better, so you should definitely
check them out. In addition to the samples,
there are several new code labs for you to check out, and the
API reference docs are always a good way to learn more. Also, we encourage you
to give us feedback. You can follow
this feedback link to file bugs or feature requests
in our official tracker. And you can also join
the Compose channel on the Kotlin Links
Slack community and provide feedback there. I really can't emphasize enough
how excited I am about Compose. I'm incredibly lucky to
be a part of this project, and now that it's an alpha, I
can't wait to see how y'all use it. I really believe it
has the potential to revolutionize UI
development on Android. Thanks for watching.