Layouts no Compose

O Jetpack Compose facilita muito o processo de projetar e criar a IU do app. Este documento explica alguns dos elementos básicos que o Compose oferece para ajudar a organizar os elementos da IU e mostra como criar layouts mais especializados quando necessário.

Funções que podem ser compostas são o elemento básico fundamental do Compose. Uma função que pode ser composta é uma função que emite uma Unit de descrição de alguma parte da IU. A função recebe alguma entrada e gera o que será exibido na tela. Para ver mais informações sobre funções que podem ser compostas, consulte a documentação Modelo mental do Compose.

Uma função que pode ser composta pode emitir vários elementos da IU. No entanto, se você não fornecer orientações sobre como eles devem ser organizados, o Compose poderá organizar os elementos de uma maneira que você não quer. Por exemplo, este código gera dois elementos de texto:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

Sem orientação sobre como você quer organizá-los, o Compose colocará os elementos de texto um sobre o outro, o que os deixará ilegíveis:

Dois elementos de texto desenhados um sobre o outro, o que torna o texto ilegível

O Compose fornece uma coleção de layouts prontos para uso que ajudam você a organizar seus elementos da IU e facilita a definição dos seus layouts mais especializados.

Componentes de layout padrão

Em muitos casos, você pode simplesmente usar os elementos de layout padrão do Compose.

Use Column para colocar itens na tela verticalmente.

@Composable
fun ArtistCard() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

Dois elementos de texto organizados em um layout de coluna, de modo que o texto seja legível

Da mesma forma, use Row para colocar itens na tela horizontalmente. Tanto Column quanto Row são compatíveis com a configuração de alinhamento dos elementos que eles contêm.

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(/*...*/)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Mostra um layout mais complexo, com uma pequena imagem ao lado de uma coluna de elementos de texto

Use Box para colocar um elemento sobre outro.

Compara três layouts simples que podem ser compostos: coluna, linha e box

Geralmente, esses elementos de criação são tudo o que você precisa. Você pode criar sua própria função que pode ser composta para combinar esses layouts em um mais elaborado, que seja adequado ao seu app.

Para definir a posição dos filhos em uma Row, defina os argumentos horizontalArrangement e verticalAlignment. Para uma Column, defina os argumentos verticalArrangement e horizontalAlignment:

@Composable
fun AlignInRow() {
    Row(
        modifier = Modifier
            .size(150.dp)
            .background(Color.Yellow),
        horizontalArrangement = Arrangement.End,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(Modifier.size(50.dp).background(Color.Red))
        Box(Modifier.size(50.dp).background(Color.Blue))
    }
}

Um grande quadrado amarelo, com caixas alinhadas no centro

Modificadores

Os modificadores permitem decorar ou aumentar as funções que podem ser compostas. Com os modificadores, é possível fazer o seguinte:

  • Mudar o tamanho, o layout, o comportamento e a aparência do elemento
  • Adicionar informações, como rótulos de acessibilidade
  • Processar a entrada do usuário
  • Adicionar interações de nível superior, como tornar um elemento clicável, rolável, arrastável ou redimensionável

Modificadores são objetos Kotlin padrão. Crie um modificador chamando uma das funções de classe Modifier. É possível encadear essas funções para fazer a composição:

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { /*...*/ }
    }
}

Um layout ainda mais complexo, usando modificadores para alterar a maneira como os elementos gráficos são organizados e quais áreas respondem às entradas do usuário

Observe diferentes funções de modificador sendo usadas juntas no código acima.

  • clickable gera uma reação que pode ser composta à entrada do usuário e exibe uma ondulação.
  • padding cria um espaço ao redor de um elemento.
  • fillMaxWidth faz a função que pode ser composta preencher a largura máxima atribuída a ela pelo elemento pai.
  • size() especifica a largura e a altura preferenciais de um elemento.

A ordem das funções modificadoras é importante. Como cada função realiza mudanças no Modifier retornado pela função anterior, a sequência afeta o resultado final. Veja um exemplo disso:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

Toda a área, incluindo o preenchimento ao redor das bordas, responde aos cliques

No código acima, a área inteira é clicável, incluindo o padding ao redor dela, porque o modificador padding foi aplicado depois do modificador clickable. Se a ordem dos modificadores for invertida, o espaço adicionado por padding não reagirá à entrada do usuário:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

O preenchimento ao redor da borda do layout não responde mais aos cliques

