@Composable Modifier vs composed factory
Composed and CMF (@Composable Modifier Factory) are two methods to create custom modifiers in Jetpack Compose. They both enable using higher-level compose APIs like animate*AsState and holding state with the remember() function.
// @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 }
}
Google initially suggested using composed over Composable Modifier Factory (CMF), even warning users in Android Studio. However, they've recently updated their recommendations, advising the use of CMF or Modifier.node due to performance concerns with composed.
We'll examine the distinctions, suitable scenarios, constraints, and performance of each method.
1. Extractability
To boost performance, we can move our modifiers outside of the Composition scope. This helps avoid the overhead of constructing them during each recomposition, particularly when using animations or withLazyColumn/LazyRow items.
val extractedModifier = Modifier.background(Color.Navy).padding(8.dp)...
@Composable
fun MyComposable() {
LazyColumn {
items(5) { Text("Hello $it", modifier = extractedModifier) }
}
}
@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.
Like....
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.graphics.*
@Composable
fun Modifier.myCMFBackground(): Modifier {
val color = LocalContentColor.current
return this.then(background(color.copy(alpha = 0.5f)))
}
fun Modifier.myComposedBackground(): Modifier = composed {
val color = LocalContentColor.current
this.then(background(color.copy(alpha = 0.5f)))
}
@Composable
fun MyScreen() {
val greenColor = remember { Color.Green }
val redColor = remember { Color.Red }
CompositionLocalProvider(LocalContentColor provides greenColor) {
val usingCMFModifier = remember { Modifier.myCMFBackground().size(16.dp) }
val usingComposedModifier = remember { Modifier.myComposedBackground().size(16.dp) }
CompositionLocalProvider(LocalContentColor provides redColor) {
Row {
// Box will have green background, not red as expected.
Box(modifier = usingCMFModifier)
// Box has green background as expected.
Box(modifier = usingComposedModifier)
}
}
}
}
3. State Resolution .
A. CMF
B Normal
when we have some lists and need to render then we need to save the composable state.
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...
}
without saved state of each composable
@Composable
fun BoxesRow() {
LazyRow {
items(10) {
Box(
modifier = Modifier.rotateOnClick().size(100.dp),
)
}
}
}
You will notice a strange behavior…
It's indeed peculiar! CMF resolves the state only once at the call site, while composed resolves the state at the usage site for each layout. This difference is crucial to keep in mind when designing reusable custom Modifiers.
4. Performance
It's not as simple as just avoiding CMF altogether. Even though composed used to work fine by calling materialize(), it's now discouraged due to performance issues.
The problem is, calling materialize() is expensive. Even a basic modifier can involve lots of other modifiers and states. Flattening all of these out affects performance and might create redundant copies of Modifier.Elements.
For example, before migrating to the Modifier. Node API, just the clickable modifier would add all these modifiers to the layout.
- 13 Modifier.composed calls
- 34 remember calls
- 11 Side Effects
- 16 Leaf Modifier.Elements
Stay tuned for upcoming articles. For any quires or suggestions, feel free to hit me on Twitter Insta + LinkedIn