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.
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.