Criar layouts adaptáveis

A IU do app precisa ser responsiva para considerar diferentes tamanhos de tela, orientações e formatos. Um layout adaptável muda com base no espaço de tela disponível. Essas mudanças variam de ajustes de layout simples para preencher espaço até mudanças completas de layout para usar mais espaço.

Como um kit de ferramentas de IU declarativo, o Jetpack Compose é adequado para projetar e implementar layouts que se ajustam para renderizar conteúdo de maneira diferente em diversos tamanhos. Este documento contém algumas diretrizes sobre como você pode usar o Compose para tornar a IU responsiva.

Explicitar grandes mudanças de layout para elementos raiz que podem ser compostos

Ao usar o Compose para criar o layout de um app inteiro, os elementos que podem ser compostos no nível raiz ocupam todo o espaço fornecido pelo app. Nesse nível do design, faz sentido mudar o layout geral de uma tela para aproveitar as telas maiores.

Evite usar valores físicos e de hardware para tomar decisões de layout. Pode ser tentador tomar decisões com base em um valor tangível fixo (o dispositivo é um tablet? A tela física tem determinada proporção?), mas as respostas a essas perguntas podem não ser úteis para determinar o espaço com que a IU pode trabalhar.

Um diagrama mostrando vários formatos de dispositivos diferentes: um smartphone, um dobrável, um tablet e um laptop

Em tablets, um app pode ser executado no modo de várias janelas, o que significa que ele pode compartilhar a tela com outro app. No Chrome OS, um app pode estar em uma janela redimensionável. Pode haver até mais de uma tela física, como em um dispositivo dobrável. Em todos esses casos, o tamanho da tela física não é relevante para decidir como exibir conteúdo.

Em vez disso, tome decisões com base na parte real da tela alocada para o app, como as métricas de janela atuais fornecidas pela biblioteca WindowManager do Jetpack. Para ver como usar o WindowManager em um app do Compose, confira o app JetNews de exemplo.

Essa abordagem tornará seu app mais flexível, porque ele se comportará bem em todos os cenários acima. Tornar seus layouts adaptáveis ao espaço de tela disponível para eles também reduz a quantidade de processamento especial para oferecer suporte a plataformas como o Chrome OS e a formatos como tablets e dispositivos dobráveis.

Ao observar o espaço relevante disponível para o app, é útil converter o tamanho bruto em uma classe de tamanho significativa, conforme descrito em Classes de tamanho de janela. Esse processo agrupa os tamanhos em buckets de tamanho padrão, que são pontos de interrupção projetados para equilibrar a simplicidade com a flexibilidade de otimizar o app para a maioria dos casos. Essas classes de tamanho se referem à janela geral do app. Portanto, use essas classes para decisões de layout que afetam o layout geral da tela. É possível transmitir essas classes de tamanho como estado ou executar uma lógica adicional para criar um estado derivado a fim de transmitir para elementos aninhados que podem ser compostos.

enum class WindowSizeClass { Compact, Medium, Expanded }

@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass != WindowSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Essa abordagem em camadas limita a lógica do tamanho da tela a um único local, em vez de distribuí-la pelo app em muitos lugares que precisam ser sincronizados. Esse único local produz um estado, que pode ser transmitido explicitamente para outros elementos que podem ser compostos, assim como você faria para qualquer outro estado do app. A transmissão explícita do estado simplifica os elementos não raiz compostos, já que eles serão apenas funções que podem ser compostas normais que usam a classe de tamanho ou a configuração especificada com outros dados.

Composições aninhadas flexíveis são reutilizáveis

Os elementos compostos são mais reutilizáveis quando podem ser colocados em uma grande variedade de lugares. Se um elemento composto presumir que será sempre colocado em determinado local com um tamanho específico, será mais difícil reutilizá-lo em outro local ou com uma quantidade diferente de espaço disponível. Isso também significa que elementos não raiz compostos e reutilizáveis devem ser evitados implicitamente, dependendo das informações de tamanho "globais".

Vejamos um exemplo: imagine um elemento composto aninhado que implemente um layout de detalhes da lista, que pode mostrar um ou dois painéis lado a lado.

