Criar um app adaptável com a navegação dinâmica

1. Introdução

Uma das grandes vantagens de desenvolver seu app na Plataforma Android é a incrível oportunidade de alcançar usuários em diferentes tipos de formatos, como wearables, dobráveis, tablets, computadores e até TV. Talvez os usuários também queiram usar o app em dispositivos de tela grande para aproveitar o aumento do espaço. Cada vez mais, os usuários do Android aproveitam os apps em vários dispositivos com tamanhos de tela variados e esperam uma experiência de alta qualidade em todos esses aparelhos.

Até agora, você aprendeu a criar apps principalmente para dispositivos móveis. Neste codelab, você vai aprender a transformar seus apps para que eles se adaptem a outros tamanhos de tela. Você vai usar padrões de layout de navegação adaptáveis que são bonitos e podem ser usados em dispositivos móveis e de telas grandes, como dobráveis, tablets e computadores.

Pré-requisitos

  • Familiaridade com a programação em Kotlin, incluindo classes, funções e condicionais
  • Familiaridade com as classes ViewModel
  • Familiaridade com a criação de funções Composable
  • Experiência em criação de layouts com o Jetpack Compose
  • Saber executar apps em um dispositivo ou emulador

O que você vai aprender

  • Como criar navegação entre telas sem o gráfico de navegação para apps simples
  • Como criar um layout de navegação adaptável usando o Jetpack Compose
  • Como criar um gerenciador de retorno personalizado

O que você vai criar

  • Você vai implementar a navegação dinâmica no app Reply para que o layout se adapte a todos os tamanhos de tela

O produto final vai ser semelhante à imagem abaixo:

56cfa13ef31d0b59.png

​​

O que é necessário

  • Um computador com acesso à Internet, um navegador da Web e o Android Studio
  • Acesso ao GitHub

2. Visão geral do app

Introdução ao app Reply

O Reply é um app multitelas semelhante a um cliente de e-mail.

a1af0f9193718abf.png

Ele contém 4 categorias diferentes que são exibidas por guias distintas: "Inbox", "Sent", "Drafts" e "Spam".

Baixar o código inicial

No Android Studio, abra a pasta basic-android-kotlin-compose-training-reply-app.

3. Tutorial do código inicial

Diretórios importantes no app Reply

O diretório de arquivos do app Reply mostra dois subdiretórios expandidos:

A camada de dados e de UI do projeto do app Reply é separada em diretórios diferentes. O ReplyViewModel, o ReplyUiState e outros elementos de composição ficam no diretório ui. As classes data e enum que definem a camada de dados e as classes do provedor de dados estão no diretório data.

Inicialização de dados no app Reply

O app Reply é inicializado com dados pelo método initializeUIState() no ReplyViewModel, que é executado na função init.

ReplyViewModel.kt

...
    init {
        initializeUIState()
    }
 

    private fun initializeUIState() {
        var mailboxes: Map<MailboxType, List<Email>> =
            LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
        _uiState.value = ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
    }
...

O elemento combinável no nível da tela

Assim como em outros apps, o Reply usa o elemento combinável ReplyApp como sendo o principal, em que viewModel e uiState são declarados. Várias funções viewModel() também são transmitidas como argumentos lambda para o elemento combinável ReplyHomeScreen.

ReplyApp.kt

...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    ReplyHomeScreen(
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
}

Outros elementos de composição

  • ReplyHomeScreen.kt: contém os elementos de composição de tela inicial, incluindo os elementos de navegação.
  • ReplyHomeContent.kt: contém elementos de composição que definem elementos mais detalhados da tela inicial.
  • ReplyDetailsScreen.kt: contém os elementos de composição de tela e outros menores para a tela de detalhes.

Analise cada arquivo em detalhes para entender melhor os elementos combináveis antes de avançar para a próxima seção do codelab.

4. Mudar telas sem um gráfico de navegação

No programa de treinamentos anterior, você aprendeu a usar uma classe NavHostController para navegar de uma tela a outra. Com o Compose, também é possível mudar telas com instruções condicionais simples, usando estados mutáveis do tempo de execução. Isso é útil especialmente em aplicativos pequenos, como o app Reply, em que você só quer alternar entre duas telas.

Mudar telas com mudanças de estado

No Compose, as telas são recompostas quando ocorre uma mudança de estado. Mude telas usando condicionais simples para responder a mudanças de estado.

Você vai usar condicionais para mostrar o conteúdo da tela inicial quando o usuário estiver nela e exibir a tela de detalhes quando a pessoa não estiver na inicial.

