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:
- Adicione uma visualização para telas médias definindo um valor
widthDp
médio no parâmetro de anotaçãoPreview
e especificando o valorWindowWidthSizeClass.Medium
como o parâmetro para o elemento combinávelReplyApp
.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Medium)
}
}
}
...
- Adicione outra visualização para telas expandidas definindo um valor
widthDp
grande no parâmetro de anotaçãoPreview
e especificando o valorWindowWidthSizeClass.Expanded
como o parâmetro para o elemento combinávelReplyApp
.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
}
}
}
...
- Crie a visualização para gerar esta tela:
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.
É 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:
- Para representar tipos diferentes de layout de conteúdo, no
WindowStateUtils.kt
, crie uma nova classeEnum
para cada tipo de conteúdo. Use o valorLIST_AND_DETAIL
para quando a tela expandida estiver em uso. Caso contrário, useLIST_ONLY
.
WindowStateUtils.kt
...
enum class ReplyContentType {
LIST_ONLY, LIST_AND_DETAIL
}
...
- Declare a variável
contentType
emReplyApp.kt
e atribua ocontentType
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
.
- No
ReplyHomeScreen.kt
, adicione ocontentType
como o parâmetro do elemento combinávelReplyHomeScreen
.
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
) {
...
- Transmita o valor
contentType
para o elemento combinávelReplyHomeScreen
.
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
)
...
- Adicione o
contentType
como um parâmetro para o elemento combinávelReplyAppContent
.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- Transmita o valor
contentType
para os dois elementos combináveisReplyAppContent
.
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
.
- No
ReplyHomeScreen.kt
, adicione uma instruçãoif/else
no elemento combinávelReplyAppContent
para mostrar o elementoReplyListAndDetailContent
quando o valor decontentType
forLIST_AND_DETAIL
e o elementoReplyListOnlyContent
na ramificaçãoelse
.
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
)
}
}
...
- 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))
)
}
}
) {
...
- Execute o app no modo tablet para conferir a tela abaixo:
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.
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:
- No
ReplyDetailsScreen.kt
, adicione uma variávelisFullScreen
como um parâmetroBoolean
ao elemento combinávelReplyDetailsScreen
.
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
) {
...
- No elemento combinável
ReplyDetailsScreen
, envolva o elementoReplyDetailsScreenTopBar
com uma instruçãoif
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.
- Transmita o valor
isFullScreen
ao elemento combinávelReplyEmailDetailsCard
. Transmita um modificador com um padding horizontal deR.dimen.detail_card_outer_padding_horizontal
se o app estiver em tela cheia. Caso contrário, transmita um modificador com um padding deR.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))
}
)
}
...
- Adicione um valor
isFullScreen
como parâmetro ao elemento combinávelReplyEmailDetailsCard
.
ReplyDetailsScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
email: Email,
mailboxType: MailboxType,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- 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 comR.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)
}
...
- Em
ReplyHomeScreen.kt
, dentro do elemento combinávelReplyHomeScreen
, transmita um valortrue
para o parâmetroisFullScreen
ao criar o combinávelReplyDetailsScreen
como independente.
ReplyHomeScreen.kt
...
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
...
- Execute o app no modo tablet e confira o seguinte layout:
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.
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:
Para testar manualmente o app Reply para verificar a continuidade da configuração, siga estas etapas:
- 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.
- Confira se o Giro automático no emulador está ativado.
- Role para baixo na lista de e-mails.
- Clique em um card de e-mail. Por exemplo, abra o e-mail da Ali.
- 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.
- Volte para a orientação de retrato para conferir se o app ainda mostra o mesmo e-mail.
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:
- No diretório de teste, crie uma nova classe Kotlin com o nome
ReplyAppTest.kt
. - Na classe
ReplyAppTest
, crie uma regra de teste usandocreateAndroidComposeRule
e transmitindoComponentActivity
como o parâmetro de tipo. OComponentActivity
é usado para acessar uma atividade vazia em vez doMainActivity
.
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
.
- 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>
- Adicione o nome da string como o argumento
testTag
para o métodotestTag
doModifier
no elemento combinávelReplyBottomNavigationBar
.
ReplyHomeScreen.kt
...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
...
modifier = Modifier
.fillMaxWidth()
.testTag(bottomNavigationContentDescription)
)
...
- Na classe
ReplyAppTest
, crie uma função de teste para testar uma tela de tamanho compacto. Defina o conteúdo decomposeTestRule
com o elemento combinávelReplyApp
e transmitaWindowWidthSizeClass.Compact
como o argumentowindowSize
.
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
}
- 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
nacomposeTestRule
, transmita a string de navegação na parte de baixo da tela e chame o métodoassertExists()
.
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()
}
- 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:
- 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>
- Transmita a string como a tag de teste usando o
Modifier
no elemento combinávelPermanentNavigationDrawer
.
ReplyHomeScreen.kt
...
val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
- Transmita a string como a tag de teste usando o
Modifier
no elemento combinávelReplyNavigationRail
.
ReplyHomeScreen.kt
...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
...
modifier = Modifier
.testTag(navigationRailContentDescription)
)
...
- 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()
}
- 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()
}
- Use um emulador de tablet ou um emulador redimensionável no modo tablet para executar o teste.
- 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:
- No diretório de teste, crie uma nova classe Kotlin com o nome
ReplyAppStateRestorationTest.kt
. - Na classe
ReplyAppStateRestorationTest
, crie uma regra de teste usandocreateAndroidComposeRule
e transmitindoComponentActivity
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>()
}
...
- 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
.
- Configure o
stateRestorationTester
transmitindo acomposeTestRule
como um argumento paraStateRestorationTester
. - Use
setContent()
com o elemento combinávelReplyApp
e transmitaWindowWidthSizeClass.Compact
como o argumentowindowSize
.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
}
...
- Verifique se um terceiro e-mail é mostrado no app. Use o método
assertIsDisplayed()
nacomposeTestRule
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()
}
...
- 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()
}
...
- 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(
}
...
- 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()
}
...
- 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()
}
...
- Execute o teste com um emulador de smartphone ou um emulador redimensionável no modo de smartphone.
- 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:
- 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
.
- Configure o
stateRestorationTester
transmitindo acomposeTestRule
como um argumento paraStateRestorationTester
. - Use
setContent()
com o elemento combinávelReplyApp
e transmitaWindowWidthSizeClass.Expanded
como o argumentowindowSize
.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
- Verifique se um terceiro e-mail é mostrado no app. Use o método
assertIsDisplayed()
nacomposeTestRule
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()
}
...
- 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()
...
}
...
- 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)))
)
...
}
...
- 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()
...
}
...
- 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)))
)
}
...
- Execute o teste com um emulador de tablet ou um emulador redimensionável no modo de tablet.
- 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:
- 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
...
- 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 emReplyAppTest
eReplyAppStateRestorationTest
.
ReplyAppTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_verifyUsingBottomNavigation() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
- 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 emReplyAppTest
.
ReplyAppTest.kt
...
@Test
@TestMediumWidth
fun mediumDevice_verifyUsingNavigationRail() {
...
- Use as anotações nas funções para testes expandidos, colocando a anotação
TestExpandedWidth
após a anotação de um teste expandido emReplyAppTest
eReplyAppStateRestorationTest
.
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
.
- No Android Studio, selecione Run > Edit Configurations…
- Renomeie o teste como Testes compactos e selecione All in Package para executar o teste.
- À direita do campo Instrumentation arguments, clique nos três pontos (…).
- Clique no botão de adição (
+
) e adicione os parâmetros extras: annotation com o valor com.example.reply.test.TestCompactWidth.
- Execute os testes com um emulador compacto.
- Confira se apenas testes compactos foram executados.
- 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.