Blog Infos
Author
Published
Topics
Published
Topics

Photo by Hal Gatewood on Unsplash

 

In the recent release of Jetpack Compose 1.5 we got Modifier.Node as stable. There was some hype around this change, so let’s see what this new API can offer and how we can implement it. This API is not well-documented yet, so it is kind of wild west for now.

With the help of the new Modifier.Node we can create lightweight modifiers that are not composable and thus more performant. Modifiers like padding and background will be treated as a new type of lightweight immutable Modifier.Element that knows how to maintain an instance of a corresponding Modifier.Node class. Even clickable modifier has migrated to this new API and the Compose team claims almost 80% performance improvement.

How it works?

Because new modifier elements are immutable, it is significantly easier and cheaper to compare to the previous state. It is a common scenario when almost nothing is changed in modifiers between the recomposition, so Compose can skip and even doesn’t need to apply the modifier at all. When the modifier is changed, Compose then diffs the previous list of modifiers with the new ones, and because all of them are comparable, Compose only applies the modified(pun intended) modifier.

In addition to that Modifier.Node#coroutineScope allows Modifier.Nodes to launch coroutines and read CompositionLocals by implementing the CompositionLocalConsumerModifierNode interface.

For in-depth stories, I highly recommend listening to the Compose Performance episode from the Android Developers Backstage podcast

In the recent release of Jetpack Compose 1.5 we got Modifier.Node as stable. There was some hype around this change, so let’s see what this new API can offer and how we can implement it. This API is not well-documented yet, so it is kind of wild west for now.
What about Modifier.composed {}

Modifier.composed{} is not going anywhere but is already removed from the Compose guidelines documentation. We still need it for some cases where we access composables from modifiers but it will not be the only option to create new modifiers from now on. Many of the Compose modifiers have been migrated to the new Modifier.Node already. The biggest problems with the composed modifier are:

  • has a Modifier return type and compostables with return type are not skippable during the recomposition.
  • composed is not a composable so Compose compiler cannot memoize lambda and cannot compare to previous value even if there is no change.

Even if we don’t use composed{} modifier excessively, basic composable functions use dozens of modifiers that were made from composed internally.

There is an awesome video by Leland Richardson that goes through the details of this change.

In the recent release of Jetpack Compose 1.5 we got Modifier.Node as stable. There was some hype around this change, so let’s see what this new API can offer and how we can implement it. This API is not well-documented yet, so it is kind of wild west for now.

From that video, we understand that if we aggregate the contents of Modifier.clickable{} then we can see the result (before introducing Modifier.Node — Compose 1.3):

  • 13 Modifier.composed calls
  • 34 remember calls
  • 11 Side Effects
  • 16 Leaf Modifier.Elements

Mind-blowing That’s why we see huge performance improvement in the Compose internally. Upgrade to Compose 1.5 at least 😉 It is backward compatible, with no change on the consumer side.

If you are a library developer or just contributing a modifier to the project, think twice about which API you need.

Here is the list of already existing Modifier Nodes: https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier.Node

Implementation

Implementation is 3 step process, we can take a look at the modifiers in the Compose samples.

https://github.com/android/compose-samples/blob/main/Jetcaster/app/src/main/java/com/example/jetcaster/util/GradientScrim.kt

This modifier is responsible for drawing a vertical gradient scrim in the foreground.

  1. First we create a modifier extension function fun Modifier.verticalGradientScrim and chain to Modifier node element.
fun Modifier.verticalGradientScrim(
    color: Color,
    @FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f,
    @FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f,
    decay: Float = 1.0f,
    numStops: Int = 16
) = this then VerticalGradientElement(color, startYPercentage, endYPercentage, decay, numStops)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Meet Jewel:Create IDE plugins in Compose

Jetpack Compose is the declarative UI toolkit for Android that makes it easy to create beautiful, responsive apps. However, until recently, there was no easy way to use Compose to create IDE plugins without too…
Watch Video

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi & Chris Sinco
UX Engineer & UX Design Lead
Google

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi & ...
UX Engineer & UX Des ...
Google

Meet Jewel:Create IDE plugins in Compose

Sebastiano Poggi ...
UX Engineer & UX Design L ...
Google

Jobs

2) Implement ModifierNodeElement with 2 core functions — create() and update(). Add inspector info for LayoutInspector.

private data class VerticalGradientElement(
    var color: Color,
    var startYPercentage: Float = 0f,
    var endYPercentage: Float = 1f,
    var decay: Float = 1.0f,
    var numStops: Int = 16
) : ModifierNodeElement<VerticalGradientModifier>() {}

3) Implement Modifier.Node and already existing interface DrawModifierNode that draws into the space of the layout. This is the androidx.compose.ui.Modifier.Node equivalent of androidx.compose.ui.draw.DrawModifier

private class VerticalGradientModifier(
    var onDraw: DrawScope.() -> Unit
) : Modifier.Node(), DrawModifierNode {

    override fun ContentDrawScope.draw() {
        onDraw()
        drawContent()
    }
}

That’s it. Keep in mind that leaving `VerticalGradientModifier` private means that the node will not be delegated — https://developer.android.com/reference/kotlin/androidx/compose/ui/node/DelegatingNode

Migration

This is the part that is not presented/documented well. Every use case is different but we can still get the idea of how to construct new modifiers.

Migrating existing Modifier.Node can be a bit challenging in the beginning because of the extra steps and rearchitecting modifier code. Here is a very good example of how the Compose team migrated Hoverable modifier to Modifier.Node.

A few important notes. Previously composed had remembermutableStateOf, and various side effects in the method body. After migration we see them disappear but the parts of the code moved to the different callbacks.

  • CoroutineScope is accessible from Modifier.Node
  • Consider passing additional values from outside (like IME state), instead of handling them in the modifier because new Modifier.Node can’t access composable, so we need to find workarounds.
  • All the remember -ed values can be stored as class members and update them via update callback from ModifierNodeElement
Summary

Modifier.Node provides a new, performant way of creating modifiers. Key benefits are:

  • Less allocations
  • Less composition
  • Smaller tree
  • Better performance
  • Backward compatible

 

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
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