Usar o Compose para adicionar layouts adaptáveis a um app Android baseado em visualização

1. Antes de começar

Existem dispositivos Android de todos os formatos e tamanhos. Por isso, você precisa adequar o layout do seu app a diferentes tamanhos de tela para que ele seja disponibilizado a usuários e dispositivos variados com um único pacote Android (APK) ou Android App Bundle (AAB). Para isso, é necessário definir o app com um layout responsivo e adaptável, em vez de usar dimensões estáticas que pressuponham um determinado tamanho de tela ou proporção. Os layouts adaptáveis mudam de acordo com o espaço de tela disponível.

Este codelab ensina os conceitos básicos de como criar IUs adaptáveis e ajustar um app que mostra uma lista de esportes e detalhes sobre cada um deles de modo a oferecer suporte a dispositivos com telas grandes. O app esportivo consiste em três telas: inicial, de favoritos e de configurações. A tela inicial mostra uma lista de esportes e um marcador de posição para notícias quando você seleciona um esporte na lista. As telas de favoritos e de configurações também mostram textos de marcador de posição. Para trocar de tela, basta selecionar o item associado no menu de navegação da parte de baixo.

A princípio, o app tem estes problemas de layout em telas grandes:

  • Ele não pode ser usado na orientação retrato.
  • Ele mostra muitos espaços em branco em telas grandes.
  • Ele sempre mostra o menu de navegação da parte de baixo em telas grandes.

Ao tornar o app adaptável, você permite que ele:

  • tenha suporte à orientação paisagem e retrato;
  • mostre a lista de esportes e notícias sobre cada esporte lado a lado quando houver espaço horizontal suficiente;
  • mostre o componente de navegação de acordo com as diretrizes do Material Design.

O app tem apenas uma atividade com vários fragmentos. Você vai usar estes arquivos:

  • O arquivo AndroidManifest.xml, que fornece os metadados sobre o app esportivo.
  • O arquivo MainActivity.kt, que contém um código gerado com o arquivo activity_main.xml, a anotação override, uma classe enum que representa a classe de tamanho de janela para a largura e uma definição de método para recuperar essa classe para a janela do app. O menu de navegação da parte de baixo da tela é inicializado quando a atividade é criada.
  • O arquivo activity_main.xml, que define o layout padrão da atividade Main.
  • O arquivo layout-sw600dp/activity_main.xml, que define um layout alternativo para a atividade Main. O layout alternativo é eficaz quando a largura da janela do app é maior que ou igual a um valor de 600dp. O conteúdo é igual ao layout padrão no ponto de partida.
  • O arquivo SportsListFragment.kt, que contém a implementação da lista de esportes e a navegação de retorno personalizada.
  • O arquivo fragment_sports_list.xml, que define um layout para a lista de esportes.
  • O arquivo navigation_menu.xml, que define os itens do menu de navegação da parte de baixo da tela.

O app esportivo mostra uma lista de esportes em uma janela compacta com uma barra de navegação como componente da parte de cima da tela. O app esportivo mostra uma lista de esportes e notícias esportivas lado a lado em uma janela média. Uma coluna de navegação aparece como componente na parte de cima da tela. O app esportivo mostra a gaveta de navegação, uma lista de esportes e as notícias em uma janela de tamanho expandido.

Figura 1. O app esportivo tem suporte a diferentes tamanhos de janela com um único APK ou AAB.

Pré-requisitos

  • Conhecimento básico de desenvolvimento de IU baseado em visualização.
  • Experiência com a sintaxe do Kotlin, incluindo funções lambda.
  • Conclusão do codelab Noções básicas do Jetpack Compose.

O que você vai aprender

  • Como oferecer suporte a mudanças de configuração.
  • Como adicionar layouts alternativos com menos modificações de código.
  • Como implementar uma IU de detalhes da lista que se comporte de maneira diferente para tamanhos de janela variados.

O que você vai criar

Um app Android com suporte a:

  • orientação paisagem do dispositivo;
  • tablets, computadores e dispositivos móveis;
  • comportamento de detalhes da lista para diferentes tamanhos de tela.

O que é necessário

2. Começar a configuração

