Why We Adopted Jetpack Compose

Illustrated with Feature Examples

Introduction

I recently wrote (and updated) a blog post about our product feature that was completely rewritten with Jetpack Compose.

Jetpack Compose: Backdrop Component

After the grinding but also exciting development phase, I have come to a good checkpoint to reflect on what we have been doing and why. I can summarize into 3 reasons why we adopted Compose and think it is the future for Android UI development. I also would like to incorporate the theory into our feature work and compare the resulting code of Compose vs. the old View system.

Thinking in Compose

Many Android engineers started the journey of learning Compose from the article Thinking in Compose from the official developer site. Indeed, the introduction captured two essential reasons why Google’s Android team designed the new UI toolkit from the platform perspective. Here are some excerpt from Thinking in Compose:

Historically, an Android view hierarchy has been representable as a tree of UI widgets. As the state of the app changes because of things like user interactions, the UI hierarchy needs to be updated to display the current data. The most common way of updating the UI is to walk the tree using functions like findViewById()and change nodes by calling methods like button.setText(String), container.addChild(View)or img.setImageBitmap(Bitmap). These methods change the internal state of the widget.

Over the last several years, the entire industry has started shifting to a declarative UI model, which greatly simplifies the engineering associated with building and updating user interfaces. The technique works by conceptually regenerating the entire screen from scratch, then applying only the necessary changes. This approach avoids the complexity of manually updating a stateful view hierarchy. Compose is a declarative UI framework.

I will expand and illustrate the 2 reasons stated above. Besides them, as an app developer, I also want to call out the 3rd reason why I really enjoy using Compose, from the API users’ perspective.

Reason #1 Declarative DSL vs. Imperative API

Declarative is a buzzword and it may mean different things in different context. But it would make more sense when we compare declarative with imperative programming styles in examples. Then we can see their relative difference.

In old Android View system, we write UI via inflating view widgets then mutate their internal states by calling getters & setters. Let us called that imperative paradigm because app developer needs to manually control internal states of view widgets.

However, in Jetpack Compose, app developers don’t have direct access to widget objects any more. The underlying UI hierarchy is hidden behind the Compose declarative API. The reason they are called declarative is because the way we call those function APIs reads like describing what we want UI looks and behaves like.

So imperative coding is more about how and declarative is more about what. Because the Compose library exposes only DSL API and encapsulate a lot of underlying heavy-lifting work.

Let us use a feature example to compare the results: we want to build a list of rooms vertically, as shown below:

In View systemwe would typically define recycler_view.xml, row_item.xml, RecyclerViewAdapter and binding & config adapter.

https://medium.com/media/c8b0e222b33d6f877264eb22cd615b8c/href https://medium.com/media/9bcdad0081a8b11550ba4b9ee9a3faf2/hrefhttps://medium.com/media/dfeeee08c359b4176f72efmediumb673f2f2f2arefhttps://medium.com/media/dfeeee08c359b4176f72efmed.comb152d012f2f3

