@Composable Modifier vs composed factory in Jetpack Compose

ilyas ipek
Teknasyon Engineering

--

Which one to choose!!!

composed and CMF (@Composable Modifier Factory) are two different approaches for creating custom modifiers that allow for using higher-level compose APIs such as animate*AsState or holding state via remember() function. for example…

// @Composable Modifier Factory
@Composable
fun Modifier.fade(enable: Boolean): Modifier {
val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
return graphicsLayer { this.alpha = alpha }
}

// composed {}
fun Modifier.fade(enable: Boolean): Modifier = composed {
val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
graphicsLayer { this.alpha = alpha }
}

Previously, Google recommended using composed over CMF, even Android Studio would show a warning if you use it. However, Google has recently changed their docs to recommend using CMF or Modifier.Node API over composed due to the performance issues it created.

In this article, we will explore each method's differences, use cases, limitations, and performance.

1- Extractability

To improve performance, we can extract our modifiers outside of the Composition scope to avoid construction costs on each recomposition, especially when using animations or withLazyColumn/LazyRow items. For instance…

val extractedModifier = Modifier.background(Color.Red).padding(16.dp)...

@Composable
fun MyComposable() {
LazyColumn {
items(10) { Text("Hello $it", modifier = extractedModifier) }
}
}

However, when we create a CMF we can’t use it outside of the Composition scope since it has to be annotated with @Composable. On the contrary, this isn’t an issue with composed at all, as it doesn’t require @Composable annotation, allowing for broader usage.

@Composable
fun Modifier.usingComposableFactory(): Modifier = ...

fun Modifier.usingComposed(): Modifier = composed {/***/}

// usingComposed can be used outisde the Composition scope
val extractedModifier = Modifier.usingComposed()

@Composable
fun MyComposable() {
...
// we can only use usingComposableFactory() inside a @Composable scope
Text("Hello $it", modifier = extractedModifier.usingComposableFactory())
}

2- Resolution Location of CompositionLocal Values

When using CompositionLocals such as LocalContentColor, CMF and composed behave differently.

  1. CMF: CompositionLocals values are resolved at the call site of the modifier factory.
  2. composed: CompositionLocals values are resolved at the usage site of the composed factory.

For example…

@Composable
fun Modifier.myCMFBackground(): Modifier {
val color = LocalContentColor.current
return background(color.copy(alpha = 0.5f))
}

fun Modifier.myComposedBackground(): Modifier = composed {
val color = LocalContentColor.current
background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
CompositionLocalProvider(LocalContentColor provides Color.Green) {
// Background modifier created with green background
val usingCMFModifier = Modifier.myCMFBackground().size(16.dp)
val usingComposedModifier = Modifier.myComposedBackground().size(16.dp)

// LocalContentColor updated to red
CompositionLocalProvider(LocalContentColor provides Color.Red) {
Row() {
// Box will have green background, not red as expected.
Box(modifier = usingCMFModifier)

// Box has green background as expected.
Box(modifier = usingComposedModifier)
}
}
}
}
The result.

This is an important thing to keep in mind, especially when creating generic modifiers, and you’re not sure how other developers might use it.

3- State Resolution

Let’s say we have a custom Modifier that sets a random background and rotates the Layout when it’s clicked. If we have 2 implementations using composed and CMF as following…

fun Modifier.rotateOnClick() = composed {
val color = remember { mutableStateOf(listOf(Color.Red, Color.Green).random()) }
var isClicked by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(targetValue = if (isClicked) 45f else 0f)

background(color = color.value)
.clickable { isClicked = !isClicked }
.graphicsLayer { rotationZ = rotation }
}

@Composable
fun Modifier.rotateOnClick(): Modifier {
// same as rotateOnClickUsingComposed...
}

Then use them inlined (each item will have a new Modifier) in a LazyRow

@Composable
fun BoxesRow() {
LazyRow {
items(10) {
Box(
modifier = Modifier.rotateOnClick().size(100.dp),
)
}
}
}

This will work perfectly as expected. It’s not impressive I know :)

BUT if you try to extract the Modifier to a variable and reuse it for more than one item…

@Composable
fun BoxesRow() {
val modifier = Modifier
.rotateOnClick()
.size(100.dp)
LazyRow {
items(10) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) { ... }
}
}
}

You will notice a strange behavior…

Strange right!!! That happens because CMF resolves that state only once at the call site whereas composed resolves state at the usage site for each Layout.

This is also an important factor to consider when creating reusable custom Modifiers.

4- Performance

Considering the three previous points, you might be thinking, “Wow, I should never use CMF and avoid all the headaches,” right? Well, it’s not that simple. composed is no longer recommended because it performs poorly.

The reason composed functions correctly is that materialize() is called before applying the Modifier to the layout. Which kind of recursively copies the states/modifiers before applying them to the Layout.

But as it turned out, calling materialize() is pretty expensive because even a simple modifier will utilize a lot of other modifiers/states, and flatting out all of that impacts the performance and potentially creates redundant copies of Modifier.Elements.

For instance, just the clickable modifier (before Modifier.Node API migration) will apply all these modifiers to the Layout…

Image from the Compose Modifiers deep dive talk

That would be a total of…

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

On the other hand, CMF does not use the materialize() function which reduces the performance overhead.

5- Skippablity

Sadly both approaches are not skippable 🥲

  1. CMF is never skipped because composable functions that have return values cannot be skipped.
  2. Since modifiers using composed aren’t @Composables themselves, the Compose Compiler can’t cache the lambda. That means that the resulting Modifier will never compare equals with the previous one, making it a non-skippable modifier.

Conclusion

As you can see, using CMF is ideal when you inline modifiers or extract a modifier for use in only one component. On the other hand, composed is useful when designing generic modifiers, but you should be aware of the performance overhead.

If you want the best of both worlds — performance, extractability, skippability, reusable modifiers — you should consider using the Modifier.Node API. This is a lower-level API for creating modifiers in Compose, and it’s the same API that Compose utilizes for its own modifiers.

That’s all for now, and I hope you enjoyed the blog! ❤️

--

--

Android developer @teknasyon, writes about Android development and productivity.