Modificadores integrados

O Jetpack Compose oferece uma lista de modificadores integrados para ajudar você a decorar ou aumentar uma função que pode ser composta. Veja a seguir uma lista dos modificadores que abrangem os casos de uso mais comuns:

Padding e tamanho

Para definir o padding ao redor de uma função que pode ser composta, adicione o modificador padding:

@Composable
fun PaddedComposable() {
    Text("Hello World", modifier = Modifier.background(Color.Green).padding(20.dp))
}

Texto com padding ao redor

Por padrão, os layouts fornecidos no Compose são agrupados aos filhos. No entanto, é possível definir um tamanho usando o modificador size:

@Composable
fun SizedComposable() {
    Box(Modifier.size(100.dp, 100.dp).background(Color.Red))
}

O tamanho especificado pode não ser respeitado caso ele não atenda às restrições provenientes do pai do layout. Caso você precise que o tamanho do elemento seja corrigido, independentemente das restrições de entrada, use o modificador requiredSize:

@Composable
fun FixedSizeComposable() {
    Box(Modifier.size(90.dp, 150.dp).background(Color.Green)) {
        Box(Modifier.requiredSize(100.dp, 100.dp).background(Color.Red))
    }
}

Dois retângulos que se encontram um sobre o outro. A caixa de cima é vermelha e mais larga e está sobre um fundo verde mais estreito

Nesse exemplo, mesmo com a largura pai definida como 90.dp, a largura da Box interna será 100.dp, já que o modificador requiredSize da caixa interna tem precedência.

Caso você queira que um layout filho preencha todo o espaço disponibilizado pelo pai, adicione o modificador fillMaxSize. O Compose também oferece fillMaxHeight e fillMaxWidth:

@Composable
fun FillSizeComposable() {
    Box(Modifier.background(Color.Green).size(50.dp).padding(10.dp)) {
        Box(Modifier.background(Color.Blue).fillMaxSize())
    }
}

Um quadrado azul no centro de um quadrado verde maior

Caso queira que um layout filho tenha o mesmo tamanho de uma Box mãe, sem afetar o tamanho da Box, use o modificador matchParentSize.

matchParentSize só está disponível em um escopo de Box, o que significa que ele se aplica apenas a filhos diretos das funções Box que podem ser compostas.

No exemplo abaixo, o Spacer interno assume o tamanho da Box mãe, que, por sua vez, assume o tamanho do Text que ela contém.

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.Green))
        Text("Hello World")
    }
}

Texto preenchendo o espaço do contêiner

Se fillMaxSize fosse usado em vez de matchParentSize, o Spacer ocuparia todo o espaço disponibilizado para o pai, fazendo com que o pai fosse expandido e preenchesse todo o espaço disponível.

Texto pequeno no canto superior de um grande quadrado verde

Caso queira adicionar padding acima da linha de base do texto, a fim de estabelecer uma distância específica do topo do layout até a linha de base, use o modificador paddingFromBaseline:

@Composable
fun TextWithPaddingFromBaseline() {
    Box(Modifier.background(Color.Yellow)) {
        Text("Hi there!", Modifier.paddingFromBaseline(top = 32.dp))
    }
}

Texto com padding na parte superior

Offset

Para posicionar um layout em relação à posição original, adicione o modificador offset e defina o deslocamento no eixo x e y. Os deslocamentos podem ser positivos e não positivos. A diferença entre padding e offset é que adicionar um offset a uma função não muda as medidas dela:

@Composable
fun OffsetComposable() {
    Box(Modifier.background(Color.Yellow).size(width = 150.dp, height = 70.dp)) {
        Text(
            "Layout offset modifier sample",
            Modifier.offset(x = 15.dp, y = 20.dp)
        )
    }
}

Texto deslocado para o lado direito do contêiner pai

O modificador offset é aplicado horizontalmente, de acordo com a direção do layout. Em um contexto de sentido da esquerda para a direita, um offset positivo desloca o elemento para a direita. Já em um contexto de sentido da direita para esquerda, o elemento é deslocado para a esquerda. Caso seja necessário definir um deslocamento sem considerar a direção do layout, analise o modificador absoluteOffset, em que um valor de deslocamento positivo sempre desloca o elemento para a direita.

Layouts roláveis

Para saber mais sobre layouts roláveis, consulte a documentação sobre gestos no Compose.

