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. Isso permite que o app acomode smartphones, tablets, dispositivos dobráveis, 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 de acordo com o espaço de exibição disponível. As mudanças vão desde pequenos ajustes de layout que preenchem o espaço (design responsivo) até a substituição completa de um layout por outro para que o app possa acomodar diferentes tamanhos de tela (design adaptável).

Como um kit de ferramentas de IU declarativa, o Jetpack Compose é ideal para projetar e implementar layouts que mudam dinamicamente para renderizar conteúdo de maneira diferente em diversos 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 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 diferentes, incluindo smartphone, dobrável, tablet e laptop
Figura 1. Formatos de smartphone, dobrável, tablet e laptop

Em tablets, um app pode estar sendo executado no modo de várias janelas, o que significa que ele pode estar dividindo 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 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 ChromeOS 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 Usar 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 simplicidade e flexibilidade de modo a otimizar o app para a maioria dos casos exclusivos. 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 combináveis aninhados.

@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 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 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, é 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 precisam ser evitados de forma implícita, 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 da 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 uma função combinável 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.
Figura 3. Card estreito que mostra apenas um ícone e um título e um card mais amplo que mostra 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 é exato nem para várias telas e nem 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 uma coleção 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 combináveis com base no espaço disponível. No entanto, isso tem algumas desvantagens, porque a função BoxWithConstraints adiaria 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 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 renderização adequada. Dados suficientes precisam ser fornecidos ao elemento combinável 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 simplifica os layouts adaptáveis, 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, a mudança de orientação ou à dobra e o 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, podemos preservar o estado do usuário à medida que o tamanho do layout muda. Por exemplo, podemos elevar uma flag booleana showMore para que o estado do usuário seja preservado quando o redimensionamento fizer o layout alternar 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

  • CanonicalLayouts é 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 interface para usar o espaço disponível.
  • Reply é uma amostra adaptativa para oferecer suporte a smartphones, tablets e dispositivos dobráveis.
  • O Now in Android é um app que usa layouts adaptáveis para oferecer suporte a diferentes tamanhos de tela.

Vídeos