When is Modifier.composed { ... }
useful? Why would I need it if I could simply do Modifier.padding()
over Modifier.composed { PaddingModifier(...) }
?
Modifier.composed allows the creation of a composition-aware modifier-factory that's useful for materializing instance-specific, stateful modifiers. From the docs:
Declare a just-in-time composition of a Modifier that will be composed for each element it modifies. composed may be used to implement stateful modifiers that have instance-specific state for each modified element, allowing the same Modifier instance to be safely reused for multiple elements while maintaining element-specific state.
In other words, it allows you to inject hoisted state into an element-specific Modifier
and use remember
, DisposableEffect
, Ambient
, etc. For instance:
fun Modifier.fancyModifier(
enabled: Boolean = false,
onClick: () -> Unit = {}
) = composed(inspectorInfo = debugInspectorInfo {
name = "fancyModifier"
value = enabled
}) {
var paddingValue by remember { mutableStateOf(0.dp) }
onCommit(enabled) {
paddingValue = if (enabled) 16.dp else 0.dp
}
fillMaxWidth()
.clickable { onClick() }
.padding(paddingValue)
}
LazyColumnFor(items = List(size = 10) { "$it" }) {
var enabled by remember { mutableStateOf(false) }
Text(
text = "fancy modifier",
modifier = Modifier.fancyModifier(enabled) {
enabled = !enabled
}
)
}
You can also declare InspectorInfo
to help with debugging by using debugInspectorInfo
. From the docs:
If inspectorInfo is specified this modifier will be visible to tools during development. Specify the name and arguments of the original modifier. as well as optionally declare
InspectorInfo
to help with debugging.
You can use the properties
field instead of value
if you have more than value you'd like to track.
Note: debugInspectorInfo lambdas are removed from release
builds.
class FancyModifierTest {
@Before
fun setup() {
isDebugInspectorInfoEnabled = true
}
@After
fun teardown() {
isDebugInspectorInfoEnabled = false
}
@Test
fun testFancyModifierInspectableValue() {
val modifier = Modifier.fancyModifier() as InspectableValue
assertEquals(modifier.nameFallback, "fancyModifier")
assertEquals(modifier.valueOverride, false)
assertEquals(modifier.inspectableElements.toList().size, 0)
}
}
Here are some more practical examples:
You can use it store memory heavy objects to not instantiate every time that modifier is called for a specific element.
This one with composed remembers color with index thus after each recomposition it returns color that was randomly created initially.
// Creates stateful modifier with multiple arguments
fun Modifier.composedBackground(width: Dp, height: Dp, index: Int) = composed(
// pass inspector information for debug
inspectorInfo = debugInspectorInfo {
// name should match the name of the modifier
name = "myModifier"
// add name and value of each argument
properties["width"] = width
properties["height"] = height
properties["index"] = index
},
// pass your modifier implementation that resolved per modified element
factory = {
val density = LocalDensity.current
val color: Color = remember(index) {
Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
}
// add your modifier implementation here
Modifier.drawBehind {
val widthInPx = with(density) { width.toPx() }
val heightInPx = with(density) { height.toPx() }
drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
}
}
)
This one creates a color each time composable is recomposed
fun Modifier.nonComposedBackground(width: Dp, height: Dp) = this.then(
// add your modifier implementation here
Modifier.drawBehind {
// 🔥 Without remember this color is created every time item using this modifier composed
val color: Color = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
val widthInPx = width.toPx()
val heightInPx = height.toPx()
drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
}
)
Usage
var counter by remember { mutableStateOf(0) }
Button(
onClick = { counter++ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Increase $counter")
}
TutorialText2(text = "Modifier.composed")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Box(
modifier = Modifier
.composedBackground(150.dp, 20.dp, 0)
.width(150.dp)
) {
Text(text = "Recomposed $counter")
}
Box(
modifier = Modifier
.composedBackground(150.dp, 20.dp, 1)
.width(150.dp)
) {
Text(text = "Recomposed $counter")
}
}
TutorialText2(text = "Modifier that is not composed")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Box(
modifier = Modifier
.nonComposedBackground(150.dp, 20.dp)
.width(150.dp)
) {
Text(text = "Recomposed $counter")
}
Box(
modifier = Modifier
.nonComposedBackground(150.dp, 20.dp)
.width(150.dp)
) {
Text(text = "Recomposed $counter")
}
}
Result
Also in a practical you can create an animated shake modifier for instance
fun Modifier.shake(enabled: Boolean) = composed(
factory = {
val scale by animateFloatAsState(
targetValue = if (enabled) .9f else 1f,
animationSpec = repeatable(
iterations = 5,
animation = tween(durationMillis = 50, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Modifier.graphicsLayer {
scaleX = if (enabled) scale else 1f
scaleY = if (enabled) scale else 1f
}
},
inspectorInfo = debugInspectorInfo {
name = "shake"
properties["enabled"] = enabled
}
)
Usage
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.shake(enabled)
.background(Color.Red, CircleShape)
.size(50.dp)
.padding(10.dp)
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With