Cómo compilar una navegación responsiva

La navegación es la interacción del usuario con la IU de una aplicación para acceder a los destinos de contenido. Los principios de navegación de Android proporcionan lineamientos que te ayudan a crear una navegación intuitiva y uniforme en las apps.

Las IU responsivas o adaptables proporcionan destinos de contenido responsivo y, a menudo, incluyen diferentes tipos de elementos de navegación en respuesta a los cambios de tamaño de la pantalla (por ejemplo, una barra de navegación inferior en pantallas pequeñas, un riel de navegación en pantallas de tamaño mediano o un panel lateral de navegación persistente en pantallas grandes), pero las IU responsivas o adaptables deben cumplir con los principios de navegación.

El componente de Navigation de Jetpack implementa los principios de navegación y facilita el desarrollo de apps con IUs responsivas o adaptables.

Figura 1: Pantallas expandidas, medianas y compactas con panel lateral de navegación, riel y barra inferior.

Navegación responsiva de la IU

El tamaño de la ventana de visualización que ocupa una app afecta la ergonomía y la usabilidad. Las clases de tamaño de ventana te permiten determinar los elementos de navegación adecuados (como barras de navegación, rieles o paneles laterales) y colocarlos en el lugar donde el usuario pueda acceder más fácilmente. En los lineamientos de diseño de Material Design, los elementos de navegación ocupan un espacio persistente en el extremo inicial de la pantalla y pueden moverse al borde inferior cuando el ancho de la app es compacto. Los elementos de navegación que elijas dependen en gran medida del tamaño de la ventana de la app y de la cantidad de partes que debe contener el elemento.

Clase de tamaño de la ventana Pocos elementos Muchos elementos
ancho compacto barra de navegación inferior panel lateral de navegación (margen inicial o inferior)
ancho medio riel de navegación panel lateral de navegación (extremo inicial)
ancho expandido riel de navegación panel lateral de navegación persistente (extremo inicial)

Los archivos de recursos de diseño se pueden calificar por puntos de interrupción de clase de tamaño de ventana para usar diferentes elementos de navegación para diferentes dimensiones de pantalla.

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

Destinos de contenido responsivos

En una IU responsiva, el diseño de cada destino de contenido se adapta a los cambios del tamaño de la ventana. Tu app puede ajustar el espaciado de diseño, cambiar la posición de los elementos, agregar o quitar contenido, o cambiar los elementos de la IU, incluidos los elementos de navegación.

Cuando cada destino individual controla los eventos de cambio de tamaño, los cambios se aíslan de la IU. El resto del estado de la app, incluida la navegación, no se ve afectado.

La navegación no debería ocurrir como un efecto secundario de los cambios de tamaño de ventana. No crees destinos de contenido solo para que se adapten a diferentes tamaños de ventanas. Por ejemplo, no crees diferentes destinos de contenido para las diferentes pantallas de un dispositivo plegable.

La navegación a destinos de contenido como efecto secundario de los cambios de tamaño de ventana tiene los siguientes problemas:

  • El destino anterior (para el tamaño de ventana anterior) podría estar visible temporalmente antes de navegar al nuevo destino.
  • Para mantener la reversibilidad (por ejemplo, cuando se pliega y se despliega un dispositivo), se requiere la navegación para cada tamaño de ventana.
  • Mantener el estado de la aplicación entre destinos puede ser difícil, ya que la navegación puede destruir el estado cuando se abre la pila de actividades.

Además, es posible que tu app ni siquiera se encuentre en primer plano cuando se produzcan los cambios de tamaño de las ventanas. El diseño de tu app podría requerir más espacio que la app en primer plano y, cuando el usuario regrese a ella, es posible que todo el tamaño de la orientación y la ventana haya cambiado.

Si tu app requiere destinos de contenido únicos según el tamaño de la ventana, considera combinar los destinos relevantes en un solo destino que incluya diseños alternativos y adaptables.

Destinos de contenido con diseños alternativos

Como parte de un diseño responsivo o adaptable, un destino de navegación único puede tener diseños alternativos según el tamaño de la ventana de la app. Cada diseño ocupa toda la ventana, pero se presentan diseños diferentes para distintos tamaños de ventana (diseño adaptable).

Un ejemplo de versión canónica es la vista de detalles de lista. En el caso de los tamaños de ventana compactos, tu app muestra un diseño de contenido para la lista y otro para los detalles. Si se navega al destino de la vista de lista-detalles, primero se muestra solo el diseño de la lista. Cuando se selecciona un elemento de la lista, tu app muestra el diseño detallado y reemplaza la lista. Cuando se selecciona el control de retroceso, se muestra el diseño de la lista y reemplaza el detalle. Sin embargo, en el caso de los tamaños de ventana expandidos, la lista y los diseños detallados se muestran lado a lado.

SlidingPaneLayout te permite crear un solo destino de navegación que muestra dos paneles de contenido, uno al lado del otro, en pantallas grandes, pero solo un panel a la vez en pantallas pequeñas, como en teléfonos convencionales.

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

Consulta Cómo crear un diseño de doble panel para obtener información detallada para implementar un diseño de lista y detalles mediante SlidingPaneLayout.

Un gráfico de navegación

Para brindar una experiencia del usuario coherente en cualquier dispositivo o tamaño de ventana, usa un solo gráfico de navegación en el que el diseño de cada destino de contenido sea responsivo.