Captura de tela de um app mostrando dois painéis lado a lado

Captura de tela de um app mostrando um layout típico de lista/detalhes. 1 é a área de lista e 2 é a área de detalhes.

Queremos que essa decisão faça parte do layout geral do app. Assim, transmitimos a decisão de um elemento que pode ser composto no nível raiz, como visto acima:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

E se você quiser que uma função que pode ser composta mude o layout de forma independente, com base no espaço disponível? Por exemplo, um card que quer mostrar detalhes adicionais, se houver espaço. Queremos executar uma lógica com base em um tamanho disponível, mas em qual tamanho especificamente?

Exemplos de dois cards diferentes: um card estreito que mostra somente um ícone e um título e um card mais amplo com o ícone, o título e uma breve descrição

Como vimos acima, precisamos evitar o uso do tamanho da tela do dispositivo. Esse tamanho não será preciso para várias telas, nem se o app não estiver em tela cheia.

Como o elemento que pode ser composto não está no nível raiz, também não devemos usar as métricas da janela atual diretamente para maximizar a reutilização. Se o componente for colocado com padding (como para encartes) ou se houver componentes como colunas de navegação ou barras de apps, a quantidade de espaço disponível para o elemento composto pode ser significativamente diferente do espaço geral disponível para o app.

Portanto, precisamos usar a largura em que o elemento composto é realmente renderizado. Temos duas opções para conseguir essa largura:

Caso queira mudar onde ou como o conteúdo é exibido, use um conjunto de modificadores ou um layout personalizado para tornar o layout responsivo. Isso pode ser feito de forma simples, por exemplo, preenchendo todo o espaço disponível com um filho ou criando o layout de filhos com várias colunas, se houver espaço suficiente.

Caso queira mudar o que você mostra, use BoxWithConstraints como uma alternativa mais eficiente. Esse elemento composto oferece restrições de medição que podem ser usadas para chamar diferentes elementos com base no espaço disponível. No entanto, isso tem algumas desvantagens, porque BoxWithConstraints adia a composição até a fase do layout, quando essas restrições são conhecidas, fazendo com que mais trabalho seja realizado durante o layout.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Garantir que todos os dados estejam disponíveis para diferentes tamanhos

Ao aproveitar melhor o espaço extra, talvez seja possível exibir mais conteúdo para o usuário em uma tela grande do que em uma tela pequena. Ao implementar um elemento composto com esse comportamento, pode ser tentador fazer isso e carregar dados como um efeito colateral do tamanho atual.

No entanto, isso vai contra os princípios do fluxo de dados unidirecional, em que os dados podem ser elevados e simplesmente fornecidos a elementos que podem ser compostos e renderizados corretamente. Dados suficientes precisam ser fornecidos ao elemento que pode ser composto para que ele sempre tenha o necessário para ser exibido em qualquer tamanho, mesmo que parte dos dados nem sempre seja usada.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Com base no exemplo de Card, observe que sempre transmitimos a description para o Card. Embora a description seja usada apenas quando a largura permite a exibição, o Card sempre precisa dela, independentemente da largura disponível.

A transmissão contínua de dados deixa os layouts adaptáveis mais simples, tornando-os menos com estado. Também evita o acionamento de efeitos colaterais ao alternar entre tamanhos, o que pode ocorrer devido a redimensionamento de janela, mudança de orientação ou dobramento e desdobramento de um dispositivo.

Esse princípio também permite preservar o estado em todas as mudanças de layout. Ao elevar informações que não podem ser usadas em todos os tamanhos, é possível preservar o estado do usuário conforme o tamanho do layout muda. Por exemplo, podemos elevar uma sinalização booleana showMore para que o estado do usuário seja preservado quando o redimensionamento fizer o layout alternar entre ocultar e exibir a descrição:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Saiba mais

Para saber mais sobre layouts personalizados no Compose, consulte os recursos abaixo.

Apps de exemplo

  • O JetNews mostra como projetar um app que adapta a IU para usar o espaço disponível.

Vídeos