Diving into Jetpack Compose
Hands on with the new UI framework for Android apps, Jetpack Compose.
Hands on with the new UI framework for Android apps
Being involved in a lot of different mobile projects the past few years I’ve used a variety of technologies like Android, ReactNative and Flutter. Switching back from ReactNative to vanilla Android gave me mixed feelings. Getting back to Kotlin was great, but I really missed the React UI framework. The small reusable components that you build your UI with are great and give a great amount of flexibility and development speed.
Back in vanilla Android, I need to worry about keeping the View hierarchy as flat as possible. Making it hard to really commit to a component based approach. This makes copy-paste driven development tempting, resulting in a messier and less maintainable codebase. Ultimately holding us back to experiment with the UI to improve our user experience.
Jetpack Compose to the rescue
So after watching the What’s new in Android session from Google I/O 2019, I immediately started to play with Compose and read more about it. Compose is a reactive UI toolkit entirely developed in Kotlin. Compose looks quite similar to existing UI frameworks like React, Litho or Flutter.
The current Android UI framework has been around since 2008 and over time it has gotten more complex with quite some legacy. Jetpack Compose aims to start fresh with the modern component philosophy in mind. The framework is written with these main goals in mind:
- Unbundled from platform releases: This allows bugs to be fixed and released quickly because it’s independent of new Android releases.
- Fewer technology stack flowcharts: The framework does not force you to use a View or Fragment when creating your UI. Everything is a component and may be composed freely together.
- Clarify state ownership and event handling: One of the most important and complex things to get right in larger applications is handling data flow and state in your UI. Compose makes it clear who is in charge of state and how events should be handled, similar to how React handles this.
- Writing less code: Writing a UI in Android usually requires a LOT of code, especially when creating more complex layouts with a RecyclerView for example. Compose aims to dramatically simplify the way you build your UI.
This makes it easier to create isolated and reusable components, making it trivial to compose a new screen with existing elements. Helping you as a developer to focus on creating a great user experience instead of trying to keep your View Hierarchy in check and taming Views and Fragments.
A simple Compose App: Hello World
Let’s take a look at the code needed for a simple “Hello World” app with Jetpack Compose.
In the onCreate
method we set the content of our Compose app by calling setContent
. This is a method that initializes the composable widget tree and wraps it in a FrameLayout
.
To make things work we need to wrap our app in a CraneWrapper
and MaterialTheme
. CraneWrapper
is responsible for setting up providers for the Context
, FocusManager
and TextInputService
. The MaterialTheme
is needed to provide colors, styles and fonts to your widgets. With this in place we can add a Text
component that will render our text on screen in a certain style.
Introducing some state
Managing data flow and state can be a challenging task. To illustrate how easy this is with Compose, let’s create a simple counter app.
Jetpack Compose adopts ideas from other modern UI frameworks like Flutter and React to deal with state. There is a unidirectional and reactive dataflow that makes your widget update or “recompose”.
In the demo above we introduce “Add” and “Subtract” buttons, along with a label that shows the current tap count. As you can see in the demo below, by updating the “amount” state, widgets intelligently recompose themselves when the state changes.
The amount
state is initialized by +state { 0 }
. Trying to figure out what kind of sorcery this is, I checked out the source code. This is my take on it, though I’m still not sure I understand 😇.
state { ... }
creates Effect<State<T>>
. The Effect
class is an opaque class that holds a block of executable code that is meant to be executed positionally in the context of a composition. The State
class wraps a single value as a Model
, basically making that value observable. The +
operator is a temporary operator that resolves the State
from the Effect
.
Custom State Models
Instead of using +state {}
to create a model for a single value we can also create our custom model using @Model
annotation. We can further improve our counter app by splitting it up into smaller widgets and passing a model to the different widgets which update and display the state from that model.
By using the @Model
annotation, the Compose Compiler plugin makes all the variables in your model observable so they can be used to recompose your widgets. Let’s update our widget to use this CounterModel
:
The single widget the counter app consisted of is now split up into multiple smaller composable widgets. The CounterModel
is passed around to the various widgets, either to display state form that model or to mutate the state of the model by calling the add()
or subtract()
function.
No more views
It’s important to realize that Jetpack Compose widgets don’t use views or fragments under the hood, they are just functions that draw onto the canvas. The Compose Compiler plugin processes all functions with the @Composable
annotation and automatically updates the UI hierarchy.
For example the Divider
widget consists of a Padding
widget that contains a DrawFillRect
widget. Looking at the source code of this DrawFillRect
it’s clear that this draws a line directly onto the canvas. All other widgets are implemented in the same way.
Source code of DrawFillRect which is used internally by the Divider widget
Taking a look at the Layout Inspector while running one of Google’s sample apps clearly shows that no Views
or ViewGroups
are involved when running an Android app with Compose. The FrameLayout
containing the CraneWrapper
that we created in code is visible, from there the Compose UI hierarchy is drawn on screen.
Not using existing views also means Jetpack Compose can’t leverage the currently available views like android.widget.Button
and must build all widgets from the ground up. Flutter for example takes the same approach and shows that this is a massive job. This is one of the reasons it will take some time before Jetpack Compose will be ready to use in production.
Everything is a widget
Very similar to Flutter, everything is a widget in Compose. More complex widgets have been broken up into very elemental widgets with clear responsibilities. Therefore even padding, spacers, etcetera are widgets. For example, if you want to add some padding around your button you just wrap it in the padding widget:
Mixing code with UI
It’s really easy to mix Kotlin code with UI widgets. For example, if you want to show some conditional or repetitive UI. For example, you can easily render a list of names as shown below.
This is a really powerful feature, but you should be careful not to introduce too much logic in your UI layer.
Compatible with your existing Android app
Compose is engineered in such a way that you can add it to your existing application and gradually move some parts of your UI to the new framework. The above examples add Jetpack Compose UI to a single activity. It’s also possible to embed Compose widgets in a existing XML layout using the GenerateView
annotation:
@Composable
@GenerateView
fun Greeting(name: String) { /* … */ }
// In some existing layout.xml
<GreetingView app:name=”John” />
Conclusion
I’m thrilled about Compose because it addresses a growing Android development pain I’ve been feeling. It helps to be more agile, keeps the focus on building a great user experience, and clear responsibility also avoids bugs.
Compose has a long road ahead, I don’t expect it to be useable in production for at least a year, or two. Still I think this is the right moment to take a look at Jetpack Compose. The creators are actively looking for feedback and in this phase it is still possible to make changes. All feedback will help improve this new framework.
Check out my “Try Jetpack Compose today” post for instructions on how to get the pre-alpha of Compose up and running. The Google I/O talk on Declarative UI Patterns is also very interesting to watch.
I’m really look forward to using Compose in real Android apps! 🤘
Check out our other Google I/O blog posts on https://engineering.q42.nl/tag/google-io.