機能モジュールを使用したナビゲーション

Dynamic Navigator ライブラリは、Jetpack Navigation コンポーネントの機能を拡張し、機能モジュールで定義されたデスティネーションと連携して動作するようにします。また、このライブラリを使用して、そうしたデスティネーションに移動したときにオンデマンド機能モジュールのシームレスなインストールを行うことができます。

セットアップ

機能モジュールをサポートするには、アプリ モジュールの build.gradle ファイルに次の依存関係を指定します。

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

他の Navigation 依存関係は、機能モジュールから利用できるようにするため、API 構成を使用する必要があります。

基本的な使用方法

機能モジュールをサポートするには、まずアプリ内の NavHostFragment のインスタンスをすべて 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"
    ... />

次に、DynamicNavHostFragment に関連付けられている、com.android.dynamic-feature モジュールのナビゲーション グラフ内の <activity><fragment>、または <navigation> デスティネーションに app:moduleName 属性を追加します。 この属性は、指定された名前を持つ機能モジュールにそのデスティネーションが属していることを Dynamic Navigator ライブラリに伝えます。

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

ユーザーがそうしたデスティネーションのいずれかに移動すると、Dynamic Navigator ライブラリは、まず機能モジュールがインストールされているかどうかを確認します。機能モジュールがすでに存在する場合、アプリは想定されたデスティネーションにナビゲートします。モジュールが存在しない場合、アプリはモジュールをインストールしながら、中間進行状況フラグメントのデスティネーションを表示します。進行状況フラグメントのデフォルト実装は、進行状況バーを含む基本 UI を表示し、すべてのインストール エラーを処理します。

ユーザーが初めて機能モジュールに移動したときに進行状況バーを含む UI を表示する 2 つの読み込み画面
図 1. ユーザーが初めてオンデマンド機能に移動したときに進行状況バーを表示する UI。アプリは、該当モジュールのダウンロード中にこの画面を表示します。

この UI をカスタマイズする場合、または独自のアプリ画面内でインストール進行状況を手動で処理する場合は、このトピックの進行状況フラグメントをカスタマイズするおよびリクエストの状態をモニタリングするをご覧ください。

app:moduleName が指定されていないデスティネーションは、アプリが通常の NavHostFragment を使用している場合と同様に、変更なしで動作します。

進行状況フラグメントをカスタマイズする

各ナビゲーション グラフの進行状況フラグメント実装をオーバーライドするには、インストール進行状況の処理に使用するデスティネーションの ID に app:progressDestination 属性を設定します。カスタムの進行状況デスティネーションは、AbstractProgressFragment から派生した Fragment にする必要があります。インストールの進行状況、エラー、その他のイベントについて通知するための抽象メソッドをオーバーライドする必要があります。それにより、独自の UI でインストール進行状況を表示できます。

デフォルト実装の DefaultProgressFragment クラスは、この API を使用してインストール進行状況を表示します。

リクエストの状態をモニタリングする

Dynamic Navigator ライブラリを使用して、オンデマンド配信の UX に関するおすすめの方法で説明されているのと同様の UX フローを実装できます。このフローでは、ユーザーはインストールが完了するのを待つ間、元の画面のコンテキストにとどまります。つまり、中間 UI や進行状況フラグメントを表示する必要は一切ありません。

機能モジュールがダウンロード中であることを示すアイコンをボトム ナビゲーション バーに表示する画面
図 2. ボトム ナビゲーション バーでダウンロードの進行状況を表示する画面

このシナリオでは、インストールの状態、進行状況の変化、エラーなどをすべて独自にモニタリングして処理する必要があります。

この非ブロッキング ナビゲーション フローを開始するには、DynamicInstallMonitor を含む DynamicExtras オブジェクトを NavController.navigate() に渡します。次の例をご覧ください。

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

