Google I/O 2022: Jetpack Compose Best Practices

Jetpack compose is a modern UI toolkit for Android and was released stable last year by Google. Compose uses a declarative component-based paradigm which enables developers to build UIs easier and faster while embracing the style and ergonomics of the Kotlin language. When using the Android view system, developers need to create the layout xmls first and then separately declare UI components used in the activity in along with having to bind them before using them. Compared to using jetpack compose, declarations of UI components are done in one place, with reduced boilerplates and thus it makes building Android UI faster and easier.

It’s been a year since the Jetpack Compose 1.0 has been released and a lot of companies have started adopting it. This year, Google has released version 1.1 stable and unveiled 1.2 beta during Google IO 2022.

State of Jetpack Compose

Jetpack compose is stable and ready for production but Google still continues to bring new features to the library on every new release, some features are graduated to stable from experimental whilst new features or APIs are introduced as experimental.

Jetpack Compose 1.1 Stable

Last February, the Android team released Compose 1.1 which brings a number of improvements:

  • Image vector caching: a caching mechanism is added to the painterResource API which allows it to cache all image vectors and invalidates them if a configuration change occurs.
  • Enforced accessibility requirements on touch targets: layout spaces on material components are expanded to comply with Material UI accessibility guidelines. For example, when a material component is set to a smaller size than the minimum size required by the guideline, then that minimum size will be applied instead. This should align Compose Material behaviour with Material Design Component counterpart and provide consistent activities when using the compose and view system together.
  • Stable animation and vector-related APIs: some animation-related APIs are graduated to stable such as EnterTransition, ExitTransition and some AnimatedVisibility APIs. Vector-related APIs that are stable on this release are: VectorProperty, VectorConfig, and RenderVectorGroup.

In addition, new experimental APIs are also introduced. For example: the ability to use rememberSaveable with AnimatedContent,  the ability to request parents to scroll to bring an item into view with the new BringIntoView API and LazyColumn or LazyRow can now be animated.

Jetpack Compose 1.2 Beta

The Jetpack compose 1.2 beta was announced during Google IO 2022 in May, introducing another round of improvements:

  • New window size classes: to provide better support for multiple devices with different form factors and resizability, new window size classes are introduced in WindowManager API. The new window size classes are introduced as well as a part of the compose material 3 library.
  • Improvement to font padding: the Android team finally added customizable includeFontPadding to compose to match its view system, making migration much easier from the latter.
  • Downloadable fonts: new APIs are added to provide access and usage of Google Fonts with easy setup and multiple apps can now share the same font through a provider.
  • Lazy layout improvements: some grid APIs are graduated from experimental (LazyVerticalGrid & LazyHorizontalGrid) and a new experimental API is introduced to enable further customization on lazy layouts.
  • Text magnifier: compose text now supports text magnifier, a feature which is supported currently by the traditional text view.
  • CoordinatorLayout interoperability: a new experimental API is introduced to make sure scrolling behaviours are interoperable when embedding a scrolling composable to a CoordinatorLayout from the view system.
  • New window insets: WindowInsets class is officially added to Compose, which features were previously supported by the insets library in Accompanist.

IDE Tooling Support on Compose

The Android team has made improvements as well to IDE tooling to support the development of applications using compose. Currently, new tooling support and improvements for compose are added throughout Android Studio Dolphin (Beta) and Electric Eel (Canary) versions.

  • Recomposition count feature:  a new feature within the layout inspector in Android Studio was added to view recomposition counts. Recomposition count is a very important feature to easily understand whether our UI is implemented correctly, for example, letting us know if it’s recomposing too many times. Available since the Dolphin version.
  • Compose animation coordination: compose animations can now be coordinated through Animation Preview in Android Studio. This feature is also available since the Dolphin version.
  • Compose live edit: an opt-in feature in Android Studio that enables code changes to composables in Android Studio to be reflected immediately in the Compose Preview and emulator or any physical device.
  • Compose multi preview: with the new multiple preview annotation class, it is now possible to preview multiple devices, fonts, and themes at the same time on Android Studio without repeating the same definitions for every single composable. Both compose live edit and multi preview are available from Electric Eel.

Besides the features mentioned above, which are all related to compose, IDE updates include other features and improvements as well such as: the new revamped logcat v2, gradle managed devices, integrated app quality insights with Firebase, resizeable emulators, wear OS emulator improvements and device mirroring. Read more about these new features here.

Tips & Tricks when using Jetpack Compose

Jetpack compose was designed to provide a performant UI for immediate use and developers should be able to write code efficiently. Since compose changes how the developers write UI at the fundamental level compared to the view system, there are some considerations that a developer should take when writing an app to avoid some common gotchas and maximize UI performance.

1. Test performance in Release Mode with R8 enabled

When an application is running in debug mode, it runs slower because the Android runtime turns off optimizations to improve the debugging experience. If you notice any performance issues on your application, always test first to see if the issue exists in release mode. Chances are, your app may not have had any performance issues to begin with.

2. Composable functions will be called on every recomposition

It is important to remember that all code inside composable functions will be re-executed during recomposition, and thus, pay special attention when there is specific code that can be optimized. We can use remember{…} to only run expensive operations or allocations once, for example sorting, and ensure that it’s running only when it’s needed. 

Fig 1. Sort operation is executed on every recomposition
Fig 2. Sort operation will only be executed when there is a change in contacts data or comparator when using remember

