Criar um app com layout adaptável

1. Introdução

No codelab anterior, você começou a transformar o app Reply em adaptável usando classes de tamanho de janela e implementando a navegação dinâmica. Esses recursos formam uma base importante e são a primeira etapa para criar apps para todos os tamanhos de tela. Se você perdeu o codelab Criar um app adaptável com a navegação dinâmica, recomendamos voltar e começar por ele.

Neste codelab, você vai usar um conceito aprendido para ampliar a implementação do layout adaptável no seu app usando layouts canônicos, um conjunto de padrões usados com frequência para telas grandes. Você também vai aprender mais sobre ferramentas e técnicas de teste para ajudar a criar apps robustos com rapidez.

Pré-requisitos

  • Já ter feito o codelab Criar um app adaptável com a navegação dinâmica
  • Ter familiaridade com a programação em Kotlin, incluindo classes, funções e condicionais
  • Conhecer as classes ViewModel
  • Conhecer as funções Composable
  • Ter experiência na criação de layouts com o Jetpack Compose
  • Executar apps em um dispositivo ou emulador
  • Ter experiência com a API WindowSizeClass

O que você vai aprender

  • Como criar um layout adaptável de padrão de visualização em lista usando o Jetpack Compose.
  • Como criar visualizações para diferentes tamanhos de tela.
  • Como testar códigos para vários tamanhos de tela.

O que você vai criar

  • Você vai continuar atualizando o app Reply para se adaptar a todos os tamanhos de tela.

O app final vai ficar assim:

O que é necessário

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

Baixar o código inicial

Para começar:

Outra opção é clonar o repositório do GitHub:

$ 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

Procure o código inicial no repositório do GitHub do Reply (link em inglês).

2. Visualizações para diferentes tamanhos de tela

Criar visualizações para diferentes tamanhos de tela

No codelab Criar um app adaptável com a navegação dinâmica, você aprendeu a usar elementos combináveis de visualização para ajudar no processo de desenvolvimento. Para um app adaptável, a prática recomendada é criar várias visualizações para o mostrar em diferentes tamanhos de tela. Com isso, é possível ver as mudanças em todos os tamanhos de tela de uma só vez. Além disso, as visualizações também servem como documentação para que outros desenvolvedores revisando seu código vejam se o app é compatível com diferentes tamanhos de tela.

Antes, só havia uma única visualização, que era compatível com tela compacta. Você vai adicionar mais visualizações a seguir.

Para adicionar visualizações em telas médias e expandidas, siga estas etapas:

  1. Adicione uma visualização para telas médias definindo um valor widthDp médio no parâmetro de anotação Preview e especificando o valor WindowWidthSizeClass.Medium como o parâmetro para o elemento combinável ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Medium)
        }
    }
}
... 
  1. Adicione outra visualização para telas expandidas definindo um valor widthDp grande no parâmetro de anotação Preview e especificando o valor WindowWidthSizeClass.Expanded como o parâmetro para o elemento combinável ReplyApp.

MainActivity.kt

...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
        }
    }
}
... 
  1. Crie a visualização para gerar esta tela:

5577b1d0fe306e33.png

f624e771b76bbc2.png

3. Implementar o layout de conteúdo adaptável

Introdução à visualização de detalhes e listas

Nas telas expandidas, o conteúdo parece esticado e não faz bom uso do espaço disponível na tela.

56cfa13ef31d0b59.png

É possível melhorar esse layout aplicando um dos layouts canônicos (link em inglês). Os layouts canônicos são composições de tela grande que servem como ponto de partida para o design e a implementação. Você pode usar os três layouts disponíveis para orientar a organização de elementos comuns no app, na visualização de lista, no painel de suporte e no feed. Cada layout considera casos de uso e componentes comuns para atender às expectativas e necessidades do usuário em relação à maneira como os apps se adaptam a tamanhos de tela e pontos de interrupção.

No app Reply, vamos implementar a visualização de detalhes e listas, que é melhor para navegar pelo conteúdo e ver os detalhes rapidamente. Com um layout de visualização de detalhes e listas, você cria outro painel ao lado da tela da lista de e-mails para mostrar os detalhes do e-mail. Esse layout permite usar a tela disponível para mostrar mais informações ao usuário e tornar o app mais produtivo.

Implementar a visualização de detalhes e listas

