Cómo compilar una app adaptable con navegación dinámica

1. Introducción

Una de las grandes ventajas de desarrollar tu app en la plataforma de Android es la gran oportunidad de llegar a los usuarios en diferentes tipos de factores de forma, como wearables, plegables, tablets, computadoras de escritorio e incluso TVs. Cuando se utiliza una app, es posible que los usuarios deseen utilizar la misma aplicación en dispositivos con pantallas grandes para aprovechar el aumento de espacio. Cada vez más, los usuarios de Android utilizan sus apps en varios dispositivos de diferentes tamaños de pantalla y esperan que los usuarios tengan una experiencia de alta calidad en todos los dispositivos.

Hasta ahora, aprendiste a crear apps principalmente para dispositivos móviles. En este codelab, aprenderás a transformarlas para que se adapten a otros tamaños de pantalla. Usarás patrones de diseño de navegación adaptables y atractivos tanto para dispositivos móviles como para pantallas grandes, como plegables, tablets y computadoras de escritorio.

Requisitos previos

  • Conocimientos de programación de Kotlin, incluidas clases, funciones y condicionales
  • Conocimiento de clases ViewModel
  • Conocimientos sobre la creación de funciones Composables
  • Experiencia en la compilación de diseños con Jetpack Compose
  • Experiencia en la ejecución de apps en un dispositivo o emulador

Qué aprenderás

  • Cómo crear la navegación entre pantallas sin el gráfico de navegación para apps simples
  • Cómo crear un diseño de navegación adaptable con Jetpack Compose
  • Cómo crear un controlador de retroceso personalizado

Qué compilarás

  • Implementarás la navegación dinámica en la app de Reply existente para que su diseño se adapte a todos los tamaños de pantalla.

El producto final se verá como la siguiente imagen:

​​ Una ilustración de la app de Reply al final de este codelab con un panel lateral de navegación que se muestra a la izquierda. El panel lateral de navegación muestra 4 pestañas que el usuario puede seleccionar: "Recibidos", "Enviados", "Borradores" y "Spam". A la derecha del panel lateral de navegación, se muestra una lista de correos electrónicos de ejemplo.

Requisitos

  • Una computadora con acceso a Internet, un navegador web y Android Studio
  • Acceso a GitHub

2. Descripción general de la app

Introducción a la app de Reply

La app de Reply es multipantalla, similar a un cliente de correo electrónico.

La app de Reply aparece en el modo de teléfono. Se muestra una lista de correos electrónicos de ejemplo que el usuario puede leer. En la parte inferior de la pantalla, hay cuatro íconos que representan las carpetas de correos recibidos y enviados, los borradores y el spam.

Contiene 4 categorías diferentes que se muestran en distintas pestañas: Recibidos, Enviados, Borradores y Spam.

Descarga el código de inicio

En Android Studio, abre la carpeta basic-android-kotlin-compose-training-reply-app.

  1. Navega a la página de repositorio de GitHub del proyecto.
  2. Verifica que el nombre de la rama coincida con el especificado en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

1e4c0d2c081a8fd2.png

  1. En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.

3. Explicación del código de inicio

Directorios importantes en la app de Reply

El directorio del archivo de la app de Reply muestra dos subdirectorios expandidos: "data" y "ui". En el directorio ui, se seleccionó MainActivity.kt. MainActivity.kt aparece al final de la lista de contenidos.

La capa de IU y datos del proyecto de la app de Reply se divide en directorios diferentes. ReplyViewModel, ReplyUiState y otros elementos de componibilidad se encuentran en el directorio ui. Las clases data y enum que definen la capa de datos y las clases de proveedores de datos se encuentran en el directorio data.

Inicialización de datos en la app de Reply

La app de Reply se inicializa con datos a través del método initilizeUIState() en el ReplyViewModel, que se ejecuta en la función init.

ReplyViewModel.kt

...
   init {
        initializeUIState()
    }

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

El elemento de componibilidad de nivel de pantalla

Al igual que con otras apps, la app de Reply usa el elemento de componibilidad ReplyApp como el principal donde se declaran viewModel y uiState. También se pasan varias funciones viewModel como argumentos lambda para el elemento de componibilidad ReplyHomeScreen.

ReplyApp.kt

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

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