Perhaps, an even better approach is to move the expensive operations or allocations out of compose and completely into the view model or a data source instead and change the compose state only when it’s needed.

3. Define a key on LazyList items

Define a unique key for your items in a LazyList to help the LazyList know what has changed specifically in a list. Without providing a key, compose will use the position of that item as the default key. This can be very bad for performance because, when an item’s position changes in the list, then every other item after it will also be recomposed. When we provide a unique key for each item in the list, compose will know which item is moved and thus they would only recompose that item.

Fig 3. Adding a unique key to LazyColumn items will improve performance

4. Use derivedStateOf to buffer the rate of change when necessary

Use derivedStateOf to buffer the rate of change and avoid unnecessary recomposition. Suppose we only need to care when a specific state changes in the UI and want to avoid too much recomposition, for example when we only need to check whether the list has been scrolled past a specific index we can use derivedStateOf to only recompose when the condition actually changes the first time.

Fig 4. We are only interested when a specific index change happens to a certain condition but it may cause unnecessary recomposition due to state change

In the example shown above, it should only be recomposed when the list is scrolled down and again when the list is scrolled back up to that specific index change condition. 

Fig. 5 derivedStateOf helps buffer changes to reduce recomposition

derivedStateOf is really useful in cases where a state is converted into a boolean for a condition check and should be used if it could help improve performance.

5. Defer reading state until we need it 

Compose has three primary phases when rendering UIs

  1. Composition: the phase where composable functions are executed; it creates or updates the content of the application and defines what the next two phases are going to do.
  2. Layout: the phase where content defined by the previous phase is measured and placed in the space where it would be displayed on the screen. It also takes into account all the modifiers and calls to other composable functions such as text, buttons and columns during measurement and placement.
  3. Draw: the phase where actual graphic instructions are issued to draw the content onto the canvas of the application such as: drawing lines, rectangles, images, text, and applying colours to the locations determined by the previous phase.

These three phrases are repeated in every frame when the data they read changes. If the data does not change, then one or more of the phases can be skipped. It is important to defer a read whenever possible in order to reduce the number of functions to be re-executed and can even allow us to skip the composition and layout phase entirely. One example is when we only need to update the background colour while leaving the rest of the data unchanged. We can use drawBehind from Modifier to trigger a function that is called during the draw phase, skipping composition and layout when only the background colour is changed.

Fig. 6  Use drawBehind for states that may be of interest only for the draw phase

While reading state in functions, like the drawBehind example, is useful to remember that it applies to all composable function instances as well. It, not only allows for phases to be skipped, but it can also be used to reduce the amount of code that needs to be re-executed.

Fig. 7 Only the call to Text will be re-executed. Calls to ContactCard and Card are skipped because they do not read the contact’s name.

6. Never do backwards write

Backwards write is a term when we write data to a value that has already been read in composition. Compose assumes that once a value has been read, it will not change until after composition is completed. Writing data to a value that has been read in composition violates this assumption and causes recomposition to occur more often or, in the worst case scenario, forever. Such a scenario may occur in the following example:

Fig. 8 The write to total in the loop causes the composition to always think that the Text  is out of date and it needs to be recomposed

In the above example, the recomposition of previous items will occur every time the total is updated and may run endlessly since the subsequent recomposition will trigger the same line of code as well. The recommended, and much better, way to calculate the total is to do it in the ViewModel and let compose take care of the rendering part only.

7. Use baseline profiles 

There is often a performance drop when starting up an application due to the effects of just-in-time compiling, however, the baseline profiles can help to improve it. Baseline profiles are a list of classes and methods included in an APK used by Android Runtime during the installation to pre-compile critical paths to machine code. The idea is to perform ahead-of-time methods of compilation listed in the profile so that those methods would execute faster during runtime. For example, if the profile contains methods used in the app launch or during frame rendering, the user will experience faster launch times or maybe reduced jank. Baseline profiles are usually created the first time a user interacts with the application.

Profiles are already added to popular Jetpack libraries (fragments, compose, etc.) and we should include the ProfileInstaller library to prepopulate ahead of time compilation traces to be used by Android Runtime. When we need to create our own baseline profiles, for example, to improve or speed up scrolling on a complex home screen structure or registration process, then we need to use the Macrobenchmark library.

Use provided tools and libraries to measure and debug your app’s performance

Don’t forget to use all available tools and libraries at your disposal to create performant compose apps such as: the android profiler, recomposition count, compose animation coordinator, live edit, multi preview, and the macrobenchmark library that was mentioned previously. Some of these tools are still available only in the beta or canary release of Android Studio and might have some bugs here and there, but still, they would help a lot during development.

Key Takeaways

Jetpack compose is a modern UI toolkit for Android and has reached version 1.1 stable and 1.2 beta currently. The Android team at Google continues to improve jetpack compose by adding new features and improvements, making it easier to adopt and debug. Some tips & tricks are discussed above as well as how to create a performant application using jetpack compose.


What’s New in Jetpack,
What’s new in Jetpack Compose,
What’s New in Android Studio Tools,
Google IO Talk: Common Performance Gotchas in Jetpack Compose,


More Articles

let's talk illustration

Loving what you're seeing so far?

It doesn’t have to be a project. Questions or love letters are fine. Drop us a line

Let's talk arrow right