Para fazer essa implementação em telas maiores, siga estas etapas:

  1. Para representar tipos diferentes de layout de conteúdo, no WindowStateUtils.kt, crie uma nova classe Enum para cada tipo de conteúdo. Use o valor LIST_AND_DETAIL para quando a tela expandida estiver em uso. Caso contrário, use LIST_ONLY.

WindowStateUtils.kt

...
enum class ReplyContentType {
    LIST_ONLY, LIST_AND_DETAIL
}
... 
  1. Declare a variável contentType em ReplyApp.kt e atribua o contentType adequado para vários tamanhos de janela. Isso ajuda a determinar a seleção correta do tipo de conteúdo, dependendo do tamanho da tela.

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyContentType
...

    val navigationType: ReplyNavigationType
    val contentType: ReplyContentType

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Medium -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
        WindowWidthSizeClass.Expanded -> {
            ...
            contentType = ReplyContentType.LIST_AND_DETAIL
        }
        else -> {
            ...
            contentType = ReplyContentType.LIST_ONLY
        }
    }
... 

Em seguida, você pode usar o valor contentType para criar ramificações diferentes de layouts no elemento combinável ReplyAppContent.

  1. No ReplyHomeScreen.kt, adicione o contentType como o parâmetro do elemento combinável ReplyHomeScreen.

ReplyHomeScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    contentType: ReplyContentType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
...
  1. Transmita o valor contentType para o elemento combinável ReplyHomeScreen.

ReplyApp.kt

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

... 
  1. Adicione o contentType como um parâmetro para o elemento combinável ReplyAppContent.

ReplyHomeScreen.kt

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

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                contentType = contentType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackButtonClicked = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
... 

Vamos mostrar a lista completa e a tela de detalhes quando o contentType for LIST_AND_DETAIL ou o conteúdo do e-mail somente em lista quando o contentType for LIST_ONLY.

  1. No ReplyHomeScreen.kt, adicione uma instrução if/else no elemento combinável ReplyAppContent para mostrar o elemento ReplyListAndDetailContent quando o valor de contentType for LIST_AND_DETAIL e o elemento ReplyListOnlyContent na ramificação else.

ReplyHomeScreen.kt

...
        Column(
            modifier = modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            if (contentType == ReplyContentType.LIST_AND_DETAIL) {
                ReplyListAndDetailContent(
                    replyUiState = replyUiState,
                    onEmailCardPressed = onEmailCardPressed,
                    modifier = Modifier.weight(1f)
                )
            } else {
                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) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        }
... 
  1. Remova a condição replyUiState.isShowingHomepage para mostrar uma gaveta de navegação permanente, já que o usuário não vai precisar acessar a visualização de detalhes se estiver usando a visualização expandida.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
        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))
                    )
                }
            }
        ) {

... 
  1. Execute o app no modo tablet para conferir a tela abaixo:

fe811a212feefea5.png

Melhorias nos elementos da interface para a visualização de detalhes e listas

No momento, o app mostra um painel de detalhes na tela inicial para as telas expandidas.

e7c540e41fe1c3d.png

No entanto, a tela contém elementos irrelevantes, como o botão "Voltar", o assunto e outros paddings, já que foi projetada para ser uma tela de detalhes independente. É possível melhorar isso a seguir com um ajuste simples.

Para melhorar a tela de detalhes para a visualização expandida, siga estas etapas:

  1. No ReplyDetailsScreen.kt, adicione uma variável isFullScreen como um parâmetro Boolean ao elemento combinável ReplyDetailsScreen.

Essa adição permite diferenciá-lo quando ele for usado como independente e na tela inicial.

ReplyDetailsScreen.kt

...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
... 
  1. No elemento combinável ReplyDetailsScreen, envolva o elemento ReplyDetailsScreenTopBar com uma instrução if para que ele apareça apenas quando o app estiver em tela cheia.

ReplyDetailsScreen.kt

...
    LazyColumn(
        modifier = modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colorScheme.inverseOnSurface)
            .padding(top = dimensionResource(R.dimen.detail_card_list_padding_top))
    ) {
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }

... 

Agora você pode adicionar padding. O padding necessário para o elemento combinável ReplyEmailDetailsCard varia caso ele seja usado em tela cheia ou não. Quando você usa o ReplyEmailDetailsCard com outros elementos combináveis na tela expandida, há paddings adicionais de outros combináveis.

  1. Transmita o valor isFullScreen ao elemento combinável ReplyEmailDetailsCard. Transmita um modificador com um padding horizontal de R.dimen.detail_card_outer_padding_horizontal se o app estiver em tela cheia. Caso contrário, transmita um modificador com um padding de R.dimen.detail_card_outer_padding_horizontal.