Otros elementos de componibilidad

  • ReplyHomeScreen.kt: contiene los elementos de componibilidad de la pantalla principal, incluidos los elementos de navegación.
  • ReplyHomeContent.kt: contiene elementos de componibilidad que definen elementos más detallados de la pantalla principal.
  • ReplyDetailsScreen.kt: contiene elementos de componibilidad de pantalla y elementos más pequeños para la pantalla de detalles.

No dudes en revisar cada archivo en detalle a fin de comprender mejor los elementos de componibilidad antes de continuar con la siguiente sección del codelab.

4. Cómo cambiar de pantalla sin un gráfico de navegación

En la ruta anterior, aprendiste a usar una clase NavHostController para navegar de una pantalla a otra. Con Compose, también puedes cambiar pantallas con declaraciones condicionales simples si usas los estados mutables del tiempo de ejecución. Esto es especialmente útil en aplicaciones pequeñas como la app de Reply, en la que solo quieres alternar entre dos pantallas.

Cómo cambiar de pantalla con cambios de estado

En Compose, las pantallas se recomponen cuando se produce un cambio de estado. Puedes cambiar las pantallas mediante condicionales simples para responder a los cambios de estado.

Usarás condicionales a fin de mostrar el contenido de la pantalla principal cuando el usuario esté en ella y la de detalles cuando el usuario no lo esté.

Modifica la app de Reply a los efectos de permitir cambios de pantalla cuando cambie el estado. Para ello, completa los siguientes pasos:

  1. Abre el código de inicio en Android Studio.
  2. En el elemento de componibilidad ReplyHomeScreen en ReplyHomeScreen.kt, une el elemento ReplyAppContent con una sentencia if para cuando la propiedad isShowingHomepage del objeto replyUiState sea true.

ReplyHomeScreen.kt

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

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

        )
    }
}

Ahora debes tener en cuenta la situación cuando el usuario no esté en la pantalla principal mostrando la pantalla de detalles.

  1. Agrega una rama else con el elemento de componibilidad ReplyDetailsScreen en su cuerpo. Agrega replyUIState, onDetailScreenBackPressed y modifier como argumentos para el elemento de componibilidad ReplyDetailsScreen.

ReplyHomeScreen.kt

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

...

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

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

El objeto replyUiState es un objeto de estado. Por lo tanto, cuando hay un cambio en la propiedad isShowingHomepage del objeto replyUiState, el elemento de componibilidad ReplyHomeScreen se vuelve a componer y la sentencia if/else se vuelve a evaluar en el tiempo de ejecución. Este enfoque admite la navegación entre diferentes pantallas sin el uso de una clase NavHostController.

Una ilustración animada de la app de Reply en un emulador de teléfono muestra los cambios de la pantalla principal a la página de detalles. La pantalla principal muestra una lista de correos electrónicos con los 4 íconos de mensajes en la parte inferior (recibidos, enviados, borradores y spam). La página de detalles muestra el texto completo de un correo electrónico de ejemplo y los botones Responder y Responder a todos debajo.

Cómo crear un controlador de retroceso personalizado

Una ventaja de usar el elemento de componibilidad NavHost para cambiar de pantalla es que las instrucciones de las pantallas anteriores se guardan en la pila de actividades. Estas pantallas guardadas permiten que el botón Atrás del sistema navegue fácilmente a la pantalla anterior cuando se lo invoca. Dado que la app de Reply no usa un NavHost, debes agregar el código para controlar el botón Atrás de forma manual. Lo harás a continuación.

Completa los siguientes pasos a fin de crear un controlador de retroceso personalizado en la app de Reply:

  1. En la primera línea del elemento de componibilidad ReplyDetailsScreen, agrega un elemento BackHandler.
  2. Llama a la función onBackPressed() en el cuerpo del elemento de componibilidad BackHandler.

ReplyDetailsScreen.kt

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

5. Cómo ejecutar la app en dispositivos con pantallas grandes

Cómo verificar tu app con el emulador de tamaño variable

A fin de crear apps usables, los desarrolladores deben comprender la experiencia de sus usuarios en varios factores de forma. Por lo tanto, debes probar apps en varios de esos factores desde el comienzo del proceso de desarrollo.

Para lograr este objetivo, puedes usar muchos emuladores de diferentes tamaños de pantalla. Sin embargo, hacerlo puede resultar engorroso, en especial cuando compilas contenido para diferentes tamaños de pantalla a la vez. Es posible que también debas probar cómo una app en ejecución responde a los cambios de tamaño de la pantalla, como los cambios de orientación, los de tamaño de ventana en computadoras de escritorio y los de estado de plegado en dispositivos plegables.