There are actually multiple reasons that View system would require more coding in general: The forced separation of xml and logic code(will mention in reason #3) would require manual view inflation and view binding(recycler view & row item view) in logic code. Also, app developers have to manually manage data binding and configure Layout. As result, everything adds up.

With Composewriting code in declarative API would result in:

https://medium.com/media/15fafddbdb2246cca3353711f2bd4e6d/href

We describe UI like:

  • Make a Column with items (as data) and lazyListState (as widget state)
  • Compose each item in RoomItem

The amount of code speaks for itself for the efficiency.

Reason #2 True State-Driven Architectures

In View system, application should hold app state in each screen, view widgets also hold their internal states. But in Jetpack Compose, app developers won’t have references to the view objects and won’t manually mutate internal states of them. Instead, we can only build composable functions like this:

@composable
fun FunctionName(inputState: T) { …}

Note that we annotate the function with @composable and the function has no return type. By doing that, we are telling Compose-compiler that this function is to convert the input state into a node that is registered in the composition tree. Composition tree is the in-memory representation of UI views that Compose-runtime manages. Composable functions would emi scheduled changes to UI tree nodes. The mental model can be diagrammed something like this:

Composable Function

The magic was done by Compose-compiler which would add an implicit parameter, Composer, to the composable function to perform a lot of underlying work such as tracking, caching and optimization. It is in similar fashion as adding the implicit parameter, Continuationto suspend functions in coroutines.

The nature of the architecture eliminated the need for app developers to manage the internal states of the view widgets. Instead, only the state input dictates how UI is rendered. The new architecture yields the following benefits:

  • By eliminating managing internal state of view widgets, it truly delivered the unidirectional data flow: InputState => UI
  • App developers only need to describe what the current state should be and no longer need to worry about the previous state that UI was in and how to transition from one state to another. The library would take care of that.
  • This allows the vast majority of the performance optimization happen in Compose library level, therefore, it alleviates such daunting task from app developers.

Let us use another example to illustrate how the new architecture plays out in real world. The feature is that assuming we already had a Row of rooms in the Revealed state of scaffold and Column of rooms in the Concealed state, we would like the same scrolled position to be transferred from Row to Column and vice versa. As shown below. IOW, the scrolled position is synchronized between Row & Column.

Scroll Position Synchronization

During the transition, we’d like both Row & Column to render on screen with different gradually increased / decreased transparency:

With View systema typical code skeleton would look like this:

https://medium.com/media/d4c9dc53fd332069ba12b48d26522b58/href

  • Inflate horizontalRecyclerView
  • Inflate verticalRecyclerView
  • During transition, manually fetch scroll-position & pass from one orientation of list to the other

Because of the design of View systemnot only we need to inflate and hold on to the recycler views, but also have to manually manage the internal states of the recycler view – the scrolled positions.

In Composewe would code something like this:

https://medium.com/media/06ec7760b1d98a9f01c7924bda0a6a06/href

Besides the code brevity(mentioned in reason #1), The state-drive architecture played an important role in the difference of the resulting code. The input state for composable function has 2 things:

  • items: List
  • listState: LazyListState

We passed them into LazyRow & LazyColumn . Notice that we even pass the exact same instance of lazyListState into both LazyRow & LazyColumn . We are effectively telling Compose-runtime just to render Row & Column based on the same scrolling state. That is how simply we can achieve the synchronization of scroll-positions. Also no need to worry about the previous scroll position and transition, because Compose renders UI based on the current input state for each frame. Thanks to Compose‘s state-driven architecture and unidirectional data flow, features like this can be easily achieved.

Reason #3 Single Skill Set

Thinking in Compose may not explicitly point it out. But personally, this was actually the main reason that drew me into learning about Compose in the first place.

Thanks to the design of Android View system, XML-based UI development becomes a separate knowledge base from the core software development. We needed to learn how to use XML to express layout, attribute, style, theme, animations, etc.

But with Composewe finally unified our skill set and write UI with the same core expertise that Android developer already possessed: Kotlin language features, functional programming, coroutines, software engineering principles of writing readable and reusable code. Composable functions are similar to normal Kotlin functions. We can express conditions and loops with the same coding struts we are used to. The more we know about Compose API and its internal implementation, the more we find it familiar with our core knowledge base:

  • DSL captures many aspects of functional programming like extension, high-order functions, lambda with receivers, operator and infix(ex. provides) functions
  • Kotlin features such as immutability, trailing lambda argument, named function parameter and default values, delegate(ex. by), destructuring(ex. mutableStateOf), inline classes(ex. Color), singleton(ex. Theme), factory(ex. LazyListState)
  • Async programming with Kotlin coroutines for UI animations
  • Patterns like reactive programming like observable State and Flow

Final Thoughts

I have formed the 3 reasons why we(also We 😅)adopted Jetpack Compose. There should be plenty of resource to understand Kotlin & DSL already. But regarding the Compose architecture and internals (compiler/runtime/UI), there seemed to be limited insightful information available online. Also, the caveat is that some of them may contain guess work from the authors. Interesting, I attached a few resources here as they have been useful for me to understand Compose better. Hope they are helpful.

References:

  • Jetpack Compose Stability Explained
  • Under the hood of Jetpack Compose — part 2 of 2
  • Jetpack Compose internals
  • ViewModel: One-off event antipatterns

https://medium.com/media/def41c2eaf5c8d57bfe13fac983d0daf/href

  • Why should you always test Compose performance in release?
  • Composable metrics
  • What does Recomposition mean to your app?
  • What is "donut-hole skipping" in Jetpack Compose?
  • Inside Jetpack Compose


Why We Adopted Jetpack Compose was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment