Cómo navegar con módulos de funciones

La biblioteca de Dynamic Navigator extiende la funcionalidad del componente de Jetpack Navigation para trabajar con destinos definidos en los módulos de funciones. Esta biblioteca también proporciona una instalación sin interrupciones de módulos de funciones a pedido cuando navegas a estos destinos.

Configuración

Para admitir módulos de funciones, usa las siguientes dependencias en el archivo build.gradle del módulo de tu app:

Groovy

dependencies {
    def nav_version = "2.8.0"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
    api "androidx.navigation:navigation-ui-ktx:$nav_version"
    api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.8.0"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
    api("androidx.navigation:navigation-ui-ktx:$nav_version")
    api("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")
}

Ten en cuenta que las otras dependencias de Navigation deben usar configuraciones de API para que estén disponibles para tus módulos de funciones.

Uso básico

Para admitir módulos de funciones, primero cambia todas las instancias de NavHostFragment en tu app por androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment:

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
    app:navGraph="@navigation/nav_graph"
    ... />

Luego, agrega un atributo app:moduleName a cualquier destino <activity>, <fragment> o <navigation> en los gráficos de navegación del módulo com.android.dynamic-feature que estén asociados con un objeto DynamicNavHostFragment. Este atributo indicará a la biblioteca de Dynamic Navigator que el destino pertenece a un módulo de funciones con el nombre que especifiques.

<fragment
    app:moduleName="myDynamicFeature"
    android:id="@+id/featureFragment"
    android:name="com.google.android.samples.feature.FeatureFragment"
    ... />

Cuando navegas a uno de estos destinos, la biblioteca de Dynamic Navigator primero comprueba si se instaló el módulo de funciones. Si el módulo de funciones ya está presente, la app navega al destino como se espera. Si el módulo no está presente, la app muestra un destino de fragmento de progreso intermedio mientras instala el módulo. La implementación predeterminada del fragmento de progreso muestra una IU básica con una barra de progreso y controla cualquier error de instalación.

Dos pantallas de carga que muestran la IU con una barra de progreso cuando navegas a un módulo de funciones por primera vez
Figura 1: IU que muestra una barra de progreso cuando un usuario navega a una función a pedido por primera vez La app muestra esta pantalla mientras se descarga el módulo correspondiente.

Para personalizar esta IU o controlar de forma manual el progreso de la instalación desde tu propia pantalla de app, consulta las secciones Personaliza el fragmento de progreso y Supervisa el estado de la solicitud en este tema.

Los destinos que no especifican app:moduleName continúan funcionando sin cambios y se comportan como si tu app usara un NavHostFragment normal.

Personaliza el fragmento de progreso

Puedes anular la implementación del fragmento de progreso para cada gráfico de navegación si configuras el atributo app:progressDestination con el ID del destino que deseas usar para manejar el progreso de la instalación. Tu destino de progreso personalizado debe ser un Fragment que derive de AbstractProgressFragment. Debes anular los métodos abstractos para las notificaciones sobre el progreso de la instalación, los errores y otros eventos. Luego, puedes mostrar el progreso de la instalación en la IU que prefieras.

La clase DefaultProgressFragment de la implementación predeterminada usa esta API para mostrar el progreso de la instalación.

Supervisa el estado de la solicitud

La biblioteca de Dynamic Navigator te permite implementar un flujo de UX similar al de las prácticas recomendadas de UX para la entrega de funciones a pedido, en las que un usuario permanece en el contexto de una pantalla anterior mientras espera que finalice la instalación. Eso significa que no necesitas mostrar una IU intermedia ni un fragmento de progreso.

Pantalla que muestra una barra de navegación inferior con un ícono que indica que se descarga un módulo de funciones
Figura 2: Pantalla que muestra el progreso de la descarga desde la barra de navegación inferior

En esta situación, eres responsable de supervisar y controlar todos los estados de instalación, cambios en el progreso, errores, etcétera.

Para iniciar este flujo de navegación sin bloqueo, pasa un objeto DynamicExtras que contenga un DynamicInstallMonitor a NavController.navigate(), como se muestra en el siguiente ejemplo:

Kotlin

val navController = ...
val installMonitor = DynamicInstallMonitor()

navController.navigate(
    destinationId,
    null,
    null,
    DynamicExtras(installMonitor)
)

Java

NavController navController = ...
DynamicInstallMonitor installMonitor = new DynamicInstallMonitor();

navController.navigate(
    destinationId,
    null,
    null,
    new DynamicExtras(installMonitor);
)

Inmediatamente después de llamar a navigate(), debes verificar el valor de installMonitor.isInstallRequired para ver si la navegación que se intentó realizó la instalación de un módulo de funciones.

  • Si el valor es false, estás navegando a un destino normal y no necesitas hacer nada más.
  • Si el valor es true, debes comenzar a observar el objeto LiveData que ahora está en installMonitor.status. Este objeto LiveData emite actualizaciones de SplitInstallSessionState desde la biblioteca de Play Core. Estas actualizaciones contienen eventos de progreso de la instalación que puedes usar para actualizar la IU. Recuerda controlar todos los estados relevantes, como se describe en la guía de Play Core, incluida la solicitud de confirmación del usuario, si es necesario.

    Kotlin

    val navController = ...
    val installMonitor = DynamicInstallMonitor()
    
    navController.navigate(
      destinationId,
      null,
      null,
      DynamicExtras(installMonitor)
    )
    
    if (installMonitor.isInstallRequired) {
      installMonitor.status.observe(this, object : Observer<SplitInstallSessionState> {
          override fun onChanged(sessionState: SplitInstallSessionState) {
              when (sessionState.status()) {
                  SplitInstallSessionStatus.INSTALLED -> {
                      // Call navigate again here or after user taps again in the UI:
                      // navController.navigate(destinationId, destinationArgs, null, null)
                  }
                  SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> {
                      SplitInstallManager.startConfirmationDialogForResult(...)
                  }
    
                  // Handle all remaining states:
                  SplitInstallSessionStatus.FAILED -> {}
                  SplitInstallSessionStatus.CANCELED -> {}
              }
    
              if (sessionState.hasTerminalStatus()) {
                  installMonitor.status.removeObserver(this);
              }
          }
      });
    }
    

    Java

    NavController navController = ...
    DynamicInstallMonitor installMonitor = new DynamicInstallMonitor();
    
    navController.navigate(
      destinationId,
      null,
      null,
      new DynamicExtras(installMonitor);
    )
    
    if (installMonitor.isInstallRequired()) {
      installMonitor.getStatus().observe(this, new Observer<SplitInstallSessionState>() {
          @Override
          public void onChanged(SplitInstallSessionState sessionState) {
              switch (sessionState.status()) {
                  case SplitInstallSessionStatus.INSTALLED:
                      // Call navigate again here or after user taps again in the UI:
                      // navController.navigate(mDestinationId, mDestinationArgs, null, null);
                      break;
                  case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
                      SplitInstallManager.startConfirmationDialogForResult(...)
                      break;
    
                  // Handle all remaining states:
                  case SplitInstallSessionStatus.FAILED:
                      break;
                  case SplitInstallSessionStatus.CANCELED:
                      break;
              }
    
              if (sessionState.hasTerminalStatus()) {
                  installMonitor.getStatus().removeObserver(this);
              }
          }
      });
    }
    

Cuando finaliza la instalación, el objeto LiveData emite un estado SplitInstallSessionStatus.INSTALLED. Luego, deberías volver a llamar a NavController.navigate(). Como el módulo ahora está instalado, la llamada ahora se ejecuta correctamente y la app navega al destino según lo previsto.

Después de alcanzar el estado de una terminal, como cuando finaliza la instalación o cuando falla la instalación, debes quitar el observador de LiveData para evitar las pérdidas de memoria. Puedes verificar si el estado representa un estado de la terminal con SplitInstallSessionStatus.hasTerminalStatus().

Consulta AbstractProgressFragment para ver una implementación de ejemplo de este observador.

Gráficos incluidos

La biblioteca de Dynamic Navigator admite incluir gráficos que se definen en módulos de funciones. Para incluir un gráfico definido en un módulo de funciones, haz lo siguiente:

  1. Usa <include-dynamic/> en lugar de <include/>, como se muestra en el siguiente ejemplo:

    <include-dynamic
        android:id="@+id/includedGraph"
        app:moduleName="includedgraphfeature"
        app:graphResName="included_feature_nav"
        app:graphPackage="com.google.android.samples.dynamic_navigator.included_graph_feature" />
    
  2. Dentro de <include-dynamic ... />, debes especificar los siguientes atributos:

    • app:graphResName: Es el nombre del archivo de recursos del gráfico de navegación. El nombre se deriva del nombre de archivo del gráfico. Por ejemplo, si el gráfico está en res/navigation/nav_graph.xml, el nombre del recurso es nav_graph.
    • android:id: Es el ID del destino del gráfico. La biblioteca de Dynamic Navigator ignora cualquier valor android:id que se encuentre en el elemento raíz del gráfico incluido.
    • app:moduleName: Es el nombre del paquete del módulo.

Cómo usar el graphPackage correcto

Es importante obtener el app:graphPackage correcto, ya que, de lo contrario, el componente Navigation no podrá incluir el navGraph especificado del módulo de funciones.

El nombre de paquete de un módulo de funciones dinámicas se crea agregando el nombre del módulo al applicationId del módulo base de la app. Entonces, si el módulo base de la app tiene un applicationId de com.example.dynamicfeatureapp y el módulo de funciones dinámicas se llama DynamicFeatureModule, el nombre del paquete del módulo dinámico será com.example.dynamicfeatureapp.DynamicFeatureModule. En el nombre del paquete, se distingue entre mayúsculas y minúsculas.

Si tienes dudas, puedes confirmar el nombre del paquete del módulo de funciones consultando el AndroidManifest.xml generado. Después de compilar el proyecto, ve a <DynamicFeatureModule>/build/intermediates/merged_manifest/debug/AndroidManifest.xml, que debería tener el siguiente aspecto:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    featureSplit="DynamicFeatureModule"
    package="com.example.dynamicfeatureapp"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="30" />

    <dist:module
        dist:instant="false"
        dist:title="@string/title_dynamicfeaturemodule" >
        <dist:delivery>
            <dist:install-time />
        </dist:delivery>

        <dist:fusing dist:include="true" />
    </dist:module>

    <application />

</manifest>

El valor featureSplit debe coincidir con el nombre del módulo de funciones dinámicas, y el paquete coincidirá con el applicationId del módulo de base de la app. La app:graphPackage es la combinación de los siguientes elementos: com.example.dynamicfeatureapp.DynamicFeatureModule.

Solo es posible navegar al elemento startDestination de un gráfico de navegación include-dynamic. El módulo dinámico es responsable de su propio gráfico de navegación, pero la app de base no lo sabe.

El mecanismo de inclusión dinámica permite que el módulo base de la app incluya un gráfico de navegación anidado que se define dentro del módulo dinámico. Este gráfico de navegación anidado se comporta como cualquier otro de su tipo. El gráfico de navegación raíz (es decir, el superior del gráfico anidado) solo puede definir el gráfico de navegación anidado como un destino, y no sus elementos secundarios. Por lo tanto, startDestination se usa cuando el gráfico de include-dynamicnavigation es el destino.

Limitaciones

  • Por el momento, los gráficos con funciones dinámicas no admiten vínculos directos.
  • Por el momento, los gráficos anidados cargados de forma dinámica (es decir, un elemento <navigation> con app:moduleName) no admiten vínculos directos.