Faça o download do código para este codelab e configure o projeto:

  1. Na linha de comando, clone o código deste repositório do GitHub (link em inglês):
$ git clone https://github.com/android/add-adaptive-layouts
  1. No Android Studio, abra o projeto AddingAdaptiveLayout. Ele está disponível em várias ramificações do GitHub:
  • A ramificação main contém o código inicial para este projeto. Você vai fazer edições nessa ramificação para concluir o codelab.
  • A ramificação end contém a solução deste codelab.

3. Migrar o componente de navegação da parte de cima da tela para o Compose

O app esportivo usa um menu de navegação na parte de baixo da tela como componente da parte de cima. Esse componente é implementado com a classe BottomNavigationView. Nesta seção, você vai migrar o componente de navegação da parte de cima para o Compose.

Representar o conteúdo do recurso de menu associado como uma classe selada

  1. Crie uma classe MenuItem selada para representar o conteúdo do arquivo navigation_menu.xml e, em seguida, transmita um parâmetro iconId, labelId e destinationId.
  2. Adicione um objeto Home, Favorite e Settings como subclasses que correspondam aos seus destinos.
sealed class MenuItem(
    // Resource ID of the icon for the menu item
    @DrawableRes val iconId: Int,
    // Resource ID of the label text for the menu item
    @StringRes val labelId: Int,
    // ID of a destination to navigate users
    @IdRes val destinationId: Int
) {

    object Home: MenuItem(
        R.drawable.ic_baseline_home_24,
        R.string.home,
        R.id.SportsListFragment
    )

    object Favorites: MenuItem(
        R.drawable.ic_baseline_favorite_24,
        R.string.favorites,
        R.id.FavoritesFragment
    )

    object Settings: MenuItem(
        R.drawable.ic_baseline_settings_24,
        R.string.settings,
        R.id.SettingsFragment
    )
}

Implementar o menu de navegação da parte de baixo da tela como uma função de composição

  1. Defina uma função de composição BottomNavigationBar que use estes três parâmetros: um objeto menuItems definido com um valor List<MenuItem>, um modifier definido com um valor Modifier = Modifier e um onMenuSelected definido como uma função lambda (MenuItem) -> Unit = {}.
  2. No corpo da função de composição BottomNavigationBar, chame uma função NavigationBar() que use um parâmetro modifier.
  3. Na função lambda transmitida para a função NavigationBar(), chame o método forEach() no parâmetro menuItems e, em seguida, chame uma função NavigationBarItem() na função lambda definida como a chamada de método foreach().
  4. Transmita para a função NavigationBarItem() um parâmetro selected definido com um valor false; um onClick definido como uma função lambda que contém uma função onMenuSelected com um parâmetro MenuItem; um icon definido como uma função lambda que contém uma função Icon com um parâmetro painter = painterResource(id = menuItem.iconId) e um contentDescription = null; e um label definido como uma função lambda que contém uma função Text com um parâmetro (text = stringResource(id = menuItem.labelId).
@Composable
fun BottomNavigationBar(
    menuItems: List<MenuItem>,
    modifier: Modifier = Modifier,
    onMenuSelected: (MenuItem) -> Unit = {}
) {

    NavigationBar(modifier = modifier) {
        menuItems.forEach { menuItem ->
            NavigationBarItem(
                selected = false,
                onClick = { onMenuSelected(menuItem) },
                icon = {
                    Icon(
                        painter = painterResource(id = menuItem.iconId),
                        contentDescription = null)
                },
                label = { Text(text = stringResource(id = menuItem.labelId))}
            )
        }
    }
}

Substituir o elemento BottomNavigationView pelo ComposeView no arquivo de recurso de layout

  • No arquivo activity_main.xml, substitua o elemento BottomNavigationView pelo elemento ComposeView. Essa mudança incorpora o componente de navegação da parte de baixo da tela a um layout de IU baseado em visualização usando o Compose.

activity_main.xml

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment_content_main"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/top_navigation"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/nav_graph" />

        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/navigation"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/top_navigation" />
    </androidx.constraintlayout.widget.ConstraintLayout>

Integrar o menu de navegação da parte de baixo da tela baseado no Compose a um layout de IU com base em visualização

  1. No arquivo MainActivity.kt, defina uma variável navigationMenuItems configurada como uma lista de objetos MenuItem. Esses objetos MenuItems aparecem no menu de navegação da parte de baixo da tela na ordem da lista.
  2. Chame a função BottomNavigationBar() para incorporar o menu de navegação da parte de baixo da tela ao objeto ComposeView.
  3. Navegue até o destino associado ao item selecionado pelos usuários na função de callback transmitida para a função BottomNavigationBar.

MainActivity.kt

val navigationMenuItems = listOf(
    MenuItem.Home,
    MenuItem.Favorites,
    MenuItem.Settings
)

binding.navigation.setContent {
    MaterialTheme {
        BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
            navController.navigate(screen.destinationId)
        }
    }
}

