Navegar com módulos de recursos

A biblioteca Dynamic Navigator estende a funcionalidade do componente de navegação do Jetpack para funcionar com destinos definidos em módulos de recursos. Essa biblioteca também oferece a instalação simples de módulos de recursos sob demanda ao navegar até esses destinos.

Configurar

Para oferecer compatibilidade com módulos de recursos, use as seguintes dependências no arquivo build.gradle do módulo do app:

Groovy

dependencies {
    def nav_version = "2.7.7"

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

    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")
}

Observe que as outras dependências de navegação precisam usar configurações de API para que fiquem disponíveis para os módulos de recursos.

Uso básico

Para oferecer suporte a módulos de recursos, primeiro mude todas as instâncias de NavHostFragment no app para 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"
    ... />

Em seguida, adicione um atributo app:moduleName para qualquer destino <activity>, <fragment> ou <navigation> nos gráficos de navegação de um módulo com.android.dynamic-feature associados a um DynamicNavHostFragment. Esse atributo informa à biblioteca Dynamic Navigator que o destino pertence a um módulo de recurso com o nome especificado por você.

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

Quando você navega para um desses destinos, a biblioteca Dynamic Navigator confere primeiro se o módulo de recursos está instalado. Se o módulo de recursos já estiver presente, o app navegará para o destino conforme o esperado. Se o módulo não estiver presente, o app mostrará um destino de fragmento de progresso intermediário à medida que o módulo é instalado. A implementação padrão do fragmento de progresso mostra uma IU básica com uma barra de progresso e processa todos os erros de instalação.

duas telas de carregamento que mostram a IU com uma barra de progresso ao navegar
         em um módulo de recursos pela primeira vez.
Figura 1. IU mostrando uma barra de progresso quando um usuário navega para um recurso sob demanda pela primeira vez. O app exibirá essa tela durante o download do módulo correspondente.

Para personalizar essa IU ou processar manualmente o progresso da instalação na tela do seu app, consulte as seções Personalizar o fragmento de progresso e Monitorar o estado da solicitação neste tópico.

Os destinos que não especificam app:moduleName continuam funcionando sem alterações e se comportam como se o app estivesse usando um NavHostFragment normal.

Personalizar o fragmento de progresso

É possível substituir a implementação do fragmento de progresso para cada gráfico de navegação definindo o atributo app:progressDestination como o ID do destino que você quer usar para processar o progresso da instalação. Seu destino de progresso personalizado precisa ser um Fragment derivado de AbstractProgressFragment. É preciso modificar os métodos abstratos para notificações sobre o progresso da instalação, erros e outros eventos. É possível mostrar o progresso da instalação em uma IU de sua escolha.

A classe DefaultProgressFragment da implementação padrão usa essa API para mostrar o progresso da instalação.

Monitorar o estado da solicitação

A biblioteca do Dynamic Navigator permite implementar um fluxo de UX semelhante ao das Práticas recomendadas de UX para entrega on demand, em que o usuário continua no contexto de uma tela anterior enquanto aguarda a conclusão da instalação. Isso significa que você não precisa exibir uma IU intermediária ou um fragmento de progresso.

tela que mostra uma barra de navegação inferior com um ícone que indica
         o download de um módulo de recursos.
Figura 2. Tela que mostra o progresso do download de uma barra de navegação na parte inferior.

Nesse cenário, você é responsável por monitorar e processar todos os estados de instalação, mudanças de progresso, erros e assim por diante.

Para iniciar esse fluxo de navegação sem bloqueio, transmita um objeto DynamicExtras que contenha um DynamicInstallMonitor para NavController.navigate(), como mostrado no exemplo a seguir:

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);
)

Imediatamente após chamar navigate(), verifique o valor de installMonitor.isInstallRequired para ver se a tentativa de navegação resultou em uma instalação do módulo de recurso.

  • Se o valor for false, você estará navegando para um destino normal e não precisará fazer mais nada.
  • Se o valor for true, comece a observar o objeto LiveData que agora está em installMonitor.status. Esse objeto LiveData emite atualizações SplitInstallSessionState da biblioteca Play Core. Essas atualizações contêm eventos de progresso de instalação que podem ser usados para atualizar a IU. Não esqueça de processar todos os status relevantes conforme descrito no Guia da Play Core, incluindo a solicitação de confirmação do usuário se necessário.

    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);
              }
          }
      });
    }
    

Quando a instalação for concluída, o objeto LiveData emite um status SplitInstallSessionStatus.INSTALLED. Em seguida, chame NavController.navigate() novamente. Como agora o módulo está instalado, a chamada será bem-sucedida e o app navegará para o destino conforme o esperado.

Depois de alcançar um estado terminal, como quando a instalação é concluída ou quando a instalação falha, remova o observador LiveData para evitar vazamentos de memória. É possível verificar se o status representa um estado de terminal usando SplitInstallSessionStatus.hasTerminalStatus().

Consulte AbstractProgressFragment para ver um exemplo de implementação desse observador.

Gráficos incluídos

A biblioteca Dynamic Navigator é compatível com a inclusão de gráficos definidos em módulos de recursos. Para incluir um gráfico definido em um módulo de recursos, faça o seguinte:

  1. Use <include-dynamic/> em vez de <include/>, como mostrado no exemplo a seguir:

    <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 ... />, você precisa especificar os seguintes atributos:

    • app:graphResName: o nome do arquivo de recursos do gráfico de navegação. O nome é derivado do nome do arquivo do gráfico. Por exemplo, se o gráfico estiver em res/navigation/nav_graph.xml, o nome do recurso será nav_graph.
    • android:id: ID do gráfico de destino. A biblioteca Dynamic Navigator ignora todos os valores android:id encontrados no elemento raiz do gráfico incluído.
    • app:moduleName: nome do pacote do módulo.

Usar o graphPackage correto

Se o app:graphPackage correto não for usado, o componente de navegação não poderá incluir a navGraph especificada no módulo do recurso.

O nome do pacote para um módulo de recurso dinâmico é composto pelo nome do módulo unido ao applicationId do módulo do app base. Assim, se o módulo do app base tiver com.example.dynamicfeatureapp como a propriedade applicationId, e o módulo do recurso dinâmico for chamado de DynamicFeatureModule, o nome do pacote para o módulo dinâmico será com.example.dynamicfeatureapp.DynamicFeatureModule. Esse nome de pacote diferencia maiúsculas de minúsculas.

Em caso de dúvida, confira o AndroidManifest.xml gerado para confirmar o nome do pacote para o módulo do recurso. Depois de criar o projeto, acesse <DynamicFeatureModule>/build/intermediates/merged_manifest/debug/AndroidManifest.xml, que terá a seguinte aparência:

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

O valor de featureSplit precisa ser igual ao nome do módulo do recurso dinâmico, e o pacote corresponderá ao applicationId do módulo do app base. O app:graphPackage é a combinação destes dois: com.example.dynamicfeatureapp.DynamicFeatureModule.

Só é possível acessar o startDestination de um gráfico de navegação include-dynamic. O módulo dinâmico é responsável pelo próprio gráfico de navegação, e o app base não tem conhecimento disso.

O mecanismo de inclusão dinâmica permite que o módulo do app base inclua um gráfico de navegação aninhado definido no módulo dinâmico. Esse gráfico se comporta como qualquer outro gráfico de navegação aninhado. O gráfico de navegação raiz, ou seja, o pai do gráfico aninhado, só pode definir o gráfico de navegação aninhado como um destino, e não como os filhos dele. Assim, o startDestination é usado quando o gráfico include-dynamicnavigation é o destino.

Limitações

  • Os gráficos incluídos dinamicamente não são compatíveis com links diretos.
  • Gráficos aninhados carregados dinamicamente (ou seja, um elemento <navigation> com um app:moduleName) não são compatíveis com links diretos atualmente.