Today, we’re here to talk about Jetpack Compose,
our modern, native UI toolkit for Android.
Compose is currently in beta, and version 1.0 is coming soon.
This means that it's time to learn and get ready to adopt it in your apps.
It’s intuitive, powerful,
and has been built from scratch to accelerate development
and let you write UI with much less code.
In other words, it’s faster and easier,
so you can focus on building features and spend less time creating UI.
But why did Android need a new UI toolkit?
The existing toolkit, based on Views, has been available for over 10 years.
In that time, technology has changed.
Devices are more powerful,
users have higher expectations of apps,
and UIs are much more dynamic and expressive.
And while you can still create beautiful apps with Views,
you told us you wanted a modern toolkit, based on modern architectures,
that capitalizes upon the power of Kotlin.
We also wanted to be able to iterate
and bring you features and improvements faster.
We started working on Jetpack Compose,
a declarative, unbundled toolkit over three years ago.
We released our first beta in February,
which means that the API surface is stable.
As we keep working towards our stable release,
now is the time to learn and get ready to adopt it,
to make building UI faster and easier.
So how does it make it faster and easier?
We've mentioned that Jetpack Compose is a declarative UI toolkit.
Let's break those two concepts apart and look at each in turn.
First, the declarative part.
Today apps aren't static data, they update, they're alive.
Traditionally, with Android Views you would declare your UI in XML,
but then to update your UI, if any data changed,
you need to mutate it.
You do this by looking up Views and setting properties on them.
So whenever your application state changed,
for example once a database or network call loads,
or after a user interaction,
you would have to update your UI with this new information
to keep everything in sync.
This can get complex, as each View holds some state
that you need to ensure you update.
This overhead of synchronizing state
between your model and your UI is a huge source of bugs.
It’s your responsibility as a developer to ensure that everything is updated.
As your app and UI get more complex, it's easy to get this wrong.
Declarative UIs like Compose take a different approach.
They transform state into UI.
The UI is immutable, there’s no way to update it after it’s been produced.
So if your app state changes, we re-execute to transform this new state
into a new representation.
This avoids the synchronization issues entirely.
You regenerate the entire UI.
Compose is actually smart enough
to skip any work for elements that haven’t changed
to make it more efficient,
but conceptually, it regenerates the entire UI for a given state.
Your code describes what the UI should look like for a given state,
not how to produce it.
So what does this look like? How do you transform state into UI?
Here’s a simple component that displays a list of strengths.
Notice that in Compose, a UI component is simply a function
with the composable annotation.
This makes it quick and easy to create UI components,
encouraging you to break your UI down into a library of reusable elements.
The function doesn’t return anything, but instead emits UI.
Here we’re using the Column and Text composable
from the Compose library,
which arrange things vertically and display simple text labels.
Now that we can use the full Kotlin syntax and control flow to produce our UI.
Here, we iterate over the messages,
but if you want to conditionally show an element,
showing a label when there are no messages,
then it’s as simple as using an "if" statement.
Composables can and should accept parameters.
That’s what we mean by transforming data into UI.
Composables take data as function parameters and emit UI.
In this way, the UI can't get out of sync.
For example forgetting to remove the no messages empty text.
If your state changes, if the list of messages changes,
then a new UI is generated by executing this function again.
We call this re-composing.
So how might the list of messages change?
Walking up the call stack,
our ViewModel exposes a LiveData of messages.
We can observe this,
and now any composable that reads the messages field
will be re-composed whenever there is new data.
You don’t need to set up observers yourself.
The Compose compiler tracks which composable read state
and automatically re-executes them when that state changes.
It’s smart enough to only re-execute composables
whose inputs have changed, and skip those that haven’t,
making it very efficient.
Each composable is immutable.
You can’t hold a reference to it and query it later or update its contents.
You need to pass any and all information to a composable
as parameters when you admit it.
But that doesn’t mean that a composable can’t be dynamic.
Let's dig into this by adding a checkbox to select all messages.
Initially, our checkbox is unchecked.
Unlike in the View world,
clicking this checkbox would not visually toggle it.
We’ve passed in a constant as its state after all.
If we want it to change, the code must reflect that.
Let’s introduce a local variable to control whether it should be checked.
Checkbox offers an onCheckChange event when it is clicked.
We can update our local state in this callback.
Doing so will cause the checkbox to be readmitted
as it reads the state.
It’s important to understand
that if we don’t change any state in this callback,
the checkbox won't update visually.
At first, it might seem unintuitive
that you need to write code to make a check box toggle when tapped,
but this is a key concept in declarative UI.
Elements are completely controlled by the parameters you pass to them.
This is what produces a single source of truth,
meaning that there’s no other state to keep in sync.
It's entirely up to you to decide how to react
to the user pressing the checkbox.
You might want to run some validation and only update the display if it’s valid.
Your code is completely in control,
rather than having to come back
and undo the change if the validation fails.
Earlier we mentioned that Compose will re-execute this composable function
when the input data changes.
However, there are variables we may want to keep
across re-executions or what we call recompositions.
Composable functions can remember a value from an earlier execution
by using the remember function.
This allows you to reuse a value
in order to avoid reallocating it or to hold on to state.
In this example, we implemented the event handling inline
but for a more realistic example,
we could instead pass in the state and update lambda
as parameters to the composable,
moving the logic up to wherever the source of truth is.
The key idea with declarative UI
is that you completely describe what your UI should look like
for a given state,
and the framework takes care of updating the UI when the state changes.
Compose can work with many application architectures,
but is an especially great fit with one that follows a unidirectional data flow,
where your ViewModel exposes a single stream of screen states.
This can be observed by your Compose UI,
and passed down to individual components as parameters.
Each of which only receives the state it needs
and therefore only needs to be updated if the data changes.
Producing a single stream of ViewState objects
helps centralize where state changes can be made,
making it easier to reason about the entire state of the screen
and reducing errors.
This pattern also makes your composable straightforward to test,
as they’re all completely controlled by their inputs.
So we looked at how the declarative paradigm
makes it easier to write UI.
Built on top of these principles, Compose offers a rich toolkit of UI components.
Jetpack Compose implements the Material Design components,
and theming system.
It offers the components and building blocks
that you need to assemble your applications,
such as Buttons, Cards, FABs, or AppBars.
All components follow material styling out of the box.
And it also implements Material Theming,
allowing you to customize all components systematically to your brand
by supplying your own colors, shapes and typography styles.
Compose also offers a new simple but powerful layout system.
It’s based on rows and columns,
which are roughly equivalent to horizontal and vertical linear layouts.
Unlike the view system however,
the Compose layout model prevents multiple measure passes,
making nested layout performant.
ConstraintLayout, with a new Compose-specific DSL,
allows you to express more complex layouts
but it’s also far simpler to create custom layouts.
Performing your own measurement and placement
to achieve just the right layout
is as easy as implementing a function.
One of the most exciting improvements is the new animation system.
It’s much, much simpler to use,
offering really powerful and simple ways to bring motion to your UIs.
And we’re also working on bringing MotionLayout to Compose.
Testing and accessibility are first-class citizens in Compose.
They’re both built on a semantics system, which creates a parallel tree to the UI,
that describes it, providing more information to accessibility services
or helping you to match UI elements to assert against them.
Compose offers a dedicated testing artifact
that has been built to maximize testability
and offer easy APIs for testing composables in isolation.
The test rule also exposes the clock,
which drives UI updates and offers APIs for controlling it.
This lets you have full control in your tests
even testing animation code.
Compose is entirely written in Kotlin
and takes advantage of the great language features
to build powerful, succinct, intuitive APIs.
Coroutines, for example, enable us to write much simpler async APIs,
such as describing gestures, animation, or scrolling.
This makes it easier to write code that combines async events,
like a gesture which hands off to an animation,
all with cancellation and clean up provided by structured concurrency.
Kotlin also brings a powerful ecosystem of tooling.
For example, making it trivial
to extract UI components into new functions, promoting reuse.
To really understand how these features come together
to make it easier to build UI, let’s walk through an example.
This is Owl, one of our official samples available on GitHub.
Let’s look at the topic screen to see some of these features in action.
Let's start by building an individual topic chip.
We’ll write a new composable function
that accepts a Topic model object as a parameter.
We can build our component with a row containing an image and a text.
This will show the data in our topic object.
For the background, we can surround the row with a Card composable.
We also want some spacing around the text.
To add this, we can decorate any composable
using its modifier parameter.
There are many different modifiers available
for customizing layout, drawing, interaction and more.
Here we’re using the padding modifier to add some space.
We want selected topics to have a check icon shown on top of the image.
We can achieve this by passing in a Boolean parameter.
We wrap the image in a box,
which allows us to stack items on top of each other
and conditionally add the icon based on the selected parameter.
Our design also calls for selected items to have a rounded corner.
We can achieve this by conditionally setting a corner radius
based on the selected parameter and passing that to the Card’s shape.
To animate this change, we can simply wrap this condition
with an animateAsState function.
This will automatically animate the corner size change.
So far, we've created a single topic item,
but on the screen we want to display a list of topics the user can choose.
Let’s create a new composable for this, which takes a list of topic objects.
Compose offers a lazy column composable
which lays elements out vertically, like a regular column,
but it only lays out enough elements to fill the viewport, like a RecyclerView.
This makes it very efficient
for displaying a window of a large data set.
We can pass the list of objects to an items block,
and how to render each individual item.
And that's it, no adapter or item layout files to manage.
However, our design calls for a horizontal staggered sequence of topics.
This is also easy to achieve in Compose by building a custom layout.
Building something like the grid shown is only a couple of lines of code.
One last thing I'd like to show you is theming.
Owl gets its yellow background from its theme.
Compose makes it easy to set up your themes
so that your app reacts to dark mode.
Let's unpack how that's done.
We can check whether the system is in Dark Theme,
and switch color schemes.
Because all theming is done at runtime,
Compose easily supports dynamic theming beyond our theme.
You can find the complete implementation of the examples we’ve covered today
in the Compose samples repo.
Hopefully that example illustrates how Jetpack Compose
makes it faster and easier to build UI.
And that's just scratching the surface.
Jetpack Compose is built to co-exist with the existing View system.
Compose can be adopted as gradually as you need,
from replacing a small element of your screen
to building a larger piece of UI, or an entire screen,
Compose helps you take the steps you need.
You can embed Compose within Views, as well as hosting Views within Compose.
This can be especially useful for displaying content
not yet built with Compose, such as Ads for MapView.
This can help you gradually migrate to Compose,
adopting it at your pace.
In addition to View interop, we provide integrations with other key libraries
so Compose can work with your existing application architecture,
no need to start from scratch.
We also provide wrappers for common image loading libraries
and adapters for converting Material or AppCompat XML themes into Compose.
There’s an entire separate talk on Compose and existing libraries,
so check that out for more details.
We know that the developer experience is not complete
without powerful tools and comprehensive documentation.
That’s why, besides updating some of the tools you’re familiar with,
like Layout Inspector, to support Compose, we’ve also built powerful new tools.
The Compose Preview, for instance,
introduces a new way of working between tools and code,
allowing you to define your previews together with your components,
iterating on them in isolation, and scrubbing through animations.
But this is just a sneak peek of the new Compose tools.
To learn more and see them in action,
check the "What’s new in Development Tools"
and "What’s new in Design Tools" talks.
We built Compose with developers in mind, in collaboration with the community.
Since open-sourcing in 2019,
your feedback, feature requests and contributions
helped shape it enormously.
We’ve also worked closely with a number of partners
who have started exploring Compose in their apps.
Early adopters have told us they are writing less code,
reducing the size of their code base,
that Compose is intuitive and more fun to work with,
freeing them up to work on features and quality.
And that it’s powerful, offering new ways of doing things,
at times surprising us with creative use of the APIs.
We’ll be sharing these partners’ stories soon,
so look out for those.
Compose is a modern toolkit,
built to work across different screen sizes,
from small formats like wearables,
to large format like tablets and Chromebooks.
And we're working to improve your development experience
across all these surfaces.
To help you get ready to adopt Jetpack Compose
when it launches 1.0 shortly,
we’ve published an extensive set of learning materials.
So join us on the Slack channel
and keep giving us feedback as you learn Compose
and get ready to adopt it in your apps.
Happy composing.