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 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
widthDp
mediano en el parámetro de anotaciónPreview
y especificando el valorWindowWidthSizeClass.Medium
como parámetro para el elementoReplyApp
componible.
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
widthDp
grande en el parámetro de anotaciónPreview
y especificando el valorWindowWidthSizeClass.Expanded
como 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 claseEnum
para diferentes tipos de contenido. Usa el valorLIST_AND_DETAIL
para cuando la pantalla expandida esté en uso yLIST_ONLY
en caso contrario.
WindowStateUtils.kt
...
enum class ReplyContentType {
LIST_ONLY, LIST_AND_DETAIL
}
...
- Declara la variable
contentType
enReplyApp.kt
y asigna elcontentType
apropiado 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
, agregacontentType
como parámetro al elementoReplyHomeScreen
componible.
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
contentType
al elementoReplyHomeScreen
componible.
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
contentType
como parámetro para el elementoReplyAppContent
componible.
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
contentType
a los dos elementosReplyAppContent
componible.
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/else
en el elementoReplyAppContent
componible para mostrar el elementoReplyListAndDetailContent
cuando el valorcontentType
seaLIST_AND_DETAIL
y mostrar el elementoReplyListOnlyContent
en 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.isShowingHomepage
para 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 variableisFullScreen
como parámetroBoolean
al 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
ReplyDetailsScreen
componible, une el elementoReplyDetailsScreenTopBar
con una sentenciaif
para 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
isFullScreen
al elementoReplyEmailDetailsCard
componible. Pasa un modificador con un padding horizontal deR.dimen.detail_card_outer_padding_horizontal
si 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
isFullScreen
como parámetro al elementoReplyEmailDetailsCard
componible.
ReplyDetailsScreen.kt
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReplyEmailDetailsCard(
email: Email,
mailboxType: MailboxType,
modifier: Modifier = Modifier,
isFullScreen: Boolean = false
) {
...
- Dentro del elemento
ReplyEmailDetailsCard
componible, 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 elementoReplyHomeScreen
componible, pasa un valortrue
para el parámetroisFullScreen
cuando crees el elementoReplyDetailsScreen
como 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 concreateAndroidComposeRule
y pasaComponentActivity
como el parámetro de tipo. Se usaComponentActivity
para 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
testTag
para el métodotestTag
deModifier
en el elementoReplyBottomNavigationBar
componible.
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 decomposeTestRule
con el elemento componibleReplyApp
y pasaWindowWidthSizeClass.Compact
como 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
onNodeWithTagForStringId
encomposeTestRule
, 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
Modifier
en el elementoPermanentNavigationDrawer
componible.
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
Modifier
en el elementoReplyNavigationRail
componible.
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 concreateAndroidComposeRule
y pasaComponentActivity
como 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
stateRestorationTester
pasandocomposeTestRule
como argumento aStateRestorationTester
. - Usa
setContent()
con el elemento componibleReplyApp
y pasa el elementoWindowWidthSizeClass.Compact
como el argumentowindowSize
.
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
stateRestorationTester
pasandocomposeTestRule
como argumento aStateRestorationTester
. - Usa
setContent()
con el elemento componibleReplyApp
y pasa el elementoWindowWidthSizeClass.Expanded
como el argumentowindowSize
.
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
testTag
en 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
,TestMediumWidth
yTestExpandedWidth
.
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
TestCompactWidth
después de la anotación de prueba para una prueba compacta enReplyAppTest
yReplyAppStateRestorationTest
.
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
TestMediumWidth
despué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
TestExpandedWidth
después de la anotación de prueba para una prueba expandida enReplyAppTest
yReplyAppStateRestorationTest
.
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.