ReplyDetailsScreen.kt

...
        item {
            if (isFullScreen) {
                ReplyDetailsScreenTopBar(
                    onBackPressed,
                    replyUiState,
                    Modifier
                        .fillMaxWidth()
                        .padding(bottom = dimensionResource(R.dimen.detail_topbar_padding_bottom))
                    )
                )
            }
            ReplyEmailDetailsCard(
                email = replyUiState.currentSelectedEmail,
                mailboxType = replyUiState.currentMailbox,
                isFullScreen = isFullScreen,
                modifier = if (isFullScreen) {
                    Modifier.padding(horizontal = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                } else {
                    Modifier.padding(end = dimensionResource(R.dimen.detail_card_outer_padding_horizontal))
                }
            )
        }
... 
  1. Adicione um valor isFullScreen como parâmetro ao elemento combinável ReplyEmailDetailsCard.

ReplyDetailsScreen.kt

...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
    email: Email,
    mailboxType: MailboxType,
    modifier: Modifier = Modifier,
    isFullScreen: Boolean = false
) {
... 
  1. Dentro do elemento combinável ReplyEmailDetailsCard, mostre o texto de assunto do e-mail somente quando o app não estiver em tela cheia, porque o layout de tela cheia já mostra o assunto do e-mail como cabeçalho. Se ele estiver em tela cheia, adicione um espaçador com R.dimen.detail_content_padding_top de altura.

ReplyDetailsScreen.kt

...
Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(dimensionResource(R.dimen.detail_card_inner_padding))
) {
    DetailsScreenHeader(
        email,
        Modifier.fillMaxWidth()
    )
    if (isFullScreen) {
        Spacer(modifier = Modifier.height(dimensionResource(R.dimen.detail_content_padding_top)))
    } else {
        Text(
            text = stringResource(email.subject),
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.outline,
            modifier = Modifier.padding(
                top = dimensionResource(R.dimen.detail_content_padding_top),
                bottom = dimensionResource(R.dimen.detail_expanded_subject_body_spacing)
            ),
        )
    }
    Text(
        text = stringResource(email.body),
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
    DetailsScreenButtonBar(mailboxType, displayToast)
}

... 
  1. Em ReplyHomeScreen.kt, dentro do elemento combinável ReplyHomeScreen, transmita um valor true para o parâmetro isFullScreen ao criar o combinável ReplyDetailsScreen como independente.

ReplyHomeScreen.kt

...
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                isFullScreen = true,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
... 
  1. Execute o app no modo tablet e confira o seguinte layout:

833b3986a71a0b67.png

Ajustar o gerenciamento de retorno para a visualização de detalhes e listas

Com as telas expandidas, não é necessário navegar para o ReplyDetailsScreen. Em vez disso, é recomendável fechar o app quando o usuário selecionar o botão "Voltar". Dessa forma, precisamos ajustar o gerenciador de retorno.

Modifique o gerenciador de retorno transmitindo a função activity.finish() como o parâmetro onBackPressed do elemento combinável ReplyDetailsScreen dentro do elemento ReplyListAndDetailContent.

ReplyHomeContent.kt

...
import android.app.Activity
import androidx.compose.ui.platform.LocalContext
...
        val activity = LocalContext.current as Activity
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            modifier = Modifier.weight(1f),
            onBackPressed = { activity.finish() }
        )
... 

4. Verificar se há diferentes tamanhos de tela

Diretriz de qualidade de apps para telas grandes

Para criar uma experiência ótima e consistente para usuários do Android, é importante criar e testar seu app com a qualidade em mente. Consulte as Principais diretrizes de qualidade de apps para saber como aprimorar ainda mais seu app.

Para criar um app de alta qualidade para todos os formatos, consulte as Diretrizes de qualidade de apps para telas grandes. O app também precisa atender aos critérios do nível 3: pronto para telas grandes.

Testar manualmente o app para que esteja pronto para telas grandes

As diretrizes de qualidade de apps oferecem recomendações e procedimentos para verificar a qualidade do seu app. Veja um exemplo de teste relevante para o app Reply.

Descrição da qualidade de apps para telas grandes para a categoria "Configuração e continuidade".

A diretriz de qualidade de apps acima exige que o app retenha ou restaure o estado após mudanças de configuração. A diretriz também fornece instruções sobre como testar apps, conforme mostrado na figura a seguir:

