1. Introducción
En el codelab anterior, comenzaste a transformar la app de Reply de modo que resulte adaptable mediante el uso de clases de tamaño de ventana y la implementación de la navegación dinámica. Estas funciones son una base importante y el primer paso a la hora de crear apps para todos los tamaños de pantalla. Si no pudiste realizar el codelab Cómo compilar una app adaptable con navegación dinámica, te recomendamos que regreses y comiences allí.
En este codelab, compilarás un concepto que aprendiste a fin de implementar aún más el diseño adaptable en tu app. Este diseño es parte de los diseños canónicos, un conjunto de patrones que se usan comúnmente para las pantallas grandes. También aprenderás más herramientas y técnicas de prueba que te ayudarán a compilar rápidamente apps sólidas.
Requisitos previos
- Haber completado el codelab Cómo compilar una app adaptable con navegación dinámica
- Conocer la programación de Kotlin, incluidas las clases, las funciones y los condicionales
- Conocer las clases
ViewModel - Conocer las funciones
Composable - Experiencia en la compilación de diseños con Jetpack Compose
- Tener experiencia en la ejecución de apps en un dispositivo o emulador
- Tener experiencia en el uso de la API de
WindowSizeClass
Qué aprenderás
- Cómo crear un diseño adaptable de patrón de vista de lista con Jetpack Compose
- Cómo crear vistas previas para diferentes tamaños de pantalla
- Cómo probar el código para varios tamaños de pantalla
Qué compilarás
- Seguirás actualizando la app de Reply para adaptarla a todos los tamaños de pantalla.
La app terminada se verá de la siguiente manera:
Requisitos
- Una computadora con acceso a Internet, un navegador web y Android Studio
- Acceso a GitHub
Descarga el código de partida
Para comenzar, descarga el código de partida:
Como alternativa, puedes clonar el repositorio de GitHub para el código:
$ 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
Puedes explorar el código de partida en el repositorio Reply de GitHub.
2. Vistas previas para diferentes tamaños de pantalla
Cómo crear vistas previas para diferentes tamaños de pantalla
En el codelab Cómo compilar una app adaptable con navegación dinámica, aprendiste a usar vistas previas componibles para tu proceso de desarrollo. En el caso de una app adaptable, te recomendamos que crees varias vistas previas para mostrar la app en diferentes tamaños de pantalla. Con las diferentes vistas previas, podrás ver los cambios en todos los tamaños de pantallas a la vez. Además, las vistas previas también sirven como documentación para otros desarrolladores que revisen tu código a fin de asegurarse de que tu app sea compatible con diferentes tamaños de pantalla.
Anteriormente, solo había una vista previa compatible con la pantalla compacta. A continuación, agregarás más.
Para agregar vistas previas de las pantallas medianas y expandidas, sigue estos pasos:
- Agrega una vista previa para pantallas medianas configurando un valor de
widthDpmediano en el parámetro de anotaciónPreviewy especificando el valorWindowWidthSizeClass.Mediumcomo parámetro para el elementoReplyAppcomponible.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 700)
@Composable
fun ReplyAppMediumPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Medium)
}
}
}
...
- Agrega otra vista previa para pantallas expandidas configurando un valor de
widthDpgrande en el parámetro de anotaciónPreviewy especificando el valorWindowWidthSizeClass.Expandedcomo parámetro del elemento componibleReplyApp.
MainActivity.kt
...
@Preview(showBackground = true, widthDp = 1000)
@Composable
fun ReplyAppExpandedPreview() {
ReplyTheme {
Surface {
ReplyApp(windowSize = WindowWidthSizeClass.Expanded)
}
}
}
...
- Compila la vista previa para ver lo siguiente:


3. Cómo implementar el diseño de contenido adaptable
Introducción a la vista de lista-detalles
Notarás que, en las pantallas expandidas, el contenido se ve estirado y no aprovecha bien el espacio disponible en pantalla.

Puedes mejorar este diseño aplicando uno de los diseños canónicos. Estos diseños son composiciones de pantalla grande que funcionan como puntos de partida para el diseño y la implementación. Puedes usar los tres diseños disponibles a fin de guiarte en la organización de los elementos comunes en una app, la vista de lista, el panel de asistencia y el feed. Cada diseño considera casos de uso y componentes comunes para abordar las expectativas y las necesidades de los usuarios sobre la forma en que las apps se adaptan a diferentes tamaños de pantalla y puntos de interrupción.
En la app de Reply, implementemos la vista de lista y detalles, ya que es la mejor opción a la hora de explorar contenido y ver detalles rápidamente. Con un diseño de vista de lista-detalles, crearás otro panel junto a la pantalla de la lista de correos electrónicos para mostrar los detalles. Este diseño te permite usar la pantalla disponible a fin de mostrarle más información al usuario y hacer que tu app sea más productiva.
Cómo implementar una vista de lista-detalles
Para implementar una vista de lista-detalles para pantallas expandidas, completa los siguientes pasos:
- Para representar diferentes tipos de diseño de contenido, en
WindowStateUtils.kt, crea una nueva claseEnumpara diferentes tipos de contenido. Usa el valorLIST_AND_DETAILpara cuando la pantalla expandida esté en uso yLIST_ONLYen caso contrario.
WindowStateUtils.kt
...
enum class ReplyContentType {
LIST_ONLY, LIST_AND_DETAIL
}
...
- Declara la variable
contentTypeenReplyApp.kty asigna elcontentTypeapropiado para varios tamaños de ventana con el objetivo de determinar la selección del tipo de contenido adecuado, según el tamaño de la pantalla.
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
}
}
...
Luego, puedes usar el valor contentType para crear diferentes ramas para diseños en el elemento ReplyAppContent componible.
- En
ReplyHomeScreen.kt, agregacontentTypecomo parámetro al elementoReplyHomeScreencomponible.
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
) {
...
- Pasa el valor
contentTypeal elementoReplyHomeScreencomponible.
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
)
...
- Agrega
contentTypecomo parámetro para el elementoReplyAppContentcomponible.
ReplyHomeScreen.kt
...
@Composable
private fun ReplyAppContent(
navigationType: ReplyNavigationType,
contentType: ReplyContentType,
replyUiState: ReplyUiState,
onTabPressed: ((MailboxType) -> Unit),
onEmailCardPressed: (Email) -> Unit,
navigationItemContentList: List<NavigationItemContent>,
modifier: Modifier = Modifier
) {
...
- Pasa el valor
contentTypea los dos elementosReplyAppContentcomponible.
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
)
}
}
...
Mostremos la pantalla completa de lista y detalles cuando contentType sea LIST_AND_DETAIL o solo el contenido de lista de correo electrónico cuando contentType sea LIST_ONLY.
- En
ReplyHomeScreen.kt, agrega una sentenciaif/elseen el elementoReplyAppContentcomponible para mostrar el elementoReplyListAndDetailContentcuando el valorcontentTypeseaLIST_AND_DETAILy mostrar el elementoReplyListOnlyContenten la ramaelse.
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
)
}
}
...
- Quita la condición
replyUiState.isShowingHomepagepara mostrar un panel lateral de navegación permanente, ya que el usuario no necesita navegar a la vista de detalles si usa la vista 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))
)
}
}
) {
...
- Ejecuta la app en el modo tablet para ver la siguiente pantalla:

Cómo mejorar los elementos de la IU para la vista de lista-detalles
Actualmente, tu app muestra un panel de detalles en la pantalla principal de las pantallas expandidas.

Sin embargo, la pantalla contiene elementos extraños, como el botón para ir hacia atrás, el encabezado del asunto y paddings adicionales, ya que se diseñaron para una pantalla de detalles independiente. A continuación, podrás mejorar esto mediante un simple ajuste.
A fin de mejorar la pantalla de detalles de la vista expandida, completa los siguientes pasos:
- En
ReplyDetailsScreen.kt, agrega una variableisFullScreencomo parámetroBooleanal elemento componibleReplyDetailsScreen.
Este agregado te permite diferenciar el elemento componible cuando lo usas de forma independiente y cuando lo usas dentro de la pantalla principal.
ReplyDetailsScreen.kt
...
@Composable
fun ReplyDetailsScreen(
replyUiState: ReplyUiState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- Dentro del elemento
ReplyDetailsScreencomponible, une el elementoReplyDetailsScreenTopBarcon una sentenciaifpara que solo se muestre cuando la app esté en pantalla completa.
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))
)
)
}
...
Ahora puedes agregar padding. El padding requerido para el elemento ReplyEmailDetailsCard componible difiere en función de si lo usas o no en pantalla completa. Cuando usas ReplyEmailDetailsCard con otros elementos componibles en la pantalla expandida, hay padding adicional de otros elementos.
- Pasa el valor
isFullScreenal elementoReplyEmailDetailsCardcomponible. Pasa un modificador con un padding horizontal deR.dimen.detail_card_outer_padding_horizontalsi la pantalla está en modo de pantalla completa y, de lo contrario, pasa un modificador con un padding final 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))
}
)
}
...
- Agrega un valor
isFullScreencomo parámetro al elementoReplyEmailDetailsCardcomponible.
ReplyDetailsScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
email: Email,
mailboxType: MailboxType,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- Dentro del elemento
ReplyEmailDetailsCardcomponible, solo muestra el texto del asunto del correo electrónico cuando la app no esté en pantalla completa, dado que este diseño ya muestra el asunto del correo electrónico como encabezado. Si está en pantalla completa, agrega un separador con una altura deR.dimen.detail_content_padding_top.
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)
}
...
- En
ReplyHomeScreen.kt, dentro del elementoReplyHomeScreencomponible, pasa un valortruepara el parámetroisFullScreencuando crees el elementoReplyDetailsScreencomo independiente.
ReplyHomeScreen.kt
...
} else {
ReplyDetailsScreen(
replyUiState = replyUiState,
isFullScreen = true,
onBackPressed = onDetailScreenBackPressed,
modifier = modifier
)
}
...
- Ejecuta la app en el modo tablet y observa el siguiente diseño:

Cómo ajustar el control de retroceso para la vista de lista-detalles
Con las pantallas expandidas, no necesitas navegar a ReplyDetailsScreen. En cambio, quieres que la app se cierre cuando el usuario seleccione el botón para ir hacia atrás. Por lo tanto, debemos ajustar el controlador de retroceso.
Para modificar el controlador de actividades anteriores, pasa la función activity.finish() como el parámetro onBackPressed del elemento ReplyDetailsScreen componible dentro del 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. Cómo verificar el diseño para diferentes tamaños de pantalla
Lineamientos de calidad de las apps en pantallas grandes
A fin de crear una experiencia excelente y coherente para los usuarios de Android, es importante que compiles y pruebes tu app teniendo en cuenta la calidad. Puedes consultar los lineamientos de calidad de la app principal a fin de determinar cómo mejorarla.
Si quieres crear una app de excelente calidad para todos los factores de forma, consulta los lineamientos de calidad de las apps para pantallas grandes. La app también debe cumplir con los requisitos de Nivel 3 de compatibilidad con pantallas grandes.
Cómo probar de forma manual la preparación de tu app para pantallas grandes
Los lineamientos de calidad de las apps brindan recomendaciones y procedimientos para los dispositivos de prueba a fin de verificar su calidad. Veamos un ejemplo de prueba relevante para la app de Reply.

El lineamiento de calidad de apps anterior requiere que la app conserve o restablezca su estado después de los cambios de configuración. También se proporcionan instrucciones para probar apps, como se muestra en la siguiente figura:

A fin de probar la continuidad de la configuración de la app de Reply de forma manual, completa los siguientes pasos:
- Ejecuta la app de Reply en un dispositivo mediano o, si usas el emulador de tamaño variable, en modo plegable desplegado.
- Asegúrate de que la opción Giro automático del emulador esté activada.

- Desplázate hacia abajo en la lista de direcciones de correo electrónico.

- Haz clic en una tarjeta de correo electrónico. Por ejemplo, abre el correo electrónico de Ali.

- Rota el dispositivo para comprobar que el correo electrónico seleccionado siga siendo coherente con el que seleccionaste en orientación vertical. En este ejemplo, todavía se muestra un correo electrónico de Ali.

- Vuelve a la orientación vertical para comprobar que la app siga mostrando el mismo correo electrónico.

5. Cómo agregar pruebas automatizadas para apps adaptables
Cómo configurar la prueba para el tamaño de pantalla compacta
En el codelab Cómo probar la app de Cupcake, aprendiste a crear pruebas de IU. Ahora, veamos cómo crear pruebas específicas para diferentes tamaños de pantalla.
En la app de Reply, usas diferentes elementos de navegación para diferentes tamaños de pantalla. Por ejemplo, esperas ver un panel lateral de navegación permanente cuando el usuario ve la pantalla expandida. Resulta útil crear pruebas para comprobar la existencia de varios elementos de navegación, como la barra de navegación inferior, el riel de navegación y el panel lateral de navegación para diferentes tamaños de pantalla.
Completa los siguientes pasos a los efectos de crear una prueba que verifique la existencia de un elemento de navegación inferior en una pantalla compacta:
- En el directorio de prueba, crea una nueva clase de Kotlin llamada
ReplyAppTest.kt. - En la clase
ReplyAppTest, crea una regla de prueba concreateAndroidComposeRuley pasaComponentActivitycomo el parámetro de tipo. Se usaComponentActivitypara acceder a una actividad vacía en lugar de aMainActivity.
ReplyAppTest.kt
...
class ReplyAppTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
...
Para diferenciar los elementos de navegación de las pantallas, agrega un elemento testTag en el elemento ReplyBottomNavigationBar componible.
- Define un recurso de cadenas para la navegación inferior.
strings.xml
...
<resources>
...
<string name="navigation_bottom">Navigation Bottom</string>
...
</resources>
- Agrega el nombre de string como el argumento de
testTagpara el métodotestTagdeModifieren el elementoReplyBottomNavigationBarcomponible.
ReplyHomeScreen.kt
...
val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
ReplyBottomNavigationBar(
...
modifier = Modifier
.fillMaxWidth()
.testTag(bottomNavigationContentDescription)
)
...
- En la clase
ReplyAppTest, crea una función de prueba para probar una pantalla de tamaño compacto. Configura el contenido decomposeTestRulecon el elementoReplyAppcomponible y pasaWindowWidthSizeClass.Compactcomo el argumentowindowSize.
ReplyAppTest.kt
...
@Test
fun compactDevice_verifyUsingBottomNavigation() {
// Set up compact window
composeTestRule.setContent {
ReplyApp(
windowSize = WindowWidthSizeClass.Compact
)
}
}
- Confirma que el elemento de navegación inferior existe con la etiqueta de prueba. Llama a la función de extensión
onNodeWithTagForStringIdencomposeTestRule, pasa la string de navegación inferior y llama al 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()
}
- Ejecuta la prueba y verifica que sea exitosa.
Cómo configurar la prueba para los tamaños de pantalla mediana y expandida
Ahora que creaste correctamente una prueba para la pantalla compacta, crearemos las pruebas correspondientes para las pantallas medianas y expandidas.
Completa los siguientes pasos a los efectos de crear pruebas que verifiquen la existencia de un riel de navegación y un panel lateral de navegación permanente para pantallas medianas y expandidas:
- Define un recurso de cadenas para el riel de navegación que se usará como etiqueta de prueba más adelante.
strings.xml
...
<resources>
...
<string name="navigation_rail">Navigation Rail</string>
...
</resources>
- Pasa la cadena como la etiqueta de prueba a través del
Modifieren el elementoPermanentNavigationDrawercomponible.
ReplyHomeScreen.kt
...
val navigationDrawerContentDescription = stringResource(R.string.navigation_drawer)
PermanentNavigationDrawer(
...
modifier = Modifier.testTag(navigationDrawerContentDescription)
)
...
- Pasa la cadena como la etiqueta de prueba a través del
Modifieren el elementoReplyNavigationRailcomponible.
ReplyHomeScreen.kt
...
val navigationRailContentDescription = stringResource(R.string.navigation_rail)
ReplyNavigationRail(
...
modifier = Modifier
.testTag(navigationRailContentDescription)
)
...
- Agrega una prueba para verificar que exista un elemento de riel de navegación en las pantallas medianas.
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()
}
- Agrega una prueba para verificar que exista un elemento de panel lateral de navegación en las pantallas 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()
}
- Usa un emulador de tablet o uno de tamaño variable en el modo Tablet a fin de ejecutar la prueba.
- Ejecuta todas las pruebas y verifica que resulten exitosas.
Cómo probar un cambio de configuración en una pantalla compacta
Un cambio de configuración es un caso común que ocurre en el ciclo de vida de tu app. Por ejemplo, cuando cambias la orientación de vertical a horizontal, se produce un cambio de configuración. Cuando eso sucede, es importante probar que tu app conserve su estado. A continuación, crearás pruebas que simulan un cambio de configuración a fin de probar que tu app conserva su estado en una pantalla compacta.
Para probar un cambio de configuración en la pantalla compacta, haz lo siguiente:
- En el directorio de prueba, crea una nueva clase de Kotlin llamada
ReplyAppStateRestorationTest.kt. - En la clase
ReplyAppStateRestorationTest, crea una regla de prueba concreateAndroidComposeRuley pasaComponentActivitycomo el 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>()
}
...
- Crea una función de prueba que verifique que un correo electrónico aún aparezca seleccionado en la pantalla compacta después de un cambio de configuración.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
Para probar un cambio de configuración, debes usar StateRestorationTester.
- Configura
stateRestorationTesterpasandocomposeTestRulecomo argumento aStateRestorationTester. - Usa
setContent()con el elementoReplyAppcomponible y pasa el elementoWindowWidthSizeClass.Compactcomo el argumento dewindowSize.
ReplyAppStateRestorationTest.kt
...
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup compact window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
}
...
- Verifica que se muestre un tercer correo electrónico en la app. Usa el método
assertIsDisplayed()encomposeTestRule, que busca el texto del tercer correo electrónico.
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()
}
...
- Haz clic en el asunto del correo electrónico para navegar a su pantalla de detalles. Usa el método
performClick()para la navegación.
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()
}
...
- Verifica que el tercer correo electrónico aparezca en la pantalla de detalles. Comprueba la existencia del botón para ir hacia atrás con el objetivo de confirmar que la app se encuentra en la pantalla de detalles y verifica que se muestre el texto del tercer correo electrónico.
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(
}
...
- Simula un cambio de configuración con
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()
}
...
- Verifica una vez más que el tercer correo electrónico aparezca en la pantalla de detalles. Comprueba la existencia del botón para ir hacia atrás con el objetivo de confirmar que la app se encuentra en la pantalla de detalles y verifica que se muestre el texto del tercer correo electrónico.
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()
}
...
- Ejecuta la prueba con un emulador de teléfono o uno de tamaño variable en el modo de Teléfono.
- Verifica que la prueba resulte exitosa.
Cómo probar un cambio de configuración en la pantalla expandida
Prueba un cambio de configuración en la pantalla expandida simulando un cambio de configuración y pasando la WindowWidthSizeClass apropiada. Para ello, completa los siguientes pasos:
- Crea una función de prueba que verifique que un correo electrónico aún aparezca seleccionado en la pantalla de detalles después de un cambio de configuración.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
}
...
Para probar un cambio de configuración, debes usar StateRestorationTester.
- Configura
stateRestorationTesterpasandocomposeTestRulecomo argumento aStateRestorationTester. - Usa
setContent()con el elementoReplyAppcomponible y pasa el elementoWindowWidthSizeClass.Expandedcomo el argumento dewindowSize.
ReplyAppStateRestorationTest.kt
...
@Test
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
// Setup expanded window
val stateRestorationTester = StateRestorationTester(composeTestRule)
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
}
...
- Verifica que se muestre un tercer correo electrónico en la app. Usa el método
assertIsDisplayed()encomposeTestRule, que busca el texto del tercer correo electrónico.
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()
}
...
- Selecciona el tercer correo electrónico en la pantalla de detalles. Usa el método
performClick()para seleccionar el correo electrónico.
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()
...
}
...
- Verifica que la pantalla de detalles muestre el tercer correo electrónico usando
testTagen la pantalla de detalles y buscando texto en sus elementos secundarios. Este enfoque garantiza que el texto se encuentre en la sección de detalles y no en la lista de direcciones de correo electrónico.
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)))
)
...
}
...
- Simula un cambio de configuración mediante
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()
...
}
...
- Vuelve a verificar que la pantalla de detalles muestre el tercer correo electrónico después de un cambio de configuración.
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)))
)
}
...
- Ejecuta la prueba con un emulador de tablet o uno de tamaño variable en el modo Tablet.
- Verifica que la prueba resulte exitosa.
Cómo usar las anotaciones para probar el grupo según diferentes tamaños de pantalla
A partir de las pruebas anteriores, es posible que notes que algunas pruebas fallan cuando se ejecutan en dispositivos con un tamaño de pantalla incompatible. Si bien puedes ejecutar la prueba una por una con un dispositivo adecuado, es posible que este enfoque no escale cuando tengas muchos casos de prueba.
Para resolver este problema, puedes crear anotaciones para indicar los tamaños de pantalla en los que se puede ejecutar la prueba y configurar la prueba anotada en los dispositivos adecuados.
Para ejecutar una prueba según el tamaño de la pantalla, completa los siguientes pasos:
- En el directorio de prueba, crea
TestAnnotations.kt, que contiene tres clases de anotación:TestCompactWidth,TestMediumWidthyTestExpandedWidth.
TestAnnotations.kt
...
annotation class TestCompactWidth
annotation class TestMediumWidth
annotation class TestExpandedWidth
...
- Usa las anotaciones en las funciones de prueba para pruebas compactas colocando la anotación
TestCompactWidthdespués de la anotación de prueba para una prueba compacta enReplyAppTestyReplyAppStateRestorationTest.
ReplyAppTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_verifyUsingBottomNavigation() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestCompactWidth
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
...
- Usa las anotaciones en las funciones de prueba para pruebas de nivel intermedio colocando la anotación
TestMediumWidthdespués de la anotación de prueba para una prueba de nivel intermedio enReplyAppTest.
ReplyAppTest.kt
...
@Test
@TestMediumWidth
fun mediumDevice_verifyUsingNavigationRail() {
...
- Usa las anotaciones en las funciones de prueba para pruebas expandidas colocando la anotación
TestExpandedWidthdespués de la anotación de prueba para una prueba expandida enReplyAppTestyReplyAppStateRestorationTest.
ReplyAppTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_verifyUsingNavigationDrawer() {
...
ReplyAppStateRestorationTest.kt
...
@Test
@TestExpandedWidth
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
...
Para garantizar el éxito, configura la prueba de modo que solo se ejecuten pruebas con anotaciones TestCompactWidth.
- En Android Studio, selecciona Run > Edit Configurations...

- Cambia el nombre de la prueba a Pruebas compactas y ejecuta la prueba All in Package.

- Haz clic en los tres puntos (…) a la derecha del campo Instrumentation arguments.
- Haz clic en el botón de signo más (
+) y agrega los parámetros adicionales: annotation en el valor com.example.reply.test.TestCompactWidth.

- Ejecuta las pruebas con un emulador compacto.
- Verifica que solo se ejecutaron pruebas compactas.

- Repite los pasos para las pantallas medianas y expandidas.
6. Obtén el código de la solución
Para descargar el código del codelab terminado, usa el siguiente comando de Git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de la solución, puedes hacerlo en GitHub.
7. Conclusión
¡Felicitaciones! Implementaste un diseño adaptable para que la app de Reply se adapte a todos los tamaños de pantalla. También aprendiste a acelerar tu desarrollo usando vistas previas y manteniendo la calidad de tu app mediante varios métodos de prueba.
No olvides compartir tu trabajo en redes sociales con el hashtag #AndroidBasics.