Si usas un gráfico de navegación diferente para cada clase de tamaño de ventana, cada vez que la app pase de una clase de tamaño a otra, debes determinar el destino actual del usuario en los otros gráficos, construir una pila de actividades y conciliar la información de estado que difiera entre los gráficos.

Host de navegación anidada

Tu app podría incluir un destino de contenido que tenga destinos de contenido propios. Por ejemplo, en un diseño de lista-detalles, el panel de detalles del elemento podría incluir elementos de la IU que navegan al contenido que reemplaza el detalle del elemento.

Para implementar este tipo de subnavegación, haz que el panel de detalles sea un host de navegación anidado con su propio gráfico de navegación que especifique los destinos a los que se accede desde el panel de detalles:

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

Esto es diferente de un gráfico de navegación anidada porque el gráfico de navegación del elemento NavHost anidado no está conectado al gráfico de navegación principal. Es decir, no puedes navegar directamente desde los destinos en un gráfico hasta los destinos en el otro.

Para obtener más información, consulta Gráficos de navegación anidada.

Estado preservado

Para proporcionar destinos de contenido responsivos, tu app debe preservar su estado cuando se rota o se pliega el dispositivo, o bien se cambia el tamaño de la ventana de la app. De forma predeterminada, los cambios de configuración como estos recrean las actividades, los fragmentos y la jerarquía de vistas de la app. La forma recomendada de guardar el estado de la IU es con un ViewModel, que persiste tras los cambios de configuración. (consulta Cómo guardar estados de la IU ).

Los cambios de tamaño deberían ser reversibles (por ejemplo, cuando el usuario rota el dispositivo y, luego, vuelve a rotarlo).

Los diseños responsivos o adaptables pueden mostrar contenido diferente en diferentes tamaños de ventana. Suelen guardar estados adicionales relacionados con el contenido, incluso si el estado no corresponde al tamaño de ventana actual. Por ejemplo, un diseño puede tener espacio para mostrar un widget adicional de desplazamiento solo en anchos más grandes de ventana. Si un evento de cambio de tamaño hace que el ancho de la ventana se vuelva demasiado pequeño, el widget se oculta. Cuando la app cambia de tamaño a sus dimensiones anteriores, el widget de desplazamiento vuelve a estar visible y se debe restablecer la posición de desplazamiento original.

Alcances de ViewModel

La guía para desarrolladores Cómo migrar al componente Navigation prescribe una arquitectura de actividad única en la que los destinos se implementan como fragmentos y sus modelos de datos se implementan mediante ViewModel.

Un ViewModel siempre tiene alcance en un ciclo de vida y, cuando este ciclo de vida termina de forma permanente, el ViewModel se borra y se puede descartar. El ciclo de vida en el que se define el alcance de ViewModel (y, por lo tanto, cuánto se puede compartir el objeto ViewModel) depende del delegado de propiedad que se use para obtener el ViewModel.

En el caso más simple, cada destino de navegación es un fragmento único con un estado de IU completamente aislado. Por lo tanto, cada fragmento puede usar el delegado de la propiedad viewModels() para obtener un ViewModel con alcance para ese fragmento.

Para compartir el estado de la IU entre fragmentos, define el alcance del elemento ViewModel en la actividad llamando a activityViewModels() en los fragmentos (el equivalente a Activity es solo viewModels()). De esta manera, la actividad y los fragmentos que se adjuntan a ella comparten la instancia ViewModel. Sin embargo, en una arquitectura de una sola actividad, este alcance ViewModel perdura de manera efectiva mientras dure la app, por lo que ViewModel permanece en la memoria incluso si no hay fragmentos que lo usen.

Supongamos que tu gráfico de navegación tiene una secuencia de destinos de fragmentos que representa un flujo de confirmación de la compra y el estado actual de toda la experiencia de confirmación de la compra se encuentra en un objeto ViewModel compartido entre los fragmentos. Determinar el alcance de ViewModel según la actividad no solo es demasiado amplio, sino que expone otro problema: si el usuario pasa por el flujo de confirmación de la compra de un pedido y, luego, lo vuelve a realizar en un segundo pedido, los dos pedidos usan la misma instancia de confirmación de la compra ViewModel. Antes de la confirmación de la compra del segundo pedido, debes borrar manualmente los datos del primer pedido. Cualquier error podría ser costoso para el usuario.

En cambio, puedes definir el alcance de ViewModel en un gráfico de navegación en el NavController actual. Crea un gráfico de navegación anidada para encapsular los destinos que forman parte del flujo de confirmación de la compra. Luego, en cada uno de esos destinos de fragmentos, usa el delegado de la propiedad navGraphViewModels() y pasa el ID del gráfico de navegación para obtener el ViewModel compartido. Esto garantiza que, una vez que el usuario salga del flujo de confirmación de la compra y el gráfico de navegación anidado esté fuera del alcance, la instancia correspondiente de ViewModel se descarte y no se use en la siguiente confirmación de la compra.

Alcance Delegado de la propiedad Puede compartir ViewModel con
Fragment Fragment.viewModels() Solo fragmento
Actividad Activity.viewModels() o Fragment.activityViewModels() La actividad y todos los fragmentos asociados a ella
Gráfico de navegación Fragment.navGraphViewModels() Todos los fragmentos en el mismo gráfico de navegación

Ten en cuenta que, si usas un host de navegación anidada (consulta la sección Host de navegación anidada), los destinos de ese host no pueden compartir instancias de ViewModel con destinos fuera del host cuando se usa navGraphViewModels(), ya que los gráficos no están conectados. En este caso, puedes usar el alcance de la actividad.

Recursos adicionales