As etapas do teste de qualidade de apps em telas grandes para a categoria "Configuração e continuidade".

Para testar manualmente o app Reply para verificar a continuidade da configuração, siga estas etapas:

  1. Execute o app Reply em um dispositivo de tamanho médio ou, se você estiver usando o emulador redimensionável, o execute no modo dobrável desdobrado.
  2. Confira se o Giro automático no emulador está ativado.

5a1c3a4cb4fc0192.png

  1. Role para baixo na lista de e-mails.

7ce0887b5b38a1f0.png

  1. Clique em um card de e-mail. Por exemplo, abra o e-mail da Ali.

16d7ca9c17206bf8.png

  1. Gire o dispositivo para conferir se o e-mail selecionado ainda é consistente com o e-mail selecionado na orientação retrato. Nesse exemplo, o e-mail da Ali ainda é mostrado.

d078601f2cc50341.png

  1. Volte para a orientação de retrato para conferir se o app ainda mostra o mesmo e-mail.

16d7ca9c17206bf8.png

5. Adicionar um teste automatizado para apps adaptáveis

Configurar teste para o tamanho de tela compacto

No codelab Testar o app Cupcake, você aprendeu a criar testes de IU. Agora, vamos aprender a criar testes específicos para diferentes tamanhos de tela.

No app Reply, elementos de navegação diferentes são usados para diferentes tamanhos de tela. Por exemplo, quando o usuário vê a tela expandida, ele espera ver uma gaveta de navegação permanente. É útil criar testes para verificar a existência de vários elementos de navegação, como a navegação na parte de baixo da tela, a coluna de navegação e a gaveta de navegação para diferentes tamanhos de tela.

Para criar um teste e verificar a existência de um elemento de navegação na parte de baixo de uma tela compacta, siga estas etapas:

  1. No diretório de teste, crie uma nova classe Kotlin com o nome ReplyAppTest.kt.
  2. Na classe ReplyAppTest, crie uma regra de teste usando createAndroidComposeRule e transmitindo ComponentActivity como o parâmetro de tipo. O ComponentActivity é usado para acessar uma atividade vazia em vez do MainActivity.

ReplyAppTest.kt

...
class ReplyAppTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...

Para diferenciar os elementos de navegação nas telas, adicione uma testTag ao elemento ReplyBottomNavigationBar.

  1. Defina um recurso de string para a navegação na parte de baixo da tela.

strings.xml

...
<resources>
...
    <string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
  1. Adicione o nome da string como o argumento testTag para o método testTag do Modifier no elemento combinável ReplyBottomNavigationBar.

ReplyHomeScreen.kt

...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
    ...
    modifier = Modifier
        .fillMaxWidth()
        .testTag(bottomNavigationContentDescription)
)
...
  1. Na classe ReplyAppTest, crie uma função de teste para testar uma tela de tamanho compacto. Defina o conteúdo de composeTestRule com o elemento combinável ReplyApp e transmita WindowWidthSizeClass.Compact como o argumento windowSize.

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
    }
  1. Declare que o elemento de navegação na parte de baixo da tela existe com a tag de teste. Chame a função de extensão onNodeWithTagForStringId na composeTestRule, transmita a string de navegação na parte de baixo da tela e chame o método assertExists().

ReplyAppTest.kt

...
    @Test
    fun compactDevice_verifyUsingBottomNavigation() {
        // Set up compact window
        composeTestRule.setContent {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact
            )
        }
        // Bottom navigation is displayed
        composeTestRule.onNodeWithTagForStringId(
            R.string.navigation_bottom
        ).assertExists()
    }
  1. Execute o teste e confira se o resultado é o esperado.

Configurar teste para os tamanhos de tela médio e expandido

Agora que você criou um teste para a tela compacta, vamos criar os testes correspondentes para telas médias e expandidas.

Para criar testes e verificar a existência de uma coluna de navegação e de uma gaveta de navegação permanente para telas médias e expandidas, siga estas etapas:

  1. Defina um recurso de string para a coluna de navegação que vai ser usada como tag de teste posteriormente.

strings.xml

...
<resources>
...
    <string name="navigation_rail">Navigation Rail</string>
...
</resources>
  1. Transmita a string como a tag de teste usando o Modifier no elemento combinável PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
        PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
  1. Transmita a string como a tag de teste usando o Modifier no elemento combinável ReplyNavigationRail.