Layouts responsivos

Um layout precisa ser projetado pensando em diferentes orientações de tela e tamanhos. O Compose oferece alguns mecanismos prontos para facilitar a adaptação dos layouts de funções que podem ser compostas a diferentes configurações de tela.

Modificador de peso em Row e Column

Conforme abordado na seção anterior sobre Preenchimento e tamanho, por padrão, o tamanho de uma função que pode ser composta é definido pelo conteúdo a que ela está agrupada. É possível fazer com que o tamanho de uma função seja flexível dentro do pai. Vamos considerar uma Row contendo duas Box que podem ser compostas. A primeira caixa recebe o dobro de weight da segunda, portanto, duas vezes a largura. Como a Row tem 210.dp de largura, a primeira Box tem 140.dp de largura e a segunda tem 70.dp:

@Composable
fun FlexibleComposable() {
    Row(Modifier.width(210.dp)) {
        Box(Modifier.weight(2f).height(50.dp).background(Color.Blue))
        Box(Modifier.weight(1f).height(50.dp).background(Color.Red))
    }
}

Dois retângulos lado a lado, sendo um retângulo azul largo à esquerda de um retângulo vermelho mais estreito

Restrições

Para saber quais são as restrições provenientes do pai e projetar o layout de acordo com isso, é necessário usar BoxWithConstraints. As restrições de medidas podem ser encontradas no escopo do lambda do conteúdo. Essas restrições de medidas podem ser usadas para compor diferentes layouts para diferentes configurações de tela:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}
Layouts baseados em slot

O Compose oferece uma grande variedade de funções baseadas no Material Design com a dependência androidx.compose.material:material (incluída ao criar o projeto do Compose no Android Studio) para facilitar a criação da IUs. Elementos como Drawer, FloatingActionButton e TopAppBar são todos fornecidos pelo Compose.

Componentes do Material Design usam muito as APIs de slot, um padrão que foi introduzido no Compose para oferecer uma camada de personalização, além dos elementos que podem ser compostos. Essa abordagem faz com que os componentes sejam mais flexíveis, porque eles aceitam um elemento filho que pode se configurar. Portanto, não é mais necessário expor todos os parâmetros de configuração do filho. Os slots deixam um espaço vazio na IU, que o desenvolvedor pode preencher como quiser. Por exemplo, estes são os slots que podem ser personalizados em um TopAppBar:

Diagrama mostrando os slots disponíveis em uma barra de apps com componentes do Material Design.

Os elementos que podem ser compostos geralmente usam um lambda que pode ser composto content ( content: @Composable () -> Unit). As APIs de slot expõem vários parâmetros content para usos específicos. Por exemplo, TopAppBar permite que você forneça o conteúdo para title, navigationIcon e actions.

Por exemplo, Scaffold permite implementar uma IU com a estrutura básica de layout do Material Design. Scaffold fornece slots para os componentes de alto nível mais comuns do Material Design, como TopAppBar, BottomAppBar, FloatingActionButton, e Drawer. Usando Scaffold, é fácil garantir que esses componentes estejam posicionados corretamente e funcionem bem juntos.

App de amostra JetNews, que usa o Scaffold para posicionar vários elementos.

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        bodyContent = { /*...*/ }
    )
}

ConstraintLayout

ConstraintLayout pode ajudar a posicionar as funções que podem ser compostas em relação a outros na tela. Essa é uma alternativa ao uso de várias Row, Column e Box aninhadas e de elementos de layouts personalizados. ConstraintLayout é útil ao implementar layouts maiores com requisitos de alinhamento mais complexos. Contudo, prefira usar Columns e Rows ao criar layouts simples.

Para usar ConstraintLayout no Compose, é necessário adicionar essa dependência ao build.gradle:

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha03"

No Compose, ConstraintLayout funciona com uma DSL:

  • As referências são criadas usando createRefs() ou createRefFor(), e cada função que pode ser composta em ConstraintLayout precisa ter uma referência associada a ela.
  • As restrições são fornecidas usando o modificador constrainAs(), que usa a referência como um parâmetro e permite especificar as restrições no lambda do corpo.
  • As restrições são especificadas usando linkTo() ou outros métodos úteis.
  • parent é uma referência já existente que pode ser usada para especificar restrições no próprio ConstraintLayout que pode ser composto.

Veja um exemplo de elemento que pode ser composto usando um ConstraintLayout:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

