Jetpack Compose: What you need to know, pt. 1
1/20/2021
Jetpack Compose
Jetpack Compose: What you need to know, pt. 1

Jetpack Compose is coming sometime this year. Although it is still under heavy development, given its significance, I think now is the right time to look at what it brings to the table.

This isn’t a technical tutorial or introduction to Compose (there are many of these floating around, but be careful, as many of them are already out of date), but rather a collection of more or less random points, notes, and findings. Let’s find out if the hype is justified!

Executive summary, but for developers

Compose is going to be one of the biggest changes Android development has ever seen.

Yes, perhaps even bigger than reactive programming, Kotlin, or coroutines. UI is a crucial part of any application and a UI toolkit built on the mindset from the 2010s instead of the 1990s is indeed a very welcome upgrade.

Also, because it relies on Kotlin-exclusive features, Compose is another nail into Java’s coffin on Android.

Making UIs is fun again!

This is Compose’s equivalent of RecyclerView with different item types:

LazyColumn {
    items(rows) { row ->
        when (row) {
            is Title -> TitleRow(row.title)
            is Item -> ItemRow(row.text)
        }
    }
}

Of course, everything isn’t that simple, but Compose really excels at its main goal—creating sophisticated and highly reusable custom components and complex layouts in a simple, effective, and safe manner.

The mental model is radically different from what we are used to in Android.

Unidirectional UI toolkits were the rage with web folks some time ago, and now they’ve finally arrived on mobile platforms.

The good news is that because we are late to the party, the paradigm has matured, and perhaps Compose won’t repeat at least some of the mistakes that caught up with early implementations on other platforms. The bad news is that the paradigm requires a significant mindset shift (say on a scale of reactive programming)—but it’s for the better, I promise.

Compose has a huge multiplatform potential.

Compose comprises several artifacts, and only some of them are Android-specific. JetBrains already work on desktop port, and covering other platforms is certainly not impossible.

Building on a combination of existing platform-specific UI toolkits and Kotlin Multiplatform features such as expect/actual declarations, one can imagine a distant future where a single UI toolkit provides the holy grail of unified implementation, native performance, and platform-specific look’n’feel.

Creating UI

There are no XML layouts, no inflaters and no objects representing the UI components.

There are no setters to mutate the current UI state because there are no objects representing the UI views (@Composable function calls only look like constructor calls, but don’t let that fool you), which means there cannot even be any internal UI state (well, the last point isn’t entirely true, but we’ll get to that later). Then you must think about states and events traveling through your UI tree in various directions and whatnot.

If you’ve never experienced a unidirectional toolkit, it will feel alien, strange, and maybe even ineffective, but the benefits are worth it.

String, font, and drawable resources are staying.

Compose doesn’t want to get rid of these and works with them just fine. However, only bitmap and vector drawables make sense with Compose. Other types such as layer list drawables, state list drawables, or shape drawables are superseded by more elegant solutions.

Colors and dimensions should be defined entirely in Kotlin code if possible, but traditional resources still may be used if needed.

There are no resource qualifiers.

Compose has the power of Kotlin at its disposal. If you need to provide alternative values depending on the device configuration (or any other factor), simply add a condition to your composable function—it’s an explicit and unambiguous way to specify the value you want.

And of course remember to keep your code DRY—if you find yourself repeating the same bit of logic in many places, refactor.

There are no themes and styles (sort of).

Compose contains basic components that expose a number of parameters to change their look and behavior. Because everything is written in Kotlin, these parameters are rich, and most importantly, type-safe.

If you need to style a bunch of components in the same way, you simply wrap the original composable function call with your own composable, setting the parameters you need to change (or exposing new ones), and use this in your code.

Simple, efficient (because there is virtually no penalty for nested calls), and without hidden surprises.

Compose comes with Material Design implementation out of the box.

Although there are no themes or styles as such, there is a way to create and use application-wide themes.

Compose comes with Material Design implementation. Just wrap your root composable with MaterialTheme, customize colors, shapes, or typography to fit your brand, and you’re good to go. You can have different MaterialTheme wrappers for different parts of your UI, effectively replacing theme overlays from the legacy system.

