Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to check if a composable is empty?

I'm building a composable like:

@Composable
fun BottomSectionWrapper(
    content: @Composable () -> Unit,
    bottomSection: @Composable () -> Unit,
)

I now would like to know if the parameter bottomSection is empty, so i can adjust my layout accordingly. Is this possible?


I won't be defining "empty" any further; i'll take whatever is technically possible:

  • an empty lambda {}
  • a lambda which doesn't emit a composable: { if(false) Text("Test") }
  • a lambda which has zero size: { Box(Modifier.size(0.dp)) }

Let me explain my typical usecase: I'm using this wrapper to attach buttons to the lower end of a screen. And if there is a button, i want to add a fade.


What have i tried?

I tried making the lambda nullable - that did not compile

BottomSectionWrapper(
    content = { /* ... */ },
    bottomSection = if(showDeleteButton){
        Button { Text("Delete") }
    } else null
)

Also: Intuitively, i would have written this like this (which would not have been detected by a simple null check):

BottomSectionWrapper(
    content = { /* ... */ },
    bottomSection = {
        if (showDeleteButton) {
            Button { Text("Delete") }
        }
    }
)
like image 590
m.reiter Avatar asked Oct 12 '25 16:10

m.reiter


1 Answers

If you wish to have nullable contents you need to set declaration as

@Composable
fun BottomSectionWrapper(
    modifier: Modifier = Modifier,
    content: @Composable (() -> Unit)? = null,
    bottomSection: @Composable (() -> Unit)? = null,
) {
    Column(modifier = modifier) {
        content?.invoke()
        bottomSection?.invoke()
    }
}

And you can use it as in this demo

@Preview
@Composable
private fun BottomSectionWrapperTest() {

    Column {
        var isSet by remember {
            mutableStateOf(false)
        }

        Button(onClick = { isSet = isSet.not() }) {
            Text("isSet: $isSet")
        }
        

        val bottomSection: (@Composable () -> Unit)? = if (isSet) {
            {
                Box(modifier = Modifier.size(100.dp).background(Color.Green)) {
                    Text("Bottom Section")
                }
            }

        } else null


        BottomSectionWrapper(
            modifier = Modifier.border(2.dp, Color.Red),
            content = {
                Text("Content Section")
            },
            bottomSection = bottomSection
        )
    }
}

If you wish to detect whether a Composable is empty or have 0.dp size or any size you can use Layout with different approaches, dimension setting and placing options.

Second one with Modifier.layoutId is used in default Composables such as TextField, Tab

@Composable
fun BottomSectionWrapperLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit = {},
    bottomSection: @Composable () -> Unit = {},
) {

    Layout(
        modifier = modifier,
        contents = listOf(content, bottomSection)
    ) { measurables: List<List<Measurable>>, constraints: Constraints ->

        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        val contentPlaceable: Placeable? =
            measurables.first().firstOrNull()?.measure(looseConstraints)

        val bottomSectionPlaceable: Placeable? =
            measurables.last().firstOrNull()?.measure(looseConstraints)

        println("content: $contentPlaceable, bottomSectionPlaceable: $bottomSectionPlaceable")

        // you can set width and height as required, i make it as a Column
        // this width and height determines whether it's a Column, Row or a Box

        val contentWidth = contentPlaceable?.width ?: 0
        val contentHeight = contentPlaceable?.height ?: 0

        val bottomSectionWidth = bottomSectionPlaceable?.width ?: 0
        val bottomSectionHeight = bottomSectionPlaceable?.height ?: 0

        val totalWidth = if (constraints.hasFixedWidth && constraints.hasBoundedWidth)
            constraints.maxWidth else contentWidth.coerceAtLeast(bottomSectionWidth)
            .coerceIn(constraints.minWidth, constraints.maxWidth)

        val totalHeight = if (constraints.hasFixedHeight && constraints.hasBoundedHeight)
            constraints.maxHeight else (contentHeight + bottomSectionHeight)
            .coerceIn(constraints.minHeight, constraints.maxHeight)

        layout(totalWidth, totalHeight) {
            // You can place them according to your logic here
            contentPlaceable?.placeRelative(0, 0)
            bottomSectionPlaceable?.placeRelative(0, contentHeight)
        }
    }
}

With this Layout you can detect whether any of them are empty by checking if Placeable is null which means empty lambda is passed and if not null you can check its size.

@Preview
@Composable
private fun LayoutTest() {
    Column(modifier = Modifier.fillMaxSize()) {
        BottomSectionWrapperLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            content = {
                Text("Content Section")
            },
            bottomSection = {
                Text("Bottom Section")
            }
        )

        Spacer(Modifier.height(20.dp))
        BottomSectionWrapperLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            bottomSection = {
                Text("Bottom Section")
            }
        )

        Spacer(Modifier.height(20.dp))
        BottomSectionWrapperLayout(
            modifier = Modifier.border(2.dp, Color.Red),
            content = {
                Text("Content Section")
            }
        )
   
    }
}

If you check out println you can see that in each case you will be able to see what's null or not. Also you can get Placable dimensions as well.

Other option is to create this layout as Textfield does in line 495.

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt;l=495?q=TextField&ss=androidx%2Fplatform%2Fframeworks%2Fsupport

But with this approach you won't be able to check if you passed empty lambda because its placed inside a Box when not null. However this approach is often used since you can check and compare contents by their ids instead of checking lists.

@Composable
fun BottomSectionWrapperLayout2(
    modifier: Modifier = Modifier,
    content: @Composable (() -> Unit)? = null,
    bottomSection: @Composable (() -> Unit)? = null,
) {

    Layout(
        modifier = modifier,
        content = {

            if (content != null) {
                Box(
                    modifier = Modifier.layoutId("content")
                ) {
                    content()
                }
            }

            if (bottomSection != null) {
                Box(
                    modifier = Modifier.layoutId("bottomSection")
                ) {
                    bottomSection()
                }
            }
        }
    ) { measurables: List<Measurable>, constraints: Constraints ->

        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

        val contentPlaceable: Placeable? =
            measurables.find { it.layoutId == "content" }?.measure(looseConstraints)
        val bottomSectionPlaceable =
            measurables.find { it.layoutId == "bottomSection" }?.measure(looseConstraints)

        // you can set width and height as required, i make it as a Column
        // this width and height determines whether it's a Column, Row or a Box

        val contentWidth = contentPlaceable?.width ?: 0
        val bottomSectionWidth = bottomSectionPlaceable?.width ?: 0

        val contentHeight = contentPlaceable?.height ?: 0
        val bottomSectionHeight = bottomSectionPlaceable?.height ?: 0

        val totalWidth = if (constraints.hasFixedWidth && constraints.hasBoundedWidth)
            constraints.maxWidth else contentWidth.coerceAtLeast(bottomSectionWidth)
            .coerceIn(constraints.minWidth, constraints.maxWidth)

        val totalHeight = if (constraints.hasFixedHeight && constraints.hasBoundedHeight)
            constraints.maxHeight else (contentHeight + bottomSectionHeight)
            .coerceIn(constraints.minHeight, constraints.maxHeight)


        layout(totalWidth, totalHeight) {
            contentPlaceable?.placeRelative(0, 0)
            bottomSectionPlaceable?.placeRelative(0, contentHeight)
        }
    }
}

Usage is same as other layout.

like image 166
Thracian Avatar answered Oct 14 '25 11:10

Thracian