ナビゲーション コードをモジュール化する

このページでは、ナビゲーション コードをモジュール化する方法について説明します。これは、一般的なアプリのモジュール化のガイダンスを補完することを目的としています。

概要

ナビゲーション コードのモジュール化とは、関連するナビゲーション キーと、それらが表すコンテンツを個別のモジュールに分割するプロセスです。これにより、責任が明確に分離され、アプリのさまざまな機能間を移動できます。

ナビゲーション コードをモジュール化する手順は次のとおりです。

  • アプリの機能ごとに 2 つのサブモジュール(api と impl)を作成する
  • 各機能のナビゲーション キーを api モジュールに配置する
  • 各機能の entryProviders とナビゲーション可能なコンテンツを、関連する impl モジュールに配置します。
  • 直接または依存関係挿入を使用して、メインアプリ モジュールに entryProviders を提供します。

機能を api サブモジュールと implementation サブモジュールに分割する

アプリの機能ごとに、api と impl(「implementation」の略)という 2 つのサブモジュールを作成します。次の表を参考にして、ナビゲーション コードを配置する場所を決定してください。

モジュール名

以下を含む

api

ナビゲーション キー

impl

NavEntry と entryProvider の定義を含む、その機能のコンテンツ。キーをコンテンツに解決するもご覧ください。

このアプローチでは、impl モジュールに含まれるコンテンツが、別のモジュールの api モジュールに含まれるナビゲーション キーに依存できるようにすることで、ある機能から別の機能に移動できるようにします。

「impl」モジュールが「api」モジュールに依存する仕組みを示す機能モジュールの依存関係図。
図 1. 実装モジュールが API モジュールに依存する方法を示す機能モジュールの依存関係図。

拡張関数を使用してナビゲーション エントリを分離する

Navigation 3 では、ナビゲーション可能なコンテンツはナビゲーション エントリを使用して定義されます。これらのエントリを個別のモジュールに分割するには、EntryProviderScope に拡張関数を作成し、その機能の impl モジュールに移動します。これらはエントリ ビルダーと呼ばれます。

次のコード例は、2 つのナビゲーション エントリを構築するエントリ ビルダーを示しています。

// import androidx.navigation3.runtime.EntryProviderScope
// import androidx.navigation3.runtime.NavKey

fun EntryProviderScope<NavKey>.featureAEntryBuilder() {
    entry<KeyA> {
        ContentRed("Screen A") {
            // Content for screen A
        }
    }
    entry<KeyA2> {
        ContentGreen("Screen A2") {
            // Content for screen A2
        }
    }
}

メインアプリ モジュールで entryProvider を定義するときに、entryProvider DSL を使用してその関数を呼び出します。

// import androidx.navigation3.runtime.entryProvider
// import androidx.navigation3.ui.NavDisplay
NavDisplay(
    entryProvider = entryProvider {
        featureAEntryBuilder()
    },
    // ...
)

依存関係挿入を使用してメインアプリにエントリを追加する

上記のコード例では、各エントリ ビルダーは、entryProvider DSL を使用してメインアプリから直接呼び出されます。アプリに多数の画面や機能モジュールがある場合、このパターンは適切にスケーリングされない可能性があります。

これを解決するには、各機能モジュールが依存関係の注入を使用して、エントリ ビルダーをアプリのアクティビティに提供するようにします。

たとえば、次のコードでは、Dagger マルチバインディング(具体的には @IntoSet)を使用して、エントリ ビルダーを MainActivity が所有する Set に挿入しています。これらは entryProvider 内で繰り返し呼び出されるため、多数のエントリ ビルダー関数を明示的に呼び出す必要がなくなります。

機能モジュール

// import dagger.Module
// import dagger.Provides
// import dagger.hilt.InstallIn
// import dagger.hilt.android.components.ActivityRetainedComponent
// import dagger.multibindings.IntoSet

@Module
@InstallIn(ActivityRetainedComponent::class)
object FeatureAModule {

    @IntoSet
    @Provides
    fun provideFeatureAEntryBuilder() : EntryProviderScope<NavKey>.() -> Unit = {
        featureAEntryBuilder()
    }
}

アプリ モジュール

// import android.os.Bundle
// import androidx.activity.ComponentActivity
// import androidx.activity.compose.setContent
// import androidx.navigation3.runtime.EntryProviderScope
// import androidx.navigation3.runtime.NavKey
// import androidx.navigation3.runtime.entryProvider
// import androidx.navigation3.ui.NavDisplay
// import javax.inject.Inject

class MainActivity : ComponentActivity() {

    @Inject
    lateinit var entryBuilders: Set<@JvmSuppressWildcards EntryProviderScope<NavKey>.() -> Unit>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            NavDisplay(
                entryProvider = entryProvider {
                    entryBuilders.forEach { builder -> this.builder() }
                },
                // ...
            )
        }
    }
}

ナビゲーション エントリでナビゲーションが必要な場合(新しい画面に移動する UI 要素が含まれている場合など)は、アプリのナビゲーション状態を変更できるオブジェクトを各ビルダー関数に挿入します。

アプリがディープリンクをサポートし、モジュール化されている場合、各機能モジュールは、所有するデスティネーションの DeepLinkMatcher インスタンスを定義する必要があります。

アプリ全体からすべてのマッチャーを照合するには、依存関係インジェクションのマルチバインディングを使用します。たとえば、各機能モジュールは、そのマッチャーを Dagger @IntoSet マルチバインディングに提供できます。

機能モジュール

次に、MainActivity などのメインアプリ モジュールで、マッチャーのセットを挿入し、それらを使用して受信リクエストを照合できます。

アプリ モジュール

ディープリンクの定義と処理について詳しくは、ディープリンクをサポートするをご覧ください。

リソース

Navigation 3 コードをモジュール化する方法を示すコードサンプルについては、以下をご覧ください。