Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When do you need Modifier.composed { ... }?

When is Modifier.composed { ... } useful? Why would I need it if I could simply do Modifier.padding() over Modifier.composed { PaddingModifier(...) }?

like image 359
Archie G. Quiñones Avatar asked Mar 02 '23 21:03

Archie G. Quiñones


2 Answers

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:

  • JetSnack Gradient
  • JetCaster Gradient
  • InspectorInfo Samples
  • cs.android.com
like image 192
adneal Avatar answered Mar 06 '23 00:03

adneal


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

enter image description here

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)
)
like image 20
Thracian Avatar answered Mar 06 '23 00:03

Thracian