Esse código restringe o topo do Button ao pai com uma margem de 16.dp e um Text na parte inferior de Button, também com uma margem de 16.dp.

Mostra um botão e um elemento de texto organizados em um ConstraintLayout

Para ver mais exemplos de como trabalhar com ConstraintLayout, consulte o codelab de layouts.

API dissociada

No exemplo de ConstraintLayout as restrições são especificadas inline, com um modificador na função que pode ser composta a que elas são aplicadas. No entanto, há situações em que é preferível dissociar as restrições dos layouts aos que elas se aplicam. Por exemplo, talvez você queira mudar as restrições com base na configuração da tela ou colocar uma animação entre dois conjuntos de restrições.

Para casos como esses, é possível usar ConstraintLayout de uma maneira diferente:

  1. Transmita um ConstraintSet como um parâmetro para ConstraintLayout.
  2. Atribua referências criadas no ConstraintSet aos elementos que podem ser compostos usando o modificador layoutId.
@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin = margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

Assim, quando precisar mudar as restrições, basta transmitir um ConstraintSet diferente.

Layouts personalizados

No Compose, os elementos de IU são representados pelas funções que podem ser compostas, que emitem uma parte da IU quando invocadas. Essa parte é, então, adicionadas a uma árvore da IU e renderizada na tela. Cada elemento da IU tem um pai e, possivelmente, muitos filhos. Cada elemento também está localizado no pai, especificado como uma posição (x, y) e um tamanho, especificado como width e um height.

Os pais definem as restrições dos elementos filhos. O tamanho do elemento é definido dentro dessas restrições. As restrições definem os valores mínimo e máximo de width e height dos elementos. Caso um elemento tenha elementos filhos, ele poderá medir cada um para ajudar a determinar o tamanho dele. Depois que um elemento determina e informa o próprio tamanho, ele tem a oportunidade de definir o posicionamento dos elementos filhos em relação a ele mesmo, conforme descrito mais detalhadamente em Como criar layouts personalizados.

A medição de passagem única é boa para o desempenho, permitindo que o Compose processe árvores de IU profundas com eficiência. Se um elemento de layout medisse o filho duas vezes e esse filho medisse um filho dele duas vezes, e assim por diante, uma única tentativa de dispor uma IU inteira tomaria muito trabalho, dificultando a manutenção do bom desempenho do app. No entanto, há momentos em que você realmente precisa de mais informações além do que uma única medição do filho oferece. Existem abordagens para lidar com uma situação como essa, que são discutidas na seção Medidas intrínsecas.

Como usar modificadores de layout

É possível usar o modificador layout para mudar a forma como um elemento é medido e disposto. Layout é um lambda. Os parâmetros dele incluem o elemento que você pode medir, transmitido como measurable, e as restrições desse elemento, transmitidas como constraints. Um modificador de layout personalizado pode ser assim:

fun Modifier.customLayoutModifier(...) =
    this.layout { measurable, constraints ->
        ...
    })

Veja a exibição de um Text na tela e controle a distância da parte superior até a linha de base da primeira linha do texto. Isso é exatamente o que o modificador paddingFromBaseline faz, implementado aqui como um exemplo. Para fazer isso, use o modificador layout para colocar manualmente a função que pode ser composta na tela. Este é o comportamento esperado em que o padding superior do Text é definido como 24.dp:

Mostra a diferença entre o preenchimento normal da IU, que define o espaço entre os elementos, e o preenchimento de texto que define o espaço de uma linha de base até a próxima

Veja o código que produz esse espaçamento:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

Veja o que está acontecendo nesse código:

  1. No parâmetro lamdba measurable, o Text representado pelo parâmetro mensurável é medido ao chamar measurable.measure(constraints).
  2. Para especificar o tamanho da função que pode ser composta, chame o método layout(width, height), que também fornece um lambda usado para posicionar os elementos agrupados. Nesse caso, ele é a altura entre a última linha de base do texto e o padding superior adicionado.
  3. Posicione os elementos agrupados na tela chamando placeable.place(x, y). Se os elementos agrupados não forem posicionados, eles não ficarão visíveis. A posição y corresponde ao padding superior, que é a posição da primeira linha de base do texto.

Para verificar se isso funciona como esperado, use este modificador em um Text:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

Várias visualizações de elementos do texto. Uma mostra o preenchimento comum entre os elementos, a outra mostra o preenchimento de uma linha de base até a próxima

