Blog Infos
Author
Published
Topics
Published
Topics

This year’s Android Dev Summit (ADS) got kicked off on October 24 with keynote and Modern Android Development track. They covered a lot of topics ranging from Jetpack Compose, App Architecture, App Performance, Android Studio Tooling support (for both Jetpack Compose & Crashlytics), Relay — a tool for Design handoff (I highly recommend this talk) and much more. In case you haven’t watched it yet I highly suggest you to go through them via this link.

NOTE: ADS is not yet over. This event is going to have more tracks like Form Factors (November 9 in London) & Platform (November 14) and later in December in Asia as well. So Stay Tuned. 🙂

Even though all the things covered very important and insightful, the topic that got my attention was Compose Modifiers deep dive by Leland Richardson where he covers the history of how Modifiers came into existence (along with some of the things to consider when designing/building a new UI system), the problems associated with their current state and how they’re evolving (thankfully without any changes on the API surface unless you’re using Modifier.composed{ }). Since this was a complex topic I decided to take notes on the same in case I need to refer something in future then I don’t have to scrimmage through the YouTube video (still I highly recommend people to watch the talk atleast once). So the remaining post is my notes on the same and some of the commentary from my end to understand the things covered in the talk.

How Modifiers Came into Existence
  • One of the approach to provide paddings (all directions), background color, gradient, border and other properties to each Compose UI Node can be via class variables but that can quickly get out of hand because of the sheer number of properties that an UI component can support.

 

Hypothetical implementation of Compose UI Node

 

  • Then another approach can be to make each of these properties i.e. padding, background color, clickable as standalone Composable but this approach requires each of this composable to have a composable lambda parameter which can accept children, which leads to a problem that Clickable/Padding needs to know about some sort of Layout Policy (which will be utilizied during the Layout phase to place the children composables)

 

Hypothetical implementation of a Composable

 

  • Then came the Modifiers which we are using these days which can be chained/cascaded to provide multiple properties to Composables e.g. padding, background color, border, shape, click listeners or even gestures.

 

Using Modifiers to provide padding, background color & click listener to a Row composable

 

  • Using Modifiers is neat but it could cause a problem if a modifier with some state (e.g. Clickable which has ripple effect animation) is stored as a variable and then passed to two or more different Composables, as each composable needs to have their own animation state. These issue was tackled by using a Modifier.composed { } & materialize() API (which is one of the first thing that gets called in Layout() composable) so that even if same modifier is passed to multiple Composables each of them will get their own state.

 

Modifier.composed {} & materialize() API usage

Job Offers

Job Offers


    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

, ,

From Scoped Storage to Photo Picker: Everything to know about Storage

Persistence is a core element of every mobile app. Android provides different APIs to access or expose files with different tradeoffs.
Watch Video

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer advocate
Google

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer ad ...
Google

From Scoped Storage to Photo Picker: Everything to know about Storage

Yacine Rezgui
Android developer advocat ...
Google

Jobs

  • Modifier.composed { } API along with materialize() solved the issue of having separate states even if same Modifier is used for multiple Composables but this ended up causing a performance issue because of following reasons.

– Because Modifier.composed { } has a return type i.e. Modifier and since Composables with return type can’t be skipped during Recomposition.

– Also since Modifier.composed() is not a composable we can’t even use the all mighty Compose compiler to cache the lambda used earlier to compose a new Modifier which results in the new Modifier composed to be not equal to the one generated earlier even if there was no change.

– This also causes many remember calls internal to the composed Modifiers to be called unnecessary.

Here we can’t skip composition because clickable function has a return type i.e. Modifier

since composed is not a Composable function we can’t memorize the lambda using remember API

To enable unique state per composable the returned Modifier from composed lambda is never same

As composed API is used to store some kind of state by Modifiers, remember calls required to cache the state

Even though it seemed like a big problem, the assumption was that Modifier.composed {} API will be required only in very few special cases, but it ended up being wrong. 😅