4. Suporte à orientação horizontal

Caso seu app tenha suporte a um dispositivo de tela grande, é esperado que ele funcione na orientação paisagem e retrato. No momento, o app tem apenas uma atividade: MainActivity.

A orientação da tela da atividade é definida no arquivo AndroidManifest.xml com o atributo android:screenOrientation, configurado com o valor portrait.

Defina o app para que ele tenha suporte à orientação paisagem:

  1. Defina o atributo android:screenOrientation com o valor fullUser. Com essa configuração, os usuários podem bloquear a orientação da tela. A orientação é determinada pelo sensor do dispositivo para qualquer uma das quatro opções possíveis.

AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:screenOrientation="fullUser">

O app esportivo oferece suporte à orientação horizontal definindo o valor fullUser como o atributo android:screenOrientation de um elemento de atividade no arquivo AndroidManifest.xml.

Figura 2. O app é executado na orientação horizontal depois que você atualiza o arquivo AndroidManifest.xml.

5. Classes de tamanho de janela

Esses são os valores de ponto de interrupção que ajudam a classificar o tamanho da janela em classes predefinidas (compacta, média e expandida) com o tamanho bruto da janela disponível para o app. Essas classes de tamanho seriam usadas ao projetar, desenvolver e testar o layout adaptável.

A largura e altura disponíveis são particionadas de forma independente para que o app seja sempre associado a duas classes de tamanho de janela: uma para a largura e outra para a altura.

Há dois pontos de interrupção entre três classes de tamanho de janela. Um valor de 600 dp é o ponto de interrupção entre compacta e média, enquanto um de 840 dp é aquele entre as classes de tamanho de janela com largura média e expandida.

Figura 3. Classes de tamanho de janela para a largura e pontos de interrupção associados.

Há dois pontos de interrupção para três classes de tamanho de janela para a altura. Um valor de 480 dp é o que está entre as classes de tamanho de janela com altura compacta e média, enquanto um valor de 900 dp é aquele entre as classes de tamanho de janela com altura média e expandida.

Figura 4. As classes de tamanho de janela para a altura e os pontos de interrupção associados.

As classes de tamanho de janela representam o tamanho atual da janela do app. Em outras palavras, não é possível determinar a classe de tamanho de janela pelo tamanho do dispositivo físico. Ainda que seu app seja executado no mesmo dispositivo, a classe de tamanho de janela associada vai mudar de acordo com a configuração, como quando você executa o app no modo de tela dividida. Isso tem duas consequências importantes:

  • Os dispositivos físicos não garantem uma classe de tamanho de janela específica.
  • A classe de tamanho de janela pode mudar durante o ciclo de vida do app.

Depois de adicionar layouts adaptáveis ao app, você o testa em todas as variações, especialmente em classes de tamanho de janela compacta, média e expandida. Os testes para cada classe de tamanho de janela são necessários, mas não suficientes em muitos casos. É importante testar o app em vários tamanhos de janela para garantir que a IU seja dimensionada corretamente. Para ver mais informações, consulte Layouts em tela grande (link em inglês) e Qualidade dos apps em telas grandes.

6. Posicionar os painéis de lista e de detalhes lado a lado em telas grandes

Uma IU de detalhes da lista pode precisar ter um comportamento diferente de acordo com a classe de tamanho da janela para largura definida atualmente. Quando a classe de tamanho de janela com largura média ou expandida é associada ao app, isso significa que ele pode ter espaço suficiente para mostrar o painel de lista e o de detalhes lado a lado para que os usuários possam ver a lista de itens e os detalhes do item selecionado sem a necessidade de transição da tela. No entanto, telas menores podem ficar muito sobrecarregadas com essa exibição. Nesse caso, é melhor mostrar um painel por vez começando pelo de lista. O painel de detalhes mostra os detalhes do item selecionado quando os usuários tocam em um item da lista. A classe SlidingPaneLayout gerencia a lógica para determinar qual dessas duas experiências do usuário é adequada para o tamanho atual da janela.

Configurar o layout do painel de lista

A classe SlidingPaneLayout é um componente de IU baseado em visualização. Você vai modificar o arquivo de recurso de layout do painel de lista, que é o arquivo fragment_sports_list.xml deste codelab.

A classe SlidingPaneLayout usa dois elementos filhos. Os atributos de largura e peso de cada elemento filho são os principais fatores para a classe SlidingPaneLayout determinar se a janela é grande o suficiente para mostrar as duas visualizações lado a lado. Se não for, a lista em tela cheia será substituída pela IU de detalhes em tela cheia. Os valores de peso são usados para dimensionar os dois painéis proporcionalmente quando o tamanho da janela é maior do que o requisito mínimo para mostrar os painéis lado a lado.

A classe SlidingPaneLayout foi aplicada ao arquivo fragment_sports_list.xml. O painel de lista está configurado para ter uma largura 1280dp. É por isso que esse painel e o de detalhes não são mostrados lado a lado.

Faça com que os painéis sejam mostrados lado a lado quando a largura da tela for maior que 580dp:

  • Defina a RecyclerView com uma largura de 280dp.

fragment_sports_list.xml

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SportsListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:clipToPadding="false"
        android:padding="8dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    <androidx.fragment.app.FragmentContainerView
        android:layout_height="match_parent"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:id="@+id/detail_container"
        android:name="com.example.android.sports.NewsDetailsFragment"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

Depois que você atualizar o arquivo sports_list_fragment.xml, o app esportivo vai mostrar a lista de esportes e as notícias do esporte selecionado lado a lado.

Figura 5. Os painéis de lista e de detalhes aparecem lado a lado depois que você atualiza o arquivo de recurso de layout.

Trocar o painel de detalhes

Agora, os painéis de lista e de detalhes aparecem lado a lado quando a classe de tamanho de janela com largura média ou expandida está associada ao app. No entanto, a tela vai mudar totalmente para o painel de detalhes quando os usuários selecionarem um item no painel de lista.

O app faz uma transição de tela e mostra apenas as notícias esportivas, embora seja esperado que ele continue mostrando a lista de esportes e as notícias lado a lado.

Figura 6. A tela faz a transição para o painel de detalhes depois que você seleciona um esporte na lista.

Esse problema é causado pela navegação acionada quando os usuários selecionam um item no painel de lista. O código relevante está disponível no arquivo SportsListFragment.kt.

SportsListFragment.kt

val adapter = SportsAdapter {
    sportsViewModel.updateCurrentSport(it)
    // Navigate to the details pane.
    val action =
       SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
    this.findNavController().navigate(action)
}

Confira se a tela só faz a transição completa para o painel de detalhes quando não há espaço suficiente para mostrar os painéis de lista e de detalhes lado a lado:

  • Na variável de função adapter, adicione uma instrução if que verifica se o atributo isSlidable da classe SlidingPaneLayout é verdadeiro e se o atributo isOpen da classe SlidingPaneLayout é falso.

SportsListFragment.kt

val adapter = SportsAdapter {
    sportsViewModel.updateCurrentSport(it)
    if(slidingPaneLayout.isSlidable && !slidingPaneLayout.isOpen){
        // Navigate to the details pane.
        val action =
           SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
        this.findNavController().navigate(action)
    }
}

7. Escolher o componente de navegação correto de acordo com a classe de tamanho de janela para a largura

O Material Design espera que seu app escolha os componentes de forma adaptável. Nesta seção, você escolhe um componente para a barra de navegação da parte de cima da tela com base na classe de tamanho de janela atual. Esta tabela descreve o componente de navegação esperado para cada classe de tamanho de janela:

Classe de tamanho de janela para a largura

Componente de navegação