Como criar layouts personalizados

O modificador layout só muda o elemento que é autor da chamada. Para medir e definir o layout de vários elementos, use o comando Layout. Com ele, é possível medir e dispor os filhos manualmente. Todos os layouts de nível superior, como Column e Row, são criados com o Layout que pode ser composto.

Vamos criar uma versão muito simples da Column. A maioria dos layouts personalizados segue este padrão:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

Assim como o modificador layout, measurables é a lista de filhos que precisam ser medidos e constraints são as restrições do pai. Seguindo a mesma lógica de antes, MyBasicColumn pode ser implementada da seguinte forma:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

As funções filhas são limitadas pelas restrições de Layout (sem as restrições de minHeight) e são posicionadas de acordo com a yPosition da função que pode ser composta anterior.

Veja como esse elemento personalizado que pode ser composto seria usado:

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

Vários elementos de texto empilhados um em cima do outro em uma coluna.

Direção do layout

Para mudar a direção do layout de uma função que pode ser composta, mude a LocalLayoutDirection compositionLocal.

Se você posicionar os elementos que podem ser compostos manualmente na tela, o LayoutDirection será parte do LayoutScope do modificador layout ou do elemento Layout que pode ser composto.

Ao usar layoutDirection, posicione os elementos que podem ser compostos usando place. Ao contrário do método place, place não muda de acordo com a direção de leitura (esquerda para a direita ou direita para a esquerda).

Medições intrínsecas

Uma das regras do Compose é que os filhos precisam ser medidos somente uma vez. Medir os filhos duas vezes gera uma exceção de tempo de execução. No entanto, há momentos em que você precisa de algumas informações sobre os filhos antes de medi-los.

Com a medição intrínseca, é possível consultar os elementos filhos antes que eles sejam realmente medidos.

Para uma função que pode ser composta, você pode solicitar intrinsicWidth ou intrinsicHeight:

  • (min|max)IntrinsicWidth: considerando essa altura, quais são as larguras mínima e máxima para que o conteúdo seja pintado corretamente?
  • (min|max)IntrinsicHeight: considerando essa largura, quais são as alturas mínima e máxima para que o conteúdo seja pintado corretamente?

Por exemplo, se você solicitar minIntrinsicHeight de um Text com width infinita, ela retornará a height do Text como se o texto tivesse sido desenhado em uma única linha.

Medições intrínsecas em ação

Imagine que queremos criar um elemento que pode ser composto que exibe dois textos na tela, separados por um divisor como este:

Dois elementos de texto lado a lado, com um divisor vertical entre eles

Como podemos fazer isso? Podemos ter uma Row com dois Texts que se expandem o máximo possível e um Divider no meio. O divisor precisa ter a mesma altura que o Text mais alto e ser fino (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)

                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

Na visualização, veremos que o divisor é expandido para a tela inteira e esse não é o resultado esperado:

Dois elementos de texto lado a lado, com um divisor entre eles, mas o divisor se expande para baixo da parte inferior do texto

Isso acontece porque Row mede cada filho individualmente, e a altura de Text não pode ser usada para limitar o Divider. O objetivo é que Divider preencha o espaço disponível com uma altura definida. Para isso, podemos usar o modificador height(IntrinsicSize.Min)

height(IntrinsicSize.Min) dimensiona os filhos, para que a altura deles seja igual à altura intrínseca mínima. Por ser recursivo, ele consultará a minIntrinsicHeight da Row e das filhas dela.

Aplicando isso ao código, ele funcionará como esperado:

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.preferredHeight(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
        Text(
            modifier = Modifier
                .weight(1f)

                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    LayoutsCodelabTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

Com a visualização:

Dois elementos de texto lado a lado, com um divisor vertical entre eles

A minIntrinsicHeight que pode ser composta da Row será a minIntrinsicHeight máxima das filhas dela. A Divider element's minIntrinsicHeight é 0 porque nele não ocupa espaço se nenhuma restrição for estabelecida. A minIntrinsicHeight do Text será a mesma do texto que recebeu uma width específica. Portanto, a restrição de height do elemento Row será a minIntrinsicHeight máxima dos Texts. O Divider expandirá a height dele para a restrição de height especificada pela Row.

Saiba mais

Para saber mais, consulte o codelab Layouts no Jetpack Compose(link em inglês).