androidandroid-jetpack-composeandroid-jetpack

How to set alignment for custom Composables in Jetpack Compose?


I'm part of a team working on an Android app prototype for our final project in a Project Management course at university, and we've run into a bit of an issue learning Jetpack Compose.

So, basically, I'm trying to keep the code as clean, modular, easy to debug, and easy to read as possible (a lot of jetpack code is ugly and painful to the eye for a team that isn't intimately familiar with Kotlin syntax), and because of this, I have a habit of wrapping UI composables in custom composables. For example:

@Composable
fun ColoredBackgroundTextBox(text : String, color: Color)
{
    ColoredBackgroundBox(content = {
        Text(text, modifier = Modifier.align(Alignment.Center));
    }, color = Color.Gray, width = 200, height = 30)
}

@Composable
fun ColoredBackgroundBox(content : @Composable () -> Unit, color : Color, width : Int, height : Int)
{
    Box(modifier = Modifier.size(width.dp, height.dp).background(color))
    {
        content();
    }
}

In the above example, I'm trying to create a text element wrapped within a colored background box (as part of a larger component; basically, we're getting a list of installed apps and generating a GUI panel for each app installed on your device).

The problem here is, the extension function Modifier.align is only available within the context of a Box, Row, or Column. As you can see, content is always created/called nested within a box, but Android Studio/Kotlin don't appear to "know" that. The only workaround appears to be, remove the middle-man composable functions and just do everything directly within a Box, but that seems super crude and inelegant, and makes the code harder to modify/extend later.

Is there any way to just "tell" the compiler "hey, even though this doesn't look like it's getting called from within the correct context, it is, so quit throwing errors and just work"?


Solution

  • Modifier.align() is a scoped Modifier and only accessible in scope it's been declared in, for Box it's BoxScope.

    If you change content : @Composable () -> Unit of ColoredBackgroundBox to content : @Composable BoxScope() -> Unit to have receiver of BoxScope content will be able to access modifiers belong to BoxScope. It will also mean you will only be able to invoke content() inside a Box.

    @Composable
    fun ColoredBackgroundTextBox(text: String, color: Color) {
        ColoredBackgroundBox(
            color = Color.Gray,
            width = 200,
            height = 30,
            content = {
                Text(text, modifier = Modifier.align(Alignment.Center))
            }
        )
    }
    
    @Composable
    fun ColoredBackgroundBox(
        content: @Composable BoxScope.() -> Unit,
        color: Color,
        width: Int,
        height: Int,
    ) {
        Box(modifier = Modifier.size(width.dp, height.dp).background(color))
        {
            content()
        }
    }