Compacta

Navegação na parte de baixo da tela

Média

Coluna de navegação

Expandida

Gaveta de navegação permanente

Implementar a coluna de navegação

  1. Crie uma função de composição NavRail() que use três parâmetros: um objeto menuItems definido com um valor List<MenuItem>, um modifier definido com um valor Modifier e uma função lambda onMenuSelected.
  2. No corpo da função, chame uma NavigationRail() que use o objeto modifier como um parâmetro.
  3. Chame uma função NavigationRailItem() para cada MenuItem no objeto menuItems, como você fez com a função NavigationBarItem na BottomNavigationBar().

Implementar a gaveta de navegação permanente

  1. Crie uma função de composição NavigationDrawer() que use estes três parâmetros: um objeto menuItems definido com um valor List<MenuItem>, um modifier definido com um valor Modifier e uma função lambda onMenuSelected.
  2. No corpo da função, chame uma Column() que use o objeto modifier como um parâmetro.
  3. Chame uma função Row() para cada MenuItem no objeto menuItems.
  4. No corpo da função Row(), adicione os identificadores icon e text como você fez com a função NavigationBarItem na BottomNavigationBar().

Selecionar o componente de navegação correto de acordo com a classe de tamanho de janela para a largura

  1. Para recuperar a classe de tamanho de janela atual para a largura, chame a função rememberWidthSizeClass() dentro da função de composição transmitida ao método setContent() no objeto ComposeView.
  2. Crie uma ramificação condicional para escolher o componente de navegação com base na classe de tamanho de janela recuperada para a largura e chame a que for selecionada.
  3. Transmita um objeto Modifier para a função NavigationDrawer para especificar a largura com um valor de 256dp.

ActivityMain.kt

binding.navigation.setContent {
    MaterialTheme {
        when(rememberWidthSizeClass()){
            WidthSizeClass.COMPACT ->
                BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
                    navController.navigate(screen.destinationId)
                }
            WidthSizeClass.MEDIUM ->
                NavRail(menuItems = navigationMenuItems){ menuItem ->
                    navController.navigate(screen.destinationId)
                }
            WidthSizeClass.EXPANDED ->
                NavigationDrawer(
                    menuItems = navigationMenuItems,
                    modifier = Modifier.width(256.dp)
                ) { menuItem ->
                    navController.navigate(screen.destinationId)
                }
        }

    }
}

Colocar o componente de navegação na posição correta

Agora, seu app pode escolher o componente de navegação correto com base na classe de tamanho de janela atual para a largura, mas os componentes selecionados não podem ser posicionados como esperado. Isso ocorre porque o elemento ComposeView é colocado no FragmentViewContainer.

Atualize o recurso de layout alternativo para a classe MainActivity:

  1. Abra o arquivo layout-sw600dp/activity_main.xml.
  2. Atualize a restrição para dispor o elemento ComposeView e o FragmentContainer horizontalmente.

layout-sw600dp/activity_main.xml

  <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment_content_main"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintLeft_toRightOf="@+id/top_navigation"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:navGraph="@navigation/nav_graph" />

        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/top_navigation"
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:menu="@menu/top_navigation" />
    </androidx.constraintlayout.widget.ConstraintLayout>

Após a modificação, o app escolhe o componente de navegação correto com base na classe de tamanho de janela associada para a largura. A primeira captura mostra a tela para a classe de tamanho de janela com largura média. A segunda captura mostra a tela para a classe de tamanho de janela com largura expandida.

O app esportivo mostra a coluna de navegação, a lista de esportes e as notícias quando está associado à classe de tamanho de janela com largura média. O app esportivo mostra uma gaveta de navegação, a lista de esportes e as notícias na tela inicial quando o app está associado à classe de tamanho de janela com largura expandida.

Figura 7. Telas das classes de tamanho de janela com largura média e expandida.

8. Parabéns

Parabéns! Você concluiu este codelab e aprendeu a usar o Compose para adicionar layouts adaptáveis a um app Android baseado em visualização. Ao fazer isso, você aprendeu sobre a classe SlidingPaneLayout, as classes de tamanho de janela e a seleção do componente de navegação com base nas classes de tamanho de janela para a largura.

Saiba mais