Modifique o app Reply para permitir mudanças na tela quando o estado mudar seguindo estas etapas:

  1. Abra o código inicial no Android Studio.
  2. No elemento combinável ReplyHomeScreen em ReplyHomeScreen.kt, envolva o elemento combinável ReplyAppContent com uma instrução if para quando a propriedade isShowingHomepage do objeto replyUiState for true.

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Int) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {

...
    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    }
}

Agora você precisa pensar na exibição da tela de detalhes quando o usuário não estiver na tela inicial.

  1. Adicione uma ramificação else com o elemento combinável ReplyDetailsScreen no corpo. Adicione replyUIState, onDetailScreenBackPressed e modifier como argumentos do elemento combinável ReplyDetailsScreen.

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Int) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {

...

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    } else {
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            onBackPressed = onDetailScreenBackPressed,
            modifier = modifier
        )
    }
}

O objeto replyUiState é um objeto de estado. Dessa forma, quando há uma mudança na propriedade isShowingHomepage do objeto replyUiState, o elemento combinável ReplyHomeScreen é recomposto, e a instrução if/else é reavaliada no tempo de execução. Essa abordagem oferece suporte à navegação entre diferentes telas sem o uso de uma classe NavHostController.

8443a3ef1a239f6e.gif

Criar gerenciador de retorno personalizado

Uma vantagem de usar o elemento combinável NavHost para alternar entre telas é que as rotas das telas anteriores são salvas na pilha de retorno. Essas telas salvas permitem que o botão "Voltar" do sistema navegue com facilidade para a tela anterior quando invocado. Como o app Reply não usa um NavHost, você precisa adicionar o código para processar o botão "Voltar" manualmente. Você fará isso a seguir.

Conclua as etapas abaixo para criar um gerenciador de retorno personalizado no app Reply:

  1. Na primeira linha do elemento combinável ReplyDetailsScreen, adicione um elemento BackHandler.
  2. Chame a função onBackPressed() no corpo do elemento combinável BackHandler.

ReplyDetailsScreen.kt

...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
    BackHandler {
        onBackPressed()
    }
... 

5. Executar aplicativo em dispositivos de tela grande

Verificar o app com o emulador redimensionável

Para criar apps utilizáveis, os desenvolvedores precisam entender a experiência dos usuários em vários formatos. Portanto, você precisa testar apps em vários formatos desde o início do processo de desenvolvimento.

Você pode usar muitos emuladores de diferentes tamanhos de tela para atingir essa meta. No entanto, isso pode ser complicado, especialmente quando você está criando para vários tamanhos de tela de uma só vez. Talvez você também precise testar como um app que está sendo executado responde a mudanças no tamanho da tela, como mudanças de orientação, de tamanho de janela em um computador e de estado de dobra no dobrável.

O Android Studio ajuda a testar esses cenários com a introdução do emulador redimensionável.

Conclua estas etapas para configurar o emulador redimensionável:

  1. No Android Studio, selecione Tools > Device Manager.

O menu &quot;Tools&quot; exibe uma lista de opções. O Gerenciador de Dispositivos, que aparece na metade da lista, está selecionado.

  1. No Gerenciador de dispositivos, clique no ícone + para criar um dispositivo virtual.

A barra de ferramentas do Gerenciador de Dispositivos exibe duas opções de menu, incluindo a de criar dispositivo virtual.

  1. Selecione a categoria Phone e o dispositivo Resizable (Experimental).
  2. Clique em Next.

A janela do Gerenciador de dispositivos exibe uma solicitação para escolher uma definição de dispositivo. Uma lista de opções aparece com um campo de pesquisa acima dela. A categoria

  1. Selecione o nível da API 34 ou posterior.
  2. Clique em Next.

A janela &quot;Virtual Device Configuration&quot; exibe uma solicitação para selecionar uma imagem do sistema. O nível de API 34 é selecionado.

  1. Escolha um nome para o novo Dispositivo Virtual Android.
  2. Clique em Finish.

A tela de configuração virtual do Dispositivo virtual Android (AVD) é exibida. A tela de configuração inclui um campo de texto para inserir o nome do AVD. Abaixo do campo de nome, há uma lista de opções do dispositivo, incluindo a definição do dispositivo (experimental redimensionável), a imagem do sistema (Tiramisu) e a orientação, com a orientação retrato selecionada por padrão. Leitura de botões

Executar aplicativo em um emulador de tela grande

