기능 모듈 탐색

Dynamic Navigator 라이브러리는 기능 모듈에 정의된 대상과 함께 작동하도록 Jetpack 탐색 구성요소의 기능을 확장합니다. 또한 이 라이브러리를 사용하면 이러한 대상으로 이동할 때 주문형 기능 모듈을 원활하게 설치할 수 있습니다.

설정

기능 모듈을 지원하려면 앱 모듈의 build.gradle 파일에 다음 종속 항목을 사용합니다.

Groovy

dependencies {
    def nav_version = "2.3.5"

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

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

다른 탐색 종속 항목은 기능 모듈에서 사용할 수 있도록 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를 사용하는 것처럼 동작합니다.

진행률 프래그먼트 맞춤설정

app:progressDestination 속성을 설치 진행률 처리에 사용할 대상의 ID로 설정하여 각 탐색 그래프의 진행률 프래그먼트 구현을 재정의할 수 있습니다. 맞춤 진행률 대상은 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)
)

자바

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

    자바

    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를 포함할 수 없으므로 app:graphPackage를 정확하게 설정하는 것이 중요합니다.

동적 기능 모듈의 패키지 이름은 모듈 이름을 기본 앱 모듈의 applicationId에 추가하여 구성됩니다. 따라서 기본 앱 모듈의 applicationIdcom.example.dynamicfeatureapp이고 동적 기능 모듈의 이름이 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 메커니즘을 사용하면 기본 앱 모듈이 동적 모듈 내에 정의된 중첩된 탐색 그래프를 포함할 수 있습니다. 이 중첩된 탐색 그래프는 다른 중첩된 탐색 그래프처럼 동작합니다. 루트 탐색 그래프(즉, 중첩된 그래프의 상위 요소)는 중첩된 탐색 그래프 자체를 하위 요소가 아닌 대상으로만 정의할 수 있습니다. 따라서 startDestination은 include-dynamicnavigation 그래프가 대상일 때 사용됩니다.

제한사항

  • 동적으로 포함된 그래프는 현재 딥 링크를 지원하지 않습니다.
  • 동적으로 로드된 중첩 그래프(즉, app:moduleName이 있는 <navigation> 요소)는 현재 딥 링크를 지원하지 않습니다.