Android Studio te permite probar estas situaciones con la introducción del emulador de tamaño variable.

Completa los siguientes pasos para configurar el emulador de tamaño variable:

  1. Asegúrate de ejecutar Android Studio Chipmunk | 2021.2.1 o una versión posterior.
  2. En Android Studio, selecciona Tools > Device Manager.

El menú Tools muestra una lista de opciones. Device Manager aparece seleccionado a mitad de la lista.

  1. En el Administrador de dispositivos, haz clic en Crear dispositivo. La barra de herramientas del Administrador del dispositivo muestra dos opciones de menú: "Virtual" y "Físico". Debajo de estas opciones, aparece el botón Crear dispositivo.
  2. Selecciona la categoría Phone y el dispositivo Resizable (Experimental).
  3. Haz clic en Siguiente.

La ventana del Administrador de dispositivos muestra un mensaje para elegir una definición de dispositivo. Aparecerá una lista de opciones con un campo de búsqueda encima. Se seleccionó la categoría "Phone" y el nombre de definición del dispositivo "Resizable (Experimental)".

  1. Selecciona el nivel de API 33.
  2. Haz clic en Siguiente.

La ventana Virtual Device Configuration muestra un mensaje para seleccionar una imagen del sistema. Se selecciona la API de Tiramisu.

  1. Asigna un nombre al nuevo dispositivo virtual de Android.
  2. Haz clic en Finish.

Se muestra la pantalla Virtual Configuration en Android Virtual Device (AVD). La pantalla de configuración incluye un campo de texto para ingresar el nombre del AVD. Debajo del campo de nombre, verás una lista de opciones de dispositivo, como la definición del dispositivo (Resizable Experimental), la imagen del sistema (Tiramisu) y la orientación (la orientación vertical está seleccionada de forma predeterminada). Los botones que dicen "Cambiar" aparecen a la derecha de la definición del dispositivo y de la información de la imagen del sistema, y la opción de orientación horizontal se encuentra a la derecha de la de orientación vertical seleccionada. En la esquina inferior derecha, se encuentran 4 botones: Cancelar, Anterior, Siguiente (que está inhabilitado y no se puede seleccionar) y Finalizar.

Cómo ejecutar la app en un emulador de pantallas grandes

Ahora que tienes la configuración del emulador de tamaño variable, veamos cómo se ve la app en una pantalla grande.

  1. Ejecuta la app en el emulador de tamaño variable.
  2. Selecciona Tablet en el modo de visualización.

Un emulador de tamaño variable muestra la app de Reply en la pantalla de un teléfono. La app muestra una lista de mensajes con 4 íconos en la parte inferior de la pantalla para Recibidos, Enviados, Borradores y Spam.

  1. Inspecciona la app en el modo tablet en modo horizontal.

Un emulador de tamaño variable muestra la app de Reply en una pantalla de tablet, con el cuerpo alargado. En la parte inferior de la pantalla, aparecen los íconos de Recibidos, Enviados, Borradores y Spam.

Observa que la pantalla de la tablet se alarga horizontalmente. Si bien esta orientación funciona, es posible que no sea el mejor uso del espacio en pantallas grandes. Analicemos eso a continuación.

Cómo diseñar para pantallas grandes

Lo primero que piensas cuando miras esta app en una tablet es que no está bien diseñada y no resulta atractiva. Tienes toda la razón: este diseño no está creado para pantallas grandes.

Cuando diseñes para pantallas grandes, como tablets y plegables, debes tener en cuenta la ergonomía y la proximidad de los dedos de los usuarios a la pantalla. Con los dispositivos móviles, los dedos de los usuarios pueden llegar con facilidad a la mayor parte de la pantalla. La ubicación de los elementos interactivos, como los botones y los elementos de navegación, no es tan importante. Sin embargo, en el caso de pantallas grandes, tener elementos interactivos esenciales en el medio de ellas puede dificultar su alcance.

Como puedes ver en la app de Reply, diseñar para pantallas grandes no consiste solo en estirar ni ampliar elementos de la IU de modo que se ajusten a la pantalla. Es una oportunidad de usar el mayor espacio a fin de crear una experiencia diferente para sus usuarios. Por ejemplo, puedes agregar otro diseño en la misma pantalla, y así evitar la necesidad de navegar a otra, o realizar varias tareas al mismo tiempo.

La app de Reply muestra la pantalla de detalles en la página principal junto con el panel lateral de navegación y la lista de direcciones de correo electrónico. A la derecha de la lista de correos electrónicos, aparecerá un correo electrónico de ejemplo. Los botones Responder y Responder a todos aparecen debajo del correo electrónico de muestra.

Este diseño puede aumentar la productividad de los usuarios y fomenta una mayor participación. Pero antes de implementar este diseño, debes aprender a crear diferentes diseños para distintos tamaños de pantalla.

6. Cómo hacer que tu diseño se adapte a diferentes tamaños de pantalla

¿Qué son los puntos de interrupción?

Es posible que te preguntes cómo mostrar diferentes diseños para la misma app. La respuesta corta es: usando condicionales en diferentes estados, como lo hiciste al comienzo de este codelab.

Para crear una app adaptable, necesitas que el diseño cambie en función del tamaño de la pantalla. El punto de medición en el que cambia un diseño se conoce como punto de interrupción. Material Design creó un rango de puntos de interrupción bien definidos que abarca la mayoría de las pantallas de Android.

Una tabla muestra el rango de puntos de interrupción (en dp) para diferentes tipos de dispositivos y configuraciones. El rango de 0 a 599 dp se usa para teléfonos celulares en modo Retrato, teléfonos con orientación horizontal, tamaño de ventana compacto, 4 columnas y 8 márgenes mínimos. El rango de 600 a 839 dp se usa para tablets pequeñas y plegables en modo Retrato u horizontal, clase de tamaño de ventana mediana, 12 columnas y 12 márgenes mínimos. El tamaño de 840 dp o más se usa para tablets grandes en modo Retrato u horizontal, la clase de tamaño de ventana expandida, 12 columnas y 32 márgenes mínimos. Las notas de la tabla indican que los márgenes y canales son flexibles y no necesitan ser del mismo tamaño, y que los teléfonos en orientación horizontal se consideran una excepción que se ajustan al rango de puntos interrupción de 0 a 599 dp.

Esta tabla de rangos de puntos de interrupción muestra, por ejemplo, que si tu app se está ejecutando en un dispositivo con un tamaño de pantalla inferior a 600 dp, debes mostrar el diseño para dispositivos móviles.

Cómo usar clases de tamaño de ventana

La API de WindowSizeClass que se introdujo para Compose facilita la implementación de los puntos de interrupción de Material Design.

Las clases de tamaño de ventana presentan tres categorías de tamaños: compacto, mediano y expandido, tanto para el ancho como para la altura.

El diagrama representa las clases de tamaño de ventana basadas en el ancho. El diagrama representa las clases de tamaño de ventana basadas en la altura.

Completa los siguientes pasos a fin de implementar la API de WindowSizeClass en la app de Reply:

  1. Agrega la dependencia material3-window-size-class al archivo build.gradle del módulo.

build.gradle

...
dependencies {
...
"androidx.compose.material3:material3-window-size-class:$material3_version"
...
  1. Haz clic en Sync Now para sincronizar Gradle después de agregar la dependencia.

El botón Sync Now se muestra debajo de las pestañas para seleccionar diferentes archivos .kt y .gradle. A la derecha del botón Sync Now, hay otro que dice "Ignore these changes".

Con el archivo build.grade actualizado, ahora puedes crear una variable que almacene el tamaño de la ventana de la app en cualquier momento.

  1. En la función onCreate() del archivo MainActivity.kt, asigna el método calculateWindowSizeClass con el contexto this pasado en el parámetro a una variable llamada windowSize.
  2. Importa el paquete calculateWindowSizeClass adecuado.

MainActivity.kt

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

...

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

        setContent {
            ReplyTheme {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()

...
  1. Observa el subrayado rojo de la sintaxis calculateWindowSizeClass, que muestra la bombilla roja. Haz clic en la bombilla roja a la izquierda de la variable windowSize y selecciona Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate' para crear una anotación sobre el método onCreate().

En el código, se selecciona la línea "val windowSize = calculateWindowSizeClass(this)" y se muestra el ícono de bombilla a la izquierda de la línea de código. Debajo de la bombilla seleccionada, hay una lista de opciones para abordar el error, con la opción "Opt in for 'ExperimentalMaterial3WindowSizeClassApi' on 'onCreate'" seleccionada.

Puedes usar la variable WindowWidthSizeClass en MainActivity.kt a fin de determinar qué diseño se mostrará en varios elementos de componibilidad. Preparemos el elemento ReplyApp para recibir este valor.

  1. En el archivo ReplyApp.kt, modifica el elemento de componibilidad ReplyApp de modo que acepte WindowWidthSizeClass como parámetro y, luego, importa el paquete correspondiente.

ReplyApp.kt

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

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...
  1. Pasa la variable windowSize al componente ReplyApp en el método onCreate() del archivo MainActivity.kt.

MainActivity.kt

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

También debes actualizar la vista previa de la app para el parámetro windowSize.

  1. Pasa WindowWidthSizeClass.Compact como parámetro windowSize al elemento de componibilidad ReplyApp para el componente de vista previa y, luego, importa el paquete correspondiente.

MainActivity.kt

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

@Preview(showBackground = true)
@Composable
fun ReplyAppPreview() {
    ReplyTheme {
        ReplyApp(
            windowSize = WindowWidthSizeClass.Compact,
        )
    }
}
  1. A fin de cambiar los diseños de la app según el tamaño de la pantalla, agrega una sentencia when en el elemento de componibilidad ReplyApp en función del valor WindowWidthSizeClass.

ReplyApp.kt

...

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

    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
        }
        WindowWidthSizeClass.Medium -> {
        }
        WindowWidthSizeClass.Expanded -> {
        }
        else -> {
        }
    }
...

En este punto, estableciste una base para usar valores WindowSizeClass a los efectos de cambiar los diseños en tu app. El siguiente paso es determinar cómo quieres que se vea tu app en diferentes tamaños de pantalla.

7. Cómo implementar el diseño de navegación adaptable

Cómo implementar la navegación de IU adaptable

Actualmente, se usa la navegación inferior para todos los tamaños de pantalla.

Navegación inferior de la app de Reply.

Como se mencionó con anterioridad, este elemento de navegación no es ideal, ya que los usuarios pueden tener dificultades a la hora de acceder a estos elementos esenciales de navegación en pantallas más grandes. Afortunadamente, existen patrones recomendados para diferentes elementos de este tipo en varias clases de tamaño de ventana de navegación para IU responsivas. En la app de Reply, puedes implementar los siguientes elementos:

En una tabla, se enumeran las clases de tamaño de ventana y los elementos que se muestran. El ancho compacto muestra una barra de navegación inferior. El ancho mediano muestra un riel de navegación. El ancho expandido muestra un panel lateral de navegación persistente y vanguardista.

El riel de navegación es otro componente de navegación de Material Design que permite acceder a opciones de navegación compactas para destinos principales desde un lado de la app.

Un ejemplo de un riel de navegación en la app de Reply muestra 4 íconos de forma vertical: Recibidos, Enviados, Borradores y Spam.

Del mismo modo, Material Design crea un panel lateral de navegación persistente/permanente como otra opción a los efectos de proporcionar acceso ergonómico a pantallas más grandes.

Un panel lateral de navegación permanente en la app de Reply muestra 4 pestañas de forma vertical con íconos y los nombres de cada pestaña: Recibidos, Enviados, Borradores y Spam.

Cómo implementar un panel lateral de navegación

Si quieres crear un panel lateral de navegación para pantallas expandidas, puedes usar el parámetro navigationType. Para hacerlo, completa los siguientes pasos:

  1. A fin de representar diferentes tipos de elementos de navegación, crea un archivo WindowStateUtils.kt en un paquete nuevo utils, que se encuentra en el directorio ui.
  2. Agrega una clase Enum para representar diferentes tipos de elementos de navegación.

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}

A fin de implementar con éxito el panel lateral de navegación, debes determinar el tipo de navegación según el tamaño de la ventana de la app.

  1. En el elemento de componibilidad ReplyApp, crea una variable navigationType y asígnale el valor ReplyNavigationType adecuado en función del tamaño de la pantalla en la sentencia when.

ReplyApp.kt

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

Puedes usar el valor navigationType en el elemento de componibilidad ReplyHomeScreen. Puedes prepararte si lo conviertes en un parámetro para el elemento de componibilidad.

  1. En el elemento ReplyHomeScreen, agrega navigationType como parámetro.

ReplyHomeScreen.kt

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

...

  1. Pasa el navigationType al elemento de componibilidad ReplyHomeScreen.

ReplyApp.kt

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

A continuación, puedes crear una rama que exhiba el contenido de la app con un panel lateral de navegación cuando el usuario abra la app en una pantalla expandida y muestre la pantalla principal.

  1. En el cuerpo del elemento de componibilidad ReplyHomeScreen, agrega una sentencia if para la condición navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage.

ReplyHomeScreen.kt

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

    if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
...
  1. A fin de crear el panel lateral permanente, crea el elemento de componibilidad PermanentNavigationDrawer en el cuerpo de la sentencia if y agrega el elemento NavigationDrawerContent como entrada para el parámetro drawerContent.
  2. Agrega el elemento de componibilidad ReplyAppContent como argumento lambda final de PermanentNavigationDrawer.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    }

...
  1. Agrega una rama else que use el cuerpo del elemento de componibilidad anterior a fin de mantener la ramificación previa para pantallas no expandidas.

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                NavigationDrawerContent(
                    selectedDestination = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier

            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
}
...
  1. Agrega una anotación experimental al elemento de componibilidad ReplyHomeScreen. La necesitas, ya que la API de PermanentNavigationDrawer aún es experimental.

ReplyHomeScreen.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit = {},
    onEmailCardPressed: (Email) -> Unit = {},
    onDetailScreenBackPressed: () -> Unit = {},
    modifier: Modifier = Modifier
) {
...
  1. Ejecuta la app en el modo Tablet. Deberías ver la siguiente pantalla:

La app de Reply se muestra en modo tablet con el panel lateral de navegación en el lado izquierdo de la pantalla y la lista de direcciones de correo electrónico a su derecha.

Cómo implementar un riel de navegación

Al igual que la implementación del panel lateral de navegación, debes usar el parámetro navigationType a los efectos de alternar entre elementos de navegación.

Primero, agregaremos un riel de navegación para pantallas medianas.

  1. Comienza con la preparación del elemento de componibilidad ReplyAppContent. A tal fin, agrega navigationType como parámetro.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
...
  1. Pasa el valor navigationType a ambos elementos de componibilidad ReplyAppContent.

ReplyHomeScreen.kt

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

A continuación, agregaremos ramas, que permiten a la app mostrar rieles de navegación para algunas situaciones.

  1. En la primera línea del cuerpo del elemento de componibilidad ReplyAppContent, une el elemento de componibilidad ReplyNavigationRail alrededor del elemento de componibilidad AnimatedVisibility y establece el parámetro visibility en true si el valor de ReplyNavigationType es NavigationRail.

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit) = {},
    onEmailCardPressed: (Email) -> Unit = {},
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()            .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList

            )
        }

}
...
  1. Para alinear correctamente los elementos de componibilidad, une los elementos de componibilidad AnimatedVisibility y Column que se encuentran en el cuerpo de ReplyAppContent en un elemento Row.

ReplyHomeScreen.kt

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

            )
        }
    }
}
...

Por último, asegurémonos de que la navegación inferior se muestre en algunas situaciones.

  1. Después del elemento de componibilidad ReplyListOnlyContent, une el elemento ReplyBottomNavigationBar con un elemento AnimatedVisibility.
  2. Establece el parámetro visible cuando el valor de ReplyNavigationType sea BOTTOM_NAVIGATION.

ReplyHomeScreen.kt

...
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
            )
            AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
                ReplyBottomNavigationBar(
                    currentTab = replyUiState.currentMailbox,
                    onTabPressed = onTabPressed,
                    navigationItemContentList = navigationItemContentList
                )
            }
...
  1. Ejecuta la app en el modo de dispositivo plegable desplegado. Deberías ver la siguiente pantalla:

La app de Reply se muestra en un plegable con el riel de navegación en el lado izquierdo de la pantalla y la lista de correos electrónicos a su derecha.

8. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar este comando de git:

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

cd basic-android-kotlin-compose-training-reply-app
git checkout nav-update

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.

9. Conclusión

¡Felicitaciones! Estás un paso más cerca de hacer que la app de Reply se adapte a todos los tamaños de pantalla implementando un diseño de navegación adaptable. Mejoraste la experiencia del usuario con muchos factores de forma de Android. En el siguiente codelab, mejorarás aún más tus habilidades de trabajo con apps adaptables mediante la implementación de diseño, pruebas y vistas previas de contenido adaptable.

No olvides compartir tu trabajo en redes sociales con el hashtag #AndroidBasics.

Más información