navigate() を呼び出したらすぐに installMonitor.isInstallRequired の値をチェックし、試行したナビゲーションによって機能モジュールのインストールが行われたかどうかを確認します。

  • 値が false の場合は、正常なデスティネーションに移動しているので、それ以上何もする必要はありません。
  • 値が true の場合は、現在 installMonitor.status にある LiveData オブジェクトの監視を開始する必要があります。この LiveData オブジェクトは、Play Core ライブラリから SplitInstallSessionState の更新情報を出力します。この更新情報には、UI の更新に使用できるインストール進行状況イベントが含まれています。Play Core ガイドで概説されているすべての関連するステータスを必ず処理してください。必要な場合は、ユーザーの確認を求めるも参照してください。

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

インストールが完了すると、LiveData オブジェクトは SplitInstallSessionStatus.INSTALLED ステータスを出力します。このとき、NavController.navigate() を再度呼び出す必要があります。モジュールがすでにインストールされているので、呼び出しは成功し、アプリは想定されたデスティネーションにナビゲートします。

最終状態(インストールの完了やインストールの失敗など)に到達した後、メモリリークを回避するために LiveData オブザーバーを削除する必要があります。SplitInstallSessionStatus.hasTerminalStatus() を使用して、ステータスが最終状態を表しているかどうかを確認できます。

このオブザーバーの実装例については、AbstractProgressFragment をご覧ください。

インクルードされたグラフ

Dynamic Navigator ライブラリは、機能モジュールで定義されたグラフのインクルードをサポートします。機能モジュールで定義されたグラフをインクルードするには、次の手順を実施します。

  1. 次の例に示すように、<include/> の代わりに <include-dynamic/> を使用します。

    <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. <include-dynamic ... /> 内で次の属性を指定する必要があります。

    • app:graphResName: ナビゲーション グラフのリソース ファイルの名前。この名前はグラフのファイル名から導かれます。たとえば、グラフが res/navigation/nav_graph.xml に含まれている場合、リソース名は nav_graph です。
    • android:id: グラフのデスティネーション ID。Dynamic Navigator ライブラリは、インクルードされたグラフのルート要素にあるすべての android:id 値を無視します。
    • app:moduleName: モジュールのパッケージ名。

正しい graphPackage を使用する

指定された navGraph を Navigation コンポーネントが機能モジュールからインクルードできるよう、app:graphPackage を正しく設定することが重要です。

動的機能モジュールのパッケージ名は、ベースアプリ モジュールの applicationId にモジュール名を追加して作成されます。したがって、ベースアプリ モジュールの com.example.dynamicfeatureappapplicationId であり、動的機能モジュールの名前が DynamicFeatureModule である場合、動的モジュールのパッケージ名は com.example.dynamicfeatureapp.DynamicFeatureModule になります。このパッケージ名では大文字と小文字が区別されます。

不確かな場合は、生成された AndroidManifest.xml をチェックすることで、機能モジュールのパッケージ名を確認できます。プロジェクトをビルドしたら <DynamicFeatureModule>/build/intermediates/merged_manifest/debug/AndroidManifest.xml に移動します。次のようになっているはずです。

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

featureSplit の値は動的機能モジュールの名前と一致し、パッケージはベースアプリ モジュールの applicationId と一致している必要があります。app:graphPackage は次のような組み合わせになっています: com.example.dynamicfeatureapp.DynamicFeatureModule.

include-dynamic ナビゲーション グラフでは startDestination にのみ移動できます。動的モジュールでは独自にナビゲーション グラフが作成され、ベースアプリはそれを認識しません。

include-dynamic メカニズムにより、ベースアプリ モジュールは、動的モジュール内で定義されたネストされたナビゲーション グラフをインクルードできます。このネストされたナビゲーション グラフは、他のネストされたナビゲーション グラフと同様に動作します。ルート ナビゲーション グラフ(ネストされたグラフの親)は、ネストされたナビゲーション グラフ自体をデスティネーションとして定義するだけであり、子を定義することはできません。したがって、include-dynamicnavigation グラフがデスティネーションである場合は startDestination が使用されます。

制限事項

  • 現在、動的にインクルードされるグラフはディープリンクをサポートしません。
  • 現在、動的に読み込まれるネストグラフ(つまり、app:moduleName を含む <navigation> 要素)はディープリンクをサポートしません。