ReplyHomeScreen.kt

...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
    ...
    modifier = Modifier
        .testTag(navigationRailContentDescription)
)
...
  1. Adicione um teste para verificar se existe um elemento de coluna de navegação nas telas médias.

ReplyAppTest.kt

...
@Test
fun mediumDevice_verifyUsingNavigationRail() {
    // Set up medium window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Medium
        )
    }
    // Navigation rail is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_rail
    ).assertExists()
}
  1. Adicione um teste para verificar se existe um elemento de gaveta de navegação nas telas expandidas.

ReplyAppTest.kt

...
@Test
fun expandedDevice_verifyUsingNavigationDrawer() {
    // Set up expanded window
    composeTestRule.setContent {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Expanded
        )
    }
    // Navigation drawer is displayed
    composeTestRule.onNodeWithTagForStringId(
        R.string.navigation_drawer
    ).assertExists()
}
  1. Use um emulador de tablet ou um emulador redimensionável no modo tablet para executar o teste.
  2. Execute todos os testes e confirme se eles foram aprovados.

Testar mudanças de configuração na tela compacta

Uma mudança de configuração é uma ocorrência comum no ciclo de vida do seu app. Por exemplo, quando você muda da orientação retrato para a paisagem, ocorre uma mudança de configuração. Quando ocorre uma mudança de configuração, é importante testar se o app mantém o próprio estado, A seguir, você vai criar testes que simulam uma mudança de configuração para testar se o app mantém o estado em uma tela compacta.

Para testar uma mudança de configuração na tela compacta:

  1. No diretório de teste, crie uma nova classe Kotlin com o nome ReplyAppStateRestorationTest.kt.
  2. Na classe ReplyAppStateRestorationTest, crie uma regra de teste usando createAndroidComposeRule e transmitindo ComponentActivity como o parâmetro de tipo.

ReplyAppStateRestorationTest.kt

...
class ReplyAppStateRestorationTest {

    /**
     * Note: To access to an empty activity, the code uses ComponentActivity instead of
     * MainActivity.
     */
    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()
}
...
  1. Crie uma função de teste para verificar se um e-mail ainda está selecionado na tela compacta após uma mudança de configuração.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    
}
...

Para testar uma mudança de configuração, você precisa usar o StateRestorationTester.

  1. Configure o stateRestorationTester transmitindo a composeTestRule como um argumento para StateRestorationTester.
  2. Use setContent() com o elemento combinável ReplyApp e transmita WindowWidthSizeClass.Compact como o argumento windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

}
...
  1. Verifique se um terceiro e-mail é mostrado no app. Use o método assertIsDisplayed() na composeTestRule que procura o texto do terceiro e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Clique no assunto do e-mail para acessar a tela de detalhes. Use o método performClick() para navegar.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
}
...
  1. Verifique se o terceiro e-mail é mostrado na tela de detalhes. Declarar a existência do botão "Voltar" para confirmar que o app está na tela de detalhes e verificar se o texto do terceiro e-mail é mostrado.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
}
...
  1. Simule uma mudança de configuração usando stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
}
...
  1. Verifique se o terceiro e-mail aparece na tela de detalhes. Declarar a existência do botão "Voltar" para confirmar que o app está na tela de detalhes e verificar se o texto do terceiro e-mail é mostrado.

ReplyAppStateRestorationTest.kt

...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup compact window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Open detailed page
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that it shows the detailed screen for the correct email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that it still shows the detailed screen for the same email
    composeTestRule.onNodeWithContentDescriptionForStringId(
        R.string.navigation_back
    ).assertExists()
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertExists()
}

...
  1. Execute o teste com um emulador de smartphone ou um emulador redimensionável no modo de smartphone.
  2. Verifique se o teste é aprovado.

Testar mudanças de configuração na tela expandida

Para testar uma mudança de configuração na tela expandida simulando uma mudança de configuração e transmitindo a WindowWidthSizeClass adequada, siga estas etapas:

  1. Crie uma função de teste para verificar se um e-mail ainda está selecionado na tela de detalhes após uma mudança de configuração.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {

}
...

Para testar uma mudança de configuração, você precisa usar o StateRestorationTester.

  1. Configure o stateRestorationTester transmitindo a composeTestRule como um argumento para StateRestorationTester.
  2. Use setContent() com o elemento combinável ReplyApp e transmita WindowWidthSizeClass.Expanded como o argumento windowSize.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
  1. Verifique se um terceiro e-mail é mostrado no app. Use o método assertIsDisplayed() na composeTestRule que procura o texto do terceiro e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()
}
...
  1. Selecione o terceiro e-mail na tela de detalhes. Use o método performClick() para selecionar o e-mail.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()
    ...
}

