Ofereça suporte a 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, seja em diferentes telas de dispositivos ou em diferentes janelas de apps no modo de várias janelas, crie layouts 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 acomodará smartphones, tablets, dispositivos dobráveis, dispositivos ChromeOS, orientações de retrato e paisagem e configurações de tela redimensionáveis, como o modo de tela dividida e janelas de área de trabalho.

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 tamanhos de tela diferentes.

Explicitar grandes mudanças de layout para elementos combináveis de conteúdo

Os elementos combináveis do app e do conteúdo ocupam todo o espaço de exibição disponível para o app. Para esses tipos de elementos combináveis, pode ser interessante mudar o layout geral do app em telas grandes.

Evite usar valores de hardware físico 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 disponível para a interface.

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 dividir a tela com outro app. No modo de janelas para computador ou 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, descrita pelas métricas de janela atuais fornecidas pela biblioteca WindowManager do Jetpack. Para conferir um exemplo de como usar o WindowManager em um app do Compose, consulte o exemplo JetNews.

Tornar seus layouts adaptáveis ao espaço de exibição disponível também reduz a quantidade de processamento especial necessária para oferecer suporte a plataformas como o ChromeOS e formatos como tablets e dispositivos dobráveis.

Depois de determinar as métricas do espaço disponível para o app, converta o tamanho bruto em uma classe de tamanho de janela, conforme descrito em Usar classes de tamanho de janela. As classes de tamanho de janela são pontos de interrupção projetados para equilibrar a simplicidade da lógica do app com a flexibilidade de otimizar o app para a maioria dos tamanhos de tela. As classes de tamanho de janela se referem à janela geral do app. Portanto, use as classes para decisões de layout que afetam o layout geral do app. É possível transmitir classes de tamanho de janela 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,
        /* ... */
    )
}

Uma abordagem em camadas limita a lógica do tamanho de exibição a um único local, em vez de distribuí-la pelo app em muitos lugares que precisam ser sincronizados. Um ú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 elementos combináveis individuais, já que eles usam a classe de tamanho da janela 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 combinável precisar ser colocado em um local específico com um tamanho específico, é improvável que ele seja reutilizável em outros contextos. 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 de exibição global.

Imagine um elemento combinável aninhado que implemente um layout de detalhes da lista, que pode mostrar um único painel ou dois painéis lado a lado:

Um app mostrando dois painéis lado a lado.
Figura 2. App mostrando um layout típico de detalhes e listas: 1 é a área da lista e 2 é a área de detalhes.

A decisão de detalhes e listas precisa fazer parte do layout geral do app. Por isso, a decisão é transmitida de um elemento combinável no nível de conteúdo:

@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 de exibição disponível, por exemplo, um card que mostra mais detalhes, se houver espaço? Você quer executar uma lógica com base em um tamanho de tela disponível, mas em qual tamanho especificamente?

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.

Evite usar o tamanho da tela real do dispositivo. Esse tamanho não é exato para diferentes tipos de telas e também não é preciso se o app não estiver em tela cheia.

Como o elemento combinável não está no nível do conteúdo, não use as métricas da janela atual diretamente. Se o componente for colocado com padding (como com encartes) ou se o app incluir componentes como colunas de navegação ou barras de apps, a quantidade de espaço de exibição disponível para o elemento combinável pode ser significativamente diferente do espaço geral disponível para o app.

Use a largura em que o elemento combinável é realmente renderizado. Você tem 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. BoxWithConstraints oferece restrições de medição que podem ser usadas para chamar diferentes elementos combináveis com base no espaço de exibiçã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 de tela

Ao implementar um elemento combinável que aproveita o espaço de exibição extra, pode ser tentador fazer isso e carregar dados como um efeito colateral do tamanho atual.

No entanto, isso vai contra o princípio 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 conteúdo suficiente para qualquer tamanho de tela, mesmo que parte do conteúdo 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 a description é sempre transmitida para o Card. Embora a description seja usada apenas quando a largura permite a exibição, o Card sempre precisa da description, independentemente da largura disponível.

A transmissão contínua de conteúdo simplifica os layouts adaptáveis, tornando-os menos "com estado", e evita o acionamento de efeitos colaterais ao alternar entre tamanhos de tela, o que pode ocorrer devido a redimensionamento de janela, mudança de orientação ou dobra 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 de tela, é possível preservar o estado do app conforme o tamanho do layout muda. Por exemplo, é possível elevar uma flag booleana showMore para que o estado do app seja preservado quando o redimensionamento da tela faz com que o layout alterne entre ocultar e mostrar o conteúdo:

@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 adaptáveis no Compose, consulte os seguintes recursos:

Apps de exemplo

  • CanonicalLayouts é um repositório de padrões de design comprovados que oferecem uma experiência do usuário ótima em telas grandes.
  • O JetNews mostra como projetar um app que adapta a interface para usar o espaço de exibição disponível.
  • Reply é uma amostra adaptativa para oferecer suporte a dispositivos móveis, tablets e dobráveis.
  • O Now in Android é um app que usa layouts adaptáveis para oferecer suporte a diferentes tamanhos de tela.

Vídeos