Agora que você configurou o emulador redimensionável, vamos ver como o app aparece em uma tela grande.

  1. Execute o app no emulador redimensionável.
  2. Selecione Tablet como modo de exibição.

bfacf9c20a30b06b.png

  1. Inspecione o app no modo tablet, no modo paisagem.

bb0fa5e954f6ca4b.png

A tela do tablet é alongada na horizontal. Embora essa orientação funcione, talvez ela não seja o melhor uso do espaço da tela grande. Vamos falar sobre isso a seguir.

Design para telas grandes

Ao analisar este app em um tablet, você pode pensar que ele foi mal projetado e não é atraente. Você acertou: esse layout não foi projetado para telas grandes.

Ao projetar para telas grandes, como tablets e dobráveis, é preciso considerar a ergonomia do usuário e a proximidade dos dedos dos usuários à tela. Em dispositivos móveis, os dedos podem facilmente alcançar a maior parte da tela. A localização dos elementos interativos, como botões e elementos de navegação, não é tão importante. No entanto, em telas grandes, ter elementos interativos importantes no meio da tela pode dificultar o alcance.

Como você pode ver no app Reply, projetar para telas grandes não é apenas alongar ou aumentar elementos de IU para se ajustarem à tela. É uma oportunidade de usar um espaço maior para criar uma experiência diferente para os usuários. Por exemplo, é possível adicionar outro layout na mesma tela para evitar a necessidade de navegar para outra tela ou possibilitar várias tarefas ao mesmo tempo.

f50e77a4ffd923a.png

Esse design pode aumentar a produtividade do usuário e promover um maior engajamento. Mas, antes de implantar esse design, é necessário aprender a criar layouts diferentes para tamanhos de tela variados.

6. Adaptar o layout a diferentes tamanhos de tela

O que são pontos de interrupção?

Você pode se perguntar como é possível mostrar layouts diferentes para o mesmo app. A resposta curta é: usando condicionais em estados diferentes, como você fez no início deste codelab.

Para criar um app adaptável, o layout precisa mudar de acordo com o tamanho da tela. O ponto de medição em que um layout muda é conhecido como ponto de interrupção. O Material Design criou um intervalo de pontos de interrupção opinativo que abrange a maioria das telas do Android.

A tabela mostra o intervalo do ponto de interrupção (em dp) para diferentes tipos de dispositivos e configurações. 0 a 599 dp é para celulares no modo retrato, smartphones no modo paisagem, tamanho de janela compacto, 4 colunas e 8 margens mínimas. 600 a 839 dp é para tablets dobráveis pequenos nos modos de retrato ou paisagem, classe de tamanho médio da janela, 12 colunas e 12 margens mínimas. 840 dp ou mais para um tablet grande nos modos retrato ou paisagem, classe de tamanho de janela expandida, 12 colunas e 32 margens mínimas. As observações da tabela indicam que as margens e os gutters são flexíveis e não precisam ter o mesmo tamanho e que os smartphones no modo paisagem são considerados uma exceção para que ainda caibam no intervalo do ponto de interrupção de 0 a 599 dp.

Essa tabela de intervalo de pontos de interrupção mostra, por exemplo, que, se o app estiver sendo executado em um dispositivo com tamanho de tela inferior a 600 dp, você vai precisar mostrar o layout para dispositivos móveis.

Usar classes de tamanho de janela

A API WindowSizeClass introduzida para o Compose simplifica a implementação de pontos de interrupção do Material Design.

As classes de tamanho de janela introduzem três categorias de tamanhos: compacto, médio e expandido, para largura e altura.

O diagrama representa as classes de tamanho de janela com base em largura. O diagrama representa as classes de tamanho de janela com base em altura.

Conclua as etapas a seguir para implementar a API WindowSizeClass no app Reply:

  1. Adicione a dependência material3-window-size-class ao arquivo build.gradle.kts do módulo.

build.gradle.kts

...
dependencies {
...
    implementation("androidx.compose.material3:material3-window-size-class")
...
  1. Clique em Sync Now para sincronizar o Gradle depois de adicionar a dependência.

b4c912a45fa8b7f4.png

Com o arquivo build.gradle.kts atualizado, agora você pode criar uma variável que armazena o tamanho da janela do app a qualquer momento.

  1. Na função onCreate() do arquivo MainActivity.kt, atribua o método calculateWindowSizeClass() com o contexto this transmitido no parâmetro a uma variável chamada windowSize.
  2. Importe o pacote calculateWindowSizeClass adequado.

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val layoutDirection = LocalLayoutDirection.current
            Surface (
               // ...
            ) {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()
...  
  1. Observe o sublinhado vermelho da sintaxe calculateWindowSizeClass, que mostra a lâmpada vermelha. Clique na lâmpada vermelha à esquerda da variável windowSize e selecione Opt in for ‘ExperimentalMaterial3WindowSizeClassApi' on ‘onCreate' para criar uma anotação sobre o método onCreate().

f8029f61dfad0306.png

Você pode usar a variável WindowWidthSizeClass no MainActivity.kt para determinar qual layout exibir em vários elementos combináveis. Vamos preparar o elemento combinável ReplyApp para receber esse valor.

  1. No arquivo ReplyApp.kt, modifique o elemento combinável ReplyApp para aceitar a WindowWidthSizeClass como o parâmetro e importar o pacote adequado.

ReplyApp.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...  
  1. Transmita a variável windowSize ao componente ReplyApp no método onCreate() do arquivo MainActivity.kt.

MainActivity.kt

...
        setContent {
            ReplyTheme {
                Surface {
                    val windowSize = calculateWindowSizeClass(this)
                    ReplyApp(
                        windowSize = windowSize.widthSizeClass
                    )
...  

Também será necessário atualizar a visualização do app para o parâmetro windowSize.

  1. Transmita o WindowWidthSizeClass.Compact como o parâmetro windowSize para o elemento combinável ReplyApp do componente de visualização e importe o pacote adequado.

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Preview(showBackground = true)
@Composable
fun ReplyAppCompactPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact,
            )
        }
    }
}
  1. Para mudar os layouts do app com base no tamanho da tela, adicione uma instrução when no elemento combinável ReplyApp com base no valor de WindowWidthSizeClass.

ReplyApp.kt

...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value
    
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
        }
        WindowWidthSizeClass.Medium -> {
        }
        WindowWidthSizeClass.Expanded -> {
        }
        else -> {
        }
    }
...  

Neste ponto, você estabeleceu uma base para usar os valores de WindowSizeClass para mudar os layouts no app. A próxima etapa é determinar como você quer que o app seja exibido em diferentes tamanhos de tela.

7. Implementar o layout da navegação adaptável

Implementar a navegação adaptável da IU

No momento, a navegação na parte de baixo é usada para todos os tamanhos de tela.

f39984211e4dd665.png

Como discutido anteriormente, esse elemento de navegação não é ideal, porque os usuários podem ter dificuldade para alcançar elementos essenciais em telas maiores. Felizmente, há padrões recomendados para diferentes elementos de navegação em várias classes de tamanho de janela na navegação para IUs responsivas. Para o app Reply, você pode implementar os seguintes elementos:

Uma tabela lista as classes de tamanho de janela e os poucos itens exibidos. A largura compacta exibe uma barra de navegação na parte de baixo. A largura média exibe uma coluna de navegação. A largura expandida exibe uma gaveta de navegação persistente com uma borda superior.

A coluna de navegação é outro componente do Material Design que permite opções de navegação compactas para destinos principais que podem ser acessadas na lateral do app.

1c73d20ace67811c.png

Da mesma forma, uma gaveta de navegação persistente/permanente é criada pelo Material Design como outra opção para oferecer acesso ergonômico a telas maiores.

6795fb31e6d4a564.png

Implementar uma gaveta de navegação

Para criar uma gaveta de navegação para telas expandidas, use o parâmetro navigationType. Conclua as etapas a seguir para fazer isso:

  1. Para representar diferentes tipos de elementos de navegação, crie um novo arquivo WindowStateUtils.kt em um novo pacote utils, que está no diretório ui.
  2. Adicione uma classe Enum para representar diferentes tipos de elementos de navegação.

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}
 

Para implementar a gaveta de navegação, você precisa determinar o tipo de navegação com base no tamanho da janela do app.

  1. No elemento combinável ReplyApp, crie uma variável navigationType e atribua a ela o valor ReplyNavigationType adequado, de acordo com o tamanho da tela na instrução when.

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyNavigationType
...
    val navigationType: ReplyNavigationType
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
        WindowWidthSizeClass.Medium -> {
            navigationType = ReplyNavigationType.NAVIGATION_RAIL
        }
        WindowWidthSizeClass.Expanded -> {
            navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        }
        else -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
    }
...
 

Você pode usar o valor navigationType no elemento combinável ReplyHomeScreen. Você pode se preparar para isso tornando-o um parâmetro para o elemento combinável.

  1. No elemento combinável ReplyHomeScreen, adicione navigationType como um parâmetro.

ReplyHomeScreen.kt

...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) 

...
 
  1. Transmita o navigationType para o elemento combinável ReplyHomeScreen.

ReplyApp.kt

...
    ReplyHomeScreen(
        navigationType = navigationType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
...
 

Em seguida, você pode criar uma ramificação para mostrar o conteúdo do app com uma gaveta de navegação quando o usuário abrir o app em uma tela expandida e exibir a tela inicial.

  1. No corpo do elemento combinável ReplyHomeScreen, adicione uma instrução if para a condição navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage.

ReplyHomeScreen.kt

import androidx.compose.material3.PermanentNavigationDrawer
...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
    }

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
...
  1. Para criar a gaveta permanente, crie o elemento combinável PermanentNavigationDrawer no corpo da instrução "if" e adicione o elemento combinável NavigationDrawerContent como entrada para o parâmetro drawerContent.
  2. Adicione o elemento combinável ReplyAppContent como o argumento lambda final de PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    }

...
  1. Adicione uma ramificação else que use o corpo do elemento combinável anterior para manter a ramificação anterior em telas não expandidas.

ReplyHomeScreen.kt

...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
}
...
  1. Execute o app no modo Tablet. Você verá a seguinte tela:

2dbbc2f88d08f6a.png

Implementar uma coluna de navegação

Assim como na implementação da gaveta de navegação, é necessário usar o parâmetro navigationType para alternar entre os elementos de navegação.

Primeiro, vamos adicionar uma coluna de navegação para telas médias.

  1. Comece com a preparação do elemento combinável ReplyAppContent, adicionando navigationType como um parâmetro.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {       
... 
  1. Transmita o valor navigationType para os dois elementos de composição ReplyAppContent.

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
... 

Em seguida, vamos adicionar ramificações, que permitem que o app mostre colunas de navegação para alguns cenários.

  1. Na primeira linha do corpo do elemento combinável ReplyAppContent, envolva o elemento combinável ReplyNavigationRail ao redor do elemento AnimatedVisibility e defina o parâmetro visible como true se o valor ReplyNavigationType for NAVIGATION_RAIL.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
    Box(modifier = modifier) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    MaterialTheme.colorScheme.inverseOnSurface
            )
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                    )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                  modifier = Modifier
                      .fillMaxWidth()
            )
        }
    }
}     
... 
  1. Para alinhar os elementos de composição corretamente, envolva AnimatedVisibility e Column encontrados no corpo ReplyAppContent em um elemento Row.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier,
) {
    Row(modifier = modifier) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            val navigationRailContentDescription = stringResource(R.string.navigation_rail)
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = Modifier
                    .fillMaxWidth()
            )
        }
    }
}

... 

Por fim, vamos garantir que a navegação na parte de baixo seja exibida em alguns cenários.

  1. Após o elemento combinável ReplyListOnlyContent, envolva o ReplyBottomNavigationBar com um elemento AnimatedVisibility.
  2. Defina o parâmetro visible quando o valor ReplyNavigationType for BOTTOM_NAVIGATION.

ReplyHomeScreen.kt

...
ReplyListOnlyContent(
    replyUiState = replyUiState,
    onEmailCardPressed = onEmailCardPressed,
    modifier = Modifier.weight(1f)
        .padding(
            horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
        )

)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
    val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
    ReplyBottomNavigationBar(
        currentTab = replyUiState.currentMailbox,
        onTabPressed = onTabPressed,
        navigationItemContentList = navigationItemContentList,
        modifier = Modifier
            .fillMaxWidth()
    )
}

... 
  1. Execute o app no modo Unfolded foldable. Você vai ver a seguinte tela:

bfacf9c20a30b06b.png

8. Acessar o código da solução

Para baixar o código do codelab concluído, use estes comandos git:

git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git 
cd basic-android-kotlin-compose-training-reply-app
git checkout nav-update

Se preferir, você pode baixar o repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Para conferir o código da solução, acesse o GitHub (link em inglês).

9. Conclusão

Parabéns! Você está a prestes a tornar o app Reply adaptável para todos os tamanhos de tela implementando um layout de navegação adaptável. Você melhorou a experiência do usuário usando muitos formatos no Android. No próximo codelab, você vai melhorar ainda mais suas habilidades ao trabalhar com apps adaptáveis, implementando layouts, testes e visualizações de conteúdo adaptável.

Não se esqueça de compartilhar seu trabalho nas redes sociais com a hashtag #AndroidBasics.

Saiba mais