Modifiers in Practice & the problems associated with it
  • The Text composable with just a Modifier.clickable{} seems very simple but Modifier.clickable{} internally cascades to more modifiers like semantics(), onKeyEvent(), indication(), pointerInput() and so on plus the Text Composable which might look very innocent, uses TextController internally which also creates it’s own Modifiers (check TextController.modifiers for better understanding) which are responsible for rendering the text, semantics & selections (i.e. which facilitates the Cut, Copy functions that we get on selecting a text). So for a simple Text Composable with just clickable modifiers leads to having some 20+ cascaded Modifiers 😲. Check below screenshots for reference:
Column with two Text composables
UI tree having a Layout with two Text composables with one of them having clickable Modifier
Mental model of the Modifier chain generated for a Text composable with clickable Modifier
  • If we aggregate the the contents of Modifier.clickable{} then we can see the result having (below Compose 1.3.0-beta01 at least):
    – 13 Modifier.composed calls
    – 34 remember calls
    – 11 Side Effects
    – 16 Leaf Modifier.Elements

What above point essentially means is that when we apply clickable {} modifier to a Composable

– On composition it’ll end up calling materialize() for all of the 13
Modifier.composed calls then all the remember calls held inside also gets called

– Which then allocates memory for Entity objects (i.e. objects that manage behaviours between different subsystems and dispatch things to these Modifiers 😵‍💫😵‍💫…🤷) for each modifier

– On Recomposition we end up allocating new Modifiers which calls materialize() for all composed Modifiers then the old Entity objects gets disposed and new ones gets allocated

  • The key takeaway here is that many of the Modifiers needs to have their own state and that state is applied per layout node. And these was only possible by Modifier.composed{} API before Compose 1.3.0-beta01 release where experimental support for Modifier.Node got implemented
Evolution of Modifiers staring Compose 1.3.0-beta01
  • In Compose 1.3.0-beta01 experimental support Modifier.Node got implemented as an alternative to Modifier.composed{} which is more performant (NOTE: This migration will happen over multiple releases and is still WIP)
  • With the introduction of Modifier.Node all the modifiers like padding, background, even clickable will be treated as a new type of lightweight immutable Modifier.Element which knows how to maintain an instance of a corresponding Modifier.Node class.
On first composition Modifier chain creates their own Modifier.Element instance
  • These Modifier.Element classes also gets a create() function which then creates/allocates Modifier.Node instances. These Node instances are directly part of the UI hierarchy and they have the same lifecycle as the layouts they are applied to, which makes them ideal owners of the state for the modifiers that need it like clickable.

Modifier.Element calling create() to instantiate Modifier.Node instances

 

  • Now talking about the earlier scenario when the recomposition happens, we don’t need to create new modifiers for the whole modifier chain because these Modifier.Element are immutable and can be compared easily. So we end up creating and applying Modifier.Element & Modifier.Node only for the changed modifiers in the whole chain. So in current case since on only the padding Modifier value changed from 10.dp -> 16.dp, only padding Modifier.Element calls update() to update PaddingNode and the remaining Modifier.Element don’t do any unnecessary computation.

On recomposition Modifier chain gets evaluated again

 

On recomposition only padding Modifier.Element calls update() to update the PaddingNode

 

Benefits of the new Modifier.Element & Modifier.Node

 

Takeaways of the new Modifier.Element & Modifier.Node

 

Alas, we’ve reached the end! 🙌 By this point you might be thinking that this seems a bit more complex and might not be needed to know as well since it’s an internal implementation (as mentioned earlier unless you’re using Modifier.composed {} API directly just updating to Compose 1.3.0-beta01 version should be sufficient to get the performance benefits of the internal changes) but it’s always better to get an idea about how things are working internally and the challenges faced in designing them. 🙂

Hope you liked reading the article. Until next time. Thank you!

This article was originally published on proandroiddev.com on November 04, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
As you might know, Jetpack Compose heavily leverage on Kotlin feature to implement Declarative…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu