Compatibilidade com diferentes tamanhos de tela

O suporte a diferentes tamanhos de tela permite que o app seja acessado pela maior variedade de dispositivos e pelo maior número de usuários.

Para oferecer suporte ao maior número possível de tamanhos de tela, crie layouts de app responsivos e adaptáveis. Os layouts responsivos/adaptáveis proporcionam uma experiência do usuário otimizada, independente do tamanho da tela, permitindo que o app acomode smartphones, tablets, dobráveis, dispositivos ChromeOS, orientações de retrato e paisagem e configurações redimensionáveis, como o modo de várias janelas.

Os layouts responsivos/adaptáveis mudam com base no espaço de exibição disponível. As mudanças variam de pequenos ajustes de layout que preenchem espaço (design responsivo) à substituição completa de um layout por outro, para que o app possa acomodar melhor diferentes tamanhos de tela (design adaptativo).

Como um kit de ferramentas de interface declarativo, o Jetpack Compose é ideal para projetar e implementar layouts que mudam dinamicamente para renderizar conteúdo de maneira diferente em vários tamanhos de tela.

Explicitar grandes mudanças de layout para elementos combináveis da tela

Ao usar o Compose para criar o layout de um app inteiro, os elementos combináveis do app e da tela ocupam todo o espaço para renderização fornecido pelo app. Nesse nível do projeto, pode fazer sentido mudar o layout geral de uma tela para aproveitar melhor o espaço disponível em 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 uma determinada proporção?), mas as respostas a essas perguntas podem não ser úteis para determinar o espaço com que a interface pode trabalhar.

Um diagrama mostrando vários formatos de dispositivos, incluindo smartphone, dobrável, tablet e laptop.
Figura 1. Formatos de smartphones, dobráveis, tablets e laptops

Em tablets, um app pode estar sendo executado no modo de várias janelas, o que significa que ele pode dividir a tela com outro app. No ChromeOS, 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 mostrar 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 os 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 ChromeOS e a formatos como tablets e dobráveis.

Quando estiver observando 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. Ele agrupa tamanhos em buckets de tamanho padrão, que são pontos de interrupção projetados para equilibrar simplicidade com a flexibilidade de otimizar o app para a maioria dos casos exclusivos. Essas classes de tamanho se referem à janela geral do app. Portanto, use-as 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 combináveis aninhados.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.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 de tamanho da tela a um único local, em vez de espalhá-la pelo app em muitos lugares que precisam ser sincronizados. Esse único local produz um estado, que pode ser transmitido explicitamente para outros elementos combináveis, assim como você faria para qualquer outro estado do app. A transmissão explícita do estado simplifica os elementos combináveis individuais, já que eles vão ser apenas funções combináveis normais que usam a classe de tamanho ou a configuração especificada com outros dados.

Elementos combináveis aninhados 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 combinável presume 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 combináveis individuais e reutilizáveis devem ser evitados implicitamente, dependendo das informações de tamanho "globais".

Considere o exemplo a seguir: imagine um elemento combinável 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.
Figura 2. Captura de tela de um app mostrando um layout típico de detalhes e listas: 1 é a área de lista, 2 é a área de detalhes.

Queremos que essa decisão faça parte do layout geral do app. Por isso, é necessário transmitir a decisão de um elemento combinável da tela, conforme mostrado acima:

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

E se você quiser que um elemento combinável mude o layout de forma independente, com base no espaço disponível? Por exemplo, um card que quer mostrar mais detalhes se houver espaço. Queremos executar alguma lógica com base em algum tamanho disponível, mas em qual tamanho especificamente?

Exemplos de dois cards diferentes.
Figura 3. Cartão estreito que mostra apenas um ícone e título e um card mais largo mostrando o ícone, o título e a descrição breve.

Como vimos acima, devemos evitar tentar usar o tamanho da tela real do dispositivo. Isso não será preciso para várias telas e também não será preciso se o app não estiver em tela cheia.

Como o elemento combinável não está no nível da tela, também não podemos 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 combinável 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:

Se você quiser 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.

Se quiser mudar o que você mostra, use BoxWithConstraints como uma alternativa mais eficiente. Esse elemento combinável 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 despesas, já que BoxWithConstraints adia a composição até a fase de 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 combinável 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 fornecidos a elementos combináveis para que sejam renderizados corretamente. Dados suficientes precisam ser fornecidos ao elemento combinável para que ele sempre tenha o que precisa 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 simplifica os layouts adaptáveis, tornando-os menos com estado, e 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 dobrar e desdobrar 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, podemos 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 os redimensionamentos fizerem com que o layout alterne entre ocultar e mostrar 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

  • Os layouts canônicos de telas grandes são um repositório de padrões de design comprovados que oferecem uma experiência do usuário ideal em dispositivos de tela grande.
  • O JetNews mostra como projetar um app que adapta a IU para usar o espaço disponível.
  • O Reply é um exemplo adaptável para compatibilidade com dispositivos móveis, tablets e dobráveis
  • O Now in Android (link em inglês) é um app que usa layouts adaptáveis para oferecer suporte a diferentes tamanhos de tela.

Vídeos