Often this is all you’ll ever need, but if your design system is more sophisticated or simply won’t fit the predefined Material Design attributes, you can implement your own from scratch. However, this is quite difficult and requires advanced knowledge of Compose to get it right.

See this blog series for valuable insights on custom design systems in Compose and this post for a comparison of different theming approaches.

We can’t completely get rid of the legacy theme system (yet).

Compose theming reaches only the parts of the UI that are managed by Compose. We might still need to set a legacy theme for our activities (to change window’s background, status bar, and navigation bar colors, etc.), or to style View-based components that don't have Compose counterparts.

Don’t expect component or feature parity with legacy View-base components or Material Design specs any time soon.

It’s the old story all over again: Writing a new UI toolkit from scratch means that there is going to be a long period in which a number of components (or at least their features) won’t be officially available.

For example, Compose’s TextField doesn’t have the same features (and API) that TextInputLayout has, and both of these implementations aren’t 100 % aligned with the Material Design spec.

This situation may be slightly annoying, but at least with Compose, it’s significantly easier to write custom components yourself.

Finally, an animation system so simple that you’ll actually use it.

Animating many things is as simple as wrapping the respective value in a function call, and for more complex animations, Compose superbly leverages the power of Kotlin.

Done right, animations are a great way to enhance user experience. With Compose animation APIs, their implementation is at last effective and fun.

Internals

Composable functions are like a new language feature.

Technically, @Composable is an annotation, but you need to think about it more like a keyword (suspend is a good analogy, more on that later). This “soft keyword” radically changes generated code, and you need to have at least a basic idea of what goes on under the hood, otherwise, it’s very well possible to shoot yourself in the foot even with innocent-looking composable functions.

The knowledge of the internals is important for creating performant UIs.

Compose compiler does a lot in this regard (like positional memoization, and fine-grained recomposition), but there are situations when the developer has to provide optimization clues, or declare and then actually honor contracts that the compiler cannot infer on its own (such as marking data classes as truly immutable).

However, I expect the compiler to become smarter in the future, alleviating the need for many of these constructs.

States

Compose UIs are declarative, but not truly stateless.

UIs in Compose are declared by constructing deeply nested trees of composable functions where states flows down and events up. At the root of the tree, there is a comprehensive, “master” state coming from some external source (the best candidate for this task is the good old view model). When the state changes, parts of the UI affected by the change are re-rendered automatically.

In theory, we want the UI to be perfectly stateless. The root state should be completely externalized and should contain everything that must be set on the screen. That would mean not just obvious things like text field strings, checkbox states, and so on, but also, for example, all styling attributes for all the views, internal animation states including clock, current animated values, etc.

In practice, this would be too cumbersome (argument lists would grow unacceptably large and “interesting” arguments like user inputs would get mixed up with purely technical ones like animation states), so besides explicit state that is passed via composable function arguments, Compose has several other ways to provide data down the component tree.

Composable functions can have their own internal state.

Yes, function can have state encapsulated in it that survives between its invocations. This is a pragmatic decision that simplifies its signature and enables some optimizations, and is especially handy for animations and other things that don’t need to be changed and/or observed from outside.

Ambients are like service locators for data passed through the UI tree.

Ambient holds a kind of global variable defined in the tree node somewhere up in the hierarchy, statically accessible to nodes below it. If this rings an alarm bell in your head, you’re right—statically accessed global variables create invisible coupling and other problems.

However, this is a trade-off that is often worth it. Ambients are best suited for values that we need to set or change explicitly but don’t want to explicitly pass through the tree. Theme attributes and properties are a prime example of such things.

State management is now more important than ever.

So we have (at least) 3 ways to store and manipulate state in Compose, and they can even be combined along the way. The question of which method to use for which part of the state becomes essential. Sometimes, the answer can be difficult, and choosing the wrong one can lead to all kinds of messy problems.

Also, especially for larger screens, both the structure and the content of the state object is crucial.

Until next time

Well, that concludes part 1. In the second and final part of this series, we’ll look at the ecosystem, performance, stability, and even the magic that makes Compose possible. Take care and stay tuned!

Tags

#android; #jetpack; #compose; #ui

Author

Jiří Hutárek

Versions

Kotlin 1.4.21
Jetpack Compose 1.0.0-alpha09