...
  1. Use testTag na tela de detalhes e procure texto nos filhos para verificar se ela mostra o terceiro e-mail. Essa abordagem garante que você encontre o texto na seção de detalhes, e não na lista de e-mails.

ReplyAppStateRestorationTest.kt

...

@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
...
}

...
  1. Simule uma mudança de configuração usando stateRestorationTester.emulateSavedInstanceStateRestore().

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    ...
    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()
    ...
}
...
  1. Verifique novamente se a tela de detalhes mostra o terceiro e-mail após uma mudança de configuração.

ReplyAppStateRestorationTest.kt

...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
    // Setup expanded window
    val stateRestorationTester = StateRestorationTester(composeTestRule)
    stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }

    // Given third email is displayed
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
    ).assertIsDisplayed()

    // Select third email
    composeTestRule.onNodeWithText(
        composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
    ).performClick()

    // Verify that third email is displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )

    // Simulate a config change
    stateRestorationTester.emulateSavedInstanceStateRestore()

    // Verify that third email is still displayed on the details screen
    composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
        .assertAny(hasAnyDescendant(hasText(
            composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
        )
}
...
  1. Execute o teste com um emulador de tablet ou um emulador redimensionável no modo de tablet.
  2. Verifique se o teste é aprovado.

Usar anotações para agrupar testes para diferentes tamanhos de tela

Os testes anteriores mostram que alguns deles falham quando são executados em dispositivos com um tamanho de tela incompatível. Embora você possa executar o teste um por um usando um dispositivo adequado, é possível que essa abordagem não seja dimensionada quando você tiver muitos casos de teste.

Para resolver esse problema, crie anotações para indicar os tamanhos de tela em que o teste pode ser executado e configure o teste com anotação para os dispositivos adequados.

Para executar um teste com base em tamanhos de tela, siga estas etapas:

  1. No diretório de teste, crie TestAnnotations.kt, que contém três classes de anotação: TestCompactWidth, TestMediumWidth, TestExpandedWidth.

TestAnnotations.kt

...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
  1. Use as anotações nas funções de teste para testes compactos. Para isso, coloque a anotação TestCompactWidth após a anotação de um teste compacto em ReplyAppTest e ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_verifyUsingBottomNavigation() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestCompactWidth
    fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {

...
  1. Use as anotações nas funções de teste para testes médios, inserindo a anotação TestMediumWidth após a anotação de um teste médio em ReplyAppTest.

ReplyAppTest.kt

...
    @Test
    @TestMediumWidth
    fun mediumDevice_verifyUsingNavigationRail() {
...
  1. Use as anotações nas funções para testes expandidos, colocando a anotação TestExpandedWidth após a anotação de um teste expandido em ReplyAppTest e ReplyAppStateRestorationTest.

ReplyAppTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_verifyUsingNavigationDrawer() {
...

ReplyAppStateRestorationTest.kt

...
    @Test
    @TestExpandedWidth
    fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...

Para garantir o sucesso, configure o teste para executar apenas testes anotados com TestCompactWidth.

  1. No Android Studio, selecione Run > Edit Configurations… 7be537f5faa1a61a.png
  2. Renomeie o teste como Testes compactos e selecione All in Package para executar o teste.

f70b74bc2e6674f1.png

  1. À direita do campo Instrumentation arguments, clique nos três pontos ().
  2. Clique no botão de adição (+) e adicione os parâmetros extras: annotation com o valor com.example.reply.test.TestCompactWidth.

cf1ef9b80a1df8aa.png

  1. Execute os testes com um emulador compacto.
  2. Confira se apenas testes compactos foram executados.

204ed40031f8615a.png

  1. Repita as etapas para telas médias e expandidas.

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

Para fazer o download do código do codelab concluído, use o seguinte comando git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git

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

Se você quiser ver o código da solução, acesse o GitHub (link em inglês).

7. Conclusão

Parabéns! Você adaptou o app Reply a todos os tamanhos de tela implementando um layout adaptável. Você também aprendeu a acelerar o desenvolvimento usando visualizações e a manter a qualidade do seu app usando vários métodos de teste.

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

Saiba mais