Compose Navigation の実装原則についての深い理解

序文

純粋な Compose プロジェクトはページ ナビゲーションのサポートなしでは機能しません。この点では、 navigation-compose がほぼ唯一の選択肢であり、これが Compose プロジェクトの標準のセカンド パーティ ライブラリでもあります。この記事のように、 Navigation-compose の使用方法に関する記事はたくさんあります実際、Navigation はコード設計の観点からも非常に学ぶ価値があるため、この記事ではその実装原理について詳しく説明します。

Jetpack ナビゲーションから始める

Jetpack Navigatioin は一般的なページ ナビゲーション フレームワークであり、navigation-composeは Compose 用のその特定の実装にすぎません。特定の実装に関係なく、ナビゲーションはコアのパブリック層で次の重要な役割を定義します。

役割 説明する
ナビホスト ナビゲーションのエントリを定義します。これは、ナビゲーション ページをホストするコンテナでもあります。
ナビコントローラー ナビゲーションのグローバルマネージャーはナビゲーションの静的情報と動的情報を保持しており、静的情報はNavGraphを指し、動的情報はナビゲーションが長すぎる場合に生成されるバックスタックNavBackStacksを指します。
ナビグラフ ナビゲーションを定義する際には、各ノードのナビゲーション情報を収集し、ナビゲーショングラフに統一的に登録する必要があります
ナビ目的地 ナビゲーション内の各ノードには、ルートや引数などの情報が含まれます。
ナビゲーター ナビゲーションの特定の実行者である NavController は、ナビゲーション グラフに基づいてターゲット ノードを取得し、Navigator を介してジャンプを実行します。

上記の役割のNavHostなどには、さまざまなシナリオで対応する実装があります。たとえば、従来のビューでは、ナビゲーション フラグメントを例として、アクティビティまたはフラグメントを使用してページをホストします。NavigatotNavDestination

  • Framement はナビゲーション グラフの NavDestination であり、DSL または XMlL を通じて NavGraph を定義し、ナビゲーション グラフの NavDestination の形式で Fragment 情報を収集します。
  • NavHostFragment は、Fragment ページを表示するためのコンテナを提供する NavHost として機能します。
  • 具体的なページジャンプロジックはFragmentNavigatorを介して実装しており、FragmentNavigator#navigateの実装ではFragmentTransaction#replaceに基づいてページ置換を実現しており、NavDestinationに関連付けられたFragmentクラス情報を介してFragmentオブジェクトをインスタンス化して置換を完了します。

今日は主人公のNavigation-Composeを見てみましょう。Navigator と NavDestination と同様に、Compose には Navigator と NavDestination 用の独自の実装があります。少し特殊なのは NavHost で、これは単なる Composable 関数であるため、パブリック ライブラリとの継承関係はありません。

Fragment のようなオブジェクト コンポーネントとは異なり、Compose は関数を使用してページを定義します。では、navigation-compose はどのようにしてNavigation を Compose のような宣言型フレームワークに実装するのでしょうか? 続いてシーンごとに紹介していきます。

ナビゲーションを定義する

NavHost(navController = navController, startDestination = "profile") {
    
    
    composable("profile") {
    
     Profile(/*...*/) }
    composable("friendslist") {
    
     FriendsList(/*...*/) }
    /*...*/
}

Compose の NavHost は本質的に Composable 関数であり、 Navigation-runtimeの同じ名前のインターフェイスと派生関係はありませんが、責任は同様であり、主な目的は NavGraph を構築することです。NavGraph が作成されると、NavController によって保持され、ナビゲーションで使用されるため、NavHost は NavController パラメーターを受け取り、それに NavGraph を割り当てます。

//androidx/navigation/compose/NavHost.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    
    
    NavHost(
        navController,
        remember(route, startDestination, builder) {
    
    
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}

@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {
    
    

    //...
    //设置 NavGraph
    navController.graph = graph
    //...
    
}

上記と同様に、NavHost の NavController とその関数に同じ名前の NavGraph を割り当てます。

コードでは、NavGraph はnavController#createGraphを通じて、NavGraph オブジェクトは NavGraphBuilder に基づいて内部的に作成されます。ビルド プロセス中に、NavHost{...}パラメーター内のビルダーが呼び出されて初期化が完了します。このビルダーは NavGraphBuilder の拡張機能であり、ナビゲーションNavHost{...}を定義、Compose で一連の {…} を通じてナビゲーション ページを定義します。・NavGraphBuilderの拡張機能でもあり、ページのナビゲーションにおける唯一のルートがパラメータで渡されます。

//androidx/navigation/compose/NavGraphBuilder.kt
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    
    
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
    
    
            this.route = route
            arguments.forEach {
    
     (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach {
    
     deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

compose(...)具体的な実装は上記の通り、 を作成しComposeNavigator.DestinationNavGraphBuilder#addDestinationNavGraphのノードに追加します。
Destination を構築するときに 2 つのメンバーを渡します。

  • provider[ComposeNavigator::class]: NavigatorProvider を通じて取得された ComposeNavigator
  • content: 現在のページに対応するコンポーズ可能な関数

もちろん、ルート、引数、ディープリンクなどの情報も宛先に渡されます。

//androidx/navigation/compose.ComposeNavigator.kt
public class Destination(
    navigator: ComposeNavigator,
    internal val content: @Composable (NavBackStackEntry) -> Unit
) : NavDestination(navigator)

これは非常に単純です。つまり、NavDestination からの継承に加えて、追加の Compsoable コンテンツが保存されます。Destination はこのコンテンツを呼び出すことで現在のナビゲーション ノードに対応するページを表示しますが、このコンテンツがどのように呼び出されるのかは後ほど説明します。

ナビゲーションジャンプ

Fragment ナビゲーションと同様に、Compose もルートNavController#navigateを指定して

navController.navigate("friendslist")

上で述べたように、NavController は最終的に Navigator を介して特定のジャンプ ロジック (フラグメント ページを を介して切り替えるFragmentNavigatorなどFragmentTransaction#replace。次に、ComposeNavigator#navigateの。

//androidx/navigation/compose/ComposeNavigator.kt
public class ComposeNavigator : Navigator<Destination>() {
    
    

    //...
    
    override fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
    
    
        entries.forEach {
    
     entry ->
            state.pushWithTransition(entry)
        }
    }
    
    //...

}

ここでの処理は非常にシンプルであり、FragmentNavigator のような特別な処理はありません。ナビゲーション プロセス中のバック スタック(現在のページ ナビゲーションのバック スタック)NavBackStackEntry内のレコードを表します。entriesstate はNavigatorStateobject、Navigation 2.4.0 以降に導入された新しいタイプです。これは、NavController などで使用するナビゲーション プロセスで状態をカプセル化するために使用されます。たとえば、backStack は次のようNavigatorStateに格納されます。

//androidx/navigation/NavigatorState.kt
public abstract class NavigatorState {
    
    
    private val backStackLock = ReentrantLock(true)
    private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
    public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()
    
    //...
    
    public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
    
    
        //...
        push(backStackEntry)
    }
    
    public open fun push(backStackEntry: NavBackStackEntry) {
    
    
        backStackLock.withLock {
    
    
            _backStack.value = _backStack.value + backStackEntry
        }
    }
    
    //...
}

Compose ページがジャンプすると、対応する NavBackStackEntry が宛先 Destination に基づいて作成され、 を通じてバックスタックにpushWithTransitionプッシュされます。backStack は StateFlow 型なので、バックスタックの変化を監視できます。NavHost{...}関数の実装を振り返ると、ここでは backState の変化を監視しており、スタックトップの変化に応じて、対応する Composable 関数を呼び出してページ切り替えを実現していることがわかります。

//androidx/navigation/compose/ComposeNavigator.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {
    
    
    //...

    // 为 NavController 设置 NavGraph
    navController.graph = graph

    //SaveableStateHolder 用于记录 Composition 的局部状态,后文介绍
    val saveableStateHolder = rememberSaveableStateHolder()

    //...
    
    // 最新的 visibleEntries 来自 backStack 的变化
    val visibleEntries = //...
    val backStackEntry = visibleEntries.lastOrNull()

    if (backStackEntry != null) {
    
    
        
        Crossfade(backStackEntry.id, modifier) {
    
    
            
            //...
            val lastEntry = backStackEntry
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
                //调用 Destination#content 显示当前导航对应的页面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
        }
    }

    //...
}

上記のように、NavHost の NavController に NavGraph を設定することに加えて、より重要な作業は、backStack の変更を監視し、ページを更新することです。

Navigation-framgentでのページ切り替えはFragmentNavigator で命令的に完了しますが、navigation-composeでのページ切り替えは NavHost で応答性の高い方法で更新されます。これは、宣言型 UI と命令型 UI の実装の考え方の違いも反映しています。

visibleEntriesNavigatorState#backStack取得した表示対象のEntryを基にしたStateなので、それが変化するとNavHostはCrossfadevisibleEntriesに応じて対応するページを再編成して表示します。ページ表示の具体的な実装も非常に簡単で、 NavHost で BackStack レスポンスを呼び出すDestination#contentだけです。このコンテンツはNavHost{...}、 で各ページに対して定義した Composable 関数です。

状態を保存する

先ほど、ナビゲーション定義とナビゲーション ジャンプの具体的な実装原則について学びましたが、次に、ナビゲーション プロセス中の状態保持について見てみましょう。
Navigation-Composeの状態保持は、主に次の 2 つのシナリオで発生します。

  1. システムの戻るボタンをクリックするか、NavController#popup を呼び出すと、ナビゲーション スタックの最上位にある backStackEntry がポップアップし、ナビゲーションが前のページに戻ります。このとき、前のページの状態になることを期待します。維持された
  2. 下部のナビゲーション バーと併用する場合、ナビゲーション バーの項目をクリックして別のページを切り替えます。このとき、切り替えられたページが前の状態を維持することを望みます。

上記のシナリオでは、ページ切り替えプロセス中にスクロール バーの位置などのページ状態が失われないことを願っていますが、前のコード分析を通じて、Compose ナビゲーションのページ切り替えが本質的に再編成していることもわかりました。そして別のコンポーザブルを呼び出します。デフォルトでは、コンポーザブルがコンポジションから離れると、コンポーザブルの状態は失われます(つまり、再コンポジションは実行されなくなります)。それでは、Navigation-Compose はどのようにして状態の損失を回避するのでしょうか? ここで重要なのは、前のコードにSaveableStateHolder登場したもの。

SaveableStateHolder & rememberSaveable

SaveableStateHolder はcompose-runtimeから取得され、次のように定義されます。

interface SaveableStateHolder {
    
    
    
    @Composable
    fun SaveableStateProvider(key: Any, content: @Composable () -> Unit)

    fun removeState(key: Any)
}

名前から、保存可能な状態 (Saveable State)SaveableStateHolderを維持することSaveableStateProvider。提供されている内部の Composable 関数を呼び出すことができ、Composable の呼び出しプロセスでrememberSaveable定義されたキーによって保存され、後続されません。 Composable のライフサイクル次回 SaveableStateProvider が実行されるとき、保存された状態はキーを介して復元できます。実験を使用して SaveableStateHolder の役割を理解してみましょう。

@Composable
fun SaveableStateHolderDemo(flag: Boolean) {
    
    
    
    val saveableStateHolder = rememberSaveableStateHolder()

    Box {
    
    
        if (flag) {
    
    
             saveableStateHolder.SaveableStateProvider(true) {
    
    
                    Screen1()
            }
        } else {
    
    
            saveableStateHolder.SaveableStateProvider(false) {
    
    
                    Screen2()
        }
    }
}

saveableStateHolder.SaveableStateProvider上記のコードでは、 Screen の内部状態が確実に保存されるように、さまざまなフラグを渡すことで Screen1 と Screen2 を切り替えることができます。たとえば、Screen1 でスクロール バーの状態rememberScrollState()を定義する、Screen1 が再度表示されると、スクロール バーは消えたときの位置に残ります。これは、rememberScrollState が内部で rememberSaveable を使用してスクロール バーの位置を保存するためです。

rememberSaveable について知らない場合は、https://developer.android.com/jetpack/compose/state#restore-ui-state を参照してください。通常の remember に比べて、rememberSaveable は長期間の状態を保存できます。コンポーザブルのライフサイクル全体にわたる時間。ハンドオーバーやプロセスの再起動の場合でも、状態の復元を実現できます。

なお、SaveableStateProviderの外でrememberSaveableを使用すると、縦横画面切り替え時の状態は保存できますが、ナビゲーションシーンでは状態保存ができません。rememberSaveable を使用して定義された状態は、構成が変更された場合にのみ自動的に保存されますが、共通の UI 構造が変更された場合には保存がトリガーされません。また、SaveableStateProvider の主な機能は、次onDisposeの場合に

//androidx/compose/runtime/saveable/SaveableStateHolder.kt

@Composable
fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
    
    
    ReusableContent(key) {
    
    
        // 持有 SaveableStateRegistry
        val registryHolder = ...
        
        CompositionLocalProvider(
            LocalSaveableStateRegistry provides registryHolder.registry,
            content = content
        )
        
        DisposableEffect(Unit) {
    
    
            ...
            onDispose {
    
    
                //通过 SaveableStateRegistry 保存状态
                registryHolder.saveTo(savedStates)
                ...
            }
        }
    }

SaveableStateRegistry上記のコードでは、 onDispose ライフ サイクルで、registryHolder#saveToによって状態が SaveStates に保存され、次回 Composite に入るときの状態回復に SavedStates が使用されることがわかります。

ちなみに、LayoutNode はキーに基づいて再利用ReusableContent{...}できるため、UI の高速な再現に役立ちます。

戻るときの状態の保持

SaveableStateHolder の役割を簡単に紹介した後、NavHost でそれがどのように機能するかを見てみましょう。

@Composable
public fun NavHost(
    ...
) {
    
    
    ...
    //SaveableStateHolder 用于记录 Composition 的局部状态,后文介绍
    val saveableStateHolder = rememberSaveableStateHolder()
    ...
        Crossfade(backStackEntry.id, modifier) {
    
    
            ...
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
                //调用 Destination#content 显示当前导航对应的页面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
            
        }

    ...
}

lastEntry.LocalOwnersProvider(saveableStateHolder)LocalOwnersProvider は内部的に呼び出されDestination#content、実際には SaveableStateProvider への呼び出しです。

@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
    saveableStateHolder: SaveableStateHolder,
    content: @Composable () -> Unit
) {
    
    
    CompositionLocalProvider(
        LocalViewModelStoreOwner provides this,
        LocalLifecycleOwner provides this,
        LocalSavedStateRegistryOwner provides this
    ) {
    
    
        // 调用 SaveableStateProvider
        saveableStateHolder.SaveableStateProvider(content)
    }
}

上記のように、SaveableStateProvider を呼び出す前に、CompositonLocal を通じて多くの Owner が注入されます。これらの Owner の実装は次のとおりです。これは、現在の NavBackStackEntry を指します。

  • LocalViewModelStoreOwner : BackStackEntry に基づいて ViewModel を作成および管理できます
  • LocalLifecycleOwner: ライフサイクルベースのサブスクリプションなどの操作を容易にするために LifecycleOwner を提供します。
  • LocalSavedStateRegistryOwner: SavedStateRegistry を通じて状態保存のコールバックを登録します。たとえば、rememberSaveable での状態保存は、実際には SavedStateRegistry を通じて登録され、特定の時点でコールバックされます。

ナビゲーション ベースの単一ページ アーキテクチャでは、NavBackStackEntry が、ページ レベルの ViewModel の提供など、Fragment と同じ役割を担っていることがわかります。

前述したように、SaveableStateProvider はキーを介して状態を復元する必要がありますが、キーはどのように指定されるのでしょうか?

LocalOwnersProvider で呼び出される SaveableStateProvider はパラメーター キーを指定していません。これは内部呼び出しのラッパーであることがわかります。

@Composable
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
    
    
    val viewModel = viewModel<BackStackEntryIdViewModel>()
    
    //设置 saveableStateHolder,后文介绍
    viewModel.saveableStateHolder = this
    
    //
    SaveableStateProvider(viewModel.id, content)
    
    DisposableEffect(viewModel) {
    
    
        onDispose {
    
    
            viewModel.saveableStateHolder = null
        }
    }
}

ここで実際の SaveableStateProvider が呼び出され、キーは ViewModel を通じて管理されます。NavBackStackEntry 自体は ViewModelStoreOwner であるため、新しい NavBackStackEntry がスタックにプッシュされると、次の NavBackStackEntry とその ViewModel はまだ存在します。NavBackStackEntry がスタックの最上位に戻ると、以前に保存した ID を BackStackEntryIdViewModel から取得し、それを SaveableStateProvider に渡すことができます。

BackStackEntryIdViewModel の実装は次のとおりです。

//androidx/navigation/compose/BackStackEntryIdViewModel.kt
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
    
    

    private val IdKey = "SaveableStateHolder_BackStackEntryKey"
    
    // 唯一 ID,可通过 SavedStateHandle 保存和恢复
    val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also {
    
     handle.set(IdKey, it) }

    var saveableStateHolder: SaveableStateHolder? = null

    override fun onCleared() {
    
    
        super.onCleared()
        saveableStateHolder?.removeState(id)
    }
}

BackStackEntryIdViewModel は名前からして主に BackStackEntryId の管理に使用されますが、実際には現在の BackStackEntry の saveableStateHolder の保持者でもあります。ViewModel は SaveableStateProvider の saveableStateHolder に渡されます。ViewModel が存在する限り、UI の状態は維持されます。失った。現在の NavBackStackEntry がスタックから出た後、対応する ViewModel で onCleared が発生します。このとき、状態は saveableStateHolder#removeState RemoveState によってクリアされます。この Destination に再度移動すると、以前の状態は残りません。

下部ナビゲーションバーが切り替わったときの状態保存

Navigation-compose は、BottomNavBar と連携して複数タブのページ切り替えを実現するためによく使用されます。NavController#navigate を直接使用してタブ ページを切り替えると、NavBackStack が無限に増大するため、ページ切り替え後に表示する必要のないページをスタックから削除する必要があります。たとえば、次のようにします。

val navController = rememberNavController()

Scaffold(
  bottomBar = {
    
    
    BottomNavigation {
    
    
      ...
      items.forEach {
    
     screen ->
        BottomNavigationItem(
          ...
          onClick = {
    
    
            navController.navigate(screen.route) {
    
    
              // 避免 BackStack 增长,跳转页面时,将栈内 startDestination 之外的页面弹出
              popUpTo(navController.graph.findStartDestination().id) {
    
    
                //出栈的 BackStack 保存状态
                saveState = true
              }
              // 避免点击同一个 Item 时反复入栈
              launchSingleTop = true
              
              // 如果之前出栈时保存状态了,那么重新入栈时恢复状态
              restoreState = true
            }
          }
        )
      }
    }
  }
) {
    
     
  NavHost(...) {
    
    
    ...
  }
}

上記のコードの重要な点は、saveState とrestoreState を設定することによって NavBackStack がスタックからポップされるときに、対応する Destination の状態が確実に保存され、Destination が再度スタックにプッシュされたときに復元できるようにすることです。

状態を保存したい場合は、関連する ViewModel を破棄できないことを意味し、NavBackStack が ViewModelStoreOwner であることはわかっていますが、NavBackStack がスタックからポップアウトされた後、引き続き ViewModel を保存するにはどうすればよいでしょうか? 実はNavBackStack管轄のViewModelはNavControllerで管理されています

上のクラス図を見ると両者の関係がよくわかりますが、NavControllerはNavViewModelStoreProviderの実装であるNavControllerViewModelを保持しており、各NavControllerに対応するViewModelStoreをMapで管理しています。NavBackStackEntry の ViewModelStore は、NavViewModelStoreProvider から取得されます。

NavBackStackEntry がスタックから飛び出すと、対応する Destination#content が画面から移動し、onDispose が実行されます。

Crossfade(backStackEntry.id, modifier) {
    
    
    
    ... 
    DisposableEffect(Unit) {
    
    
        ...
        
        onDispose {
    
    
            visibleEntries.forEach {
    
     entry ->
                //显示中的 Entry 移出屏幕,调用 onTransitionComplete
                composeNavigator.onTransitionComplete(entry)
            }
        }
    }

    lastEntry.LocalOwnersProvider(saveableStateHolder) {
    
    
        (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
    }
}

onTransitionComplete で NavigatorState#markTransitionComplete を呼び出します。

override fun markTransitionComplete(entry: NavBackStackEntry) {
    
    
    val savedState = entrySavedState[entry] == true
    ...
    if (!backQueue.contains(entry)) {
    
    
        ...
        if (backQueue.none {
    
     it.id == entry.id } && !savedState) {
    
    
            viewModel?.clear(entry.id)  //清空 ViewModel
        }
        ...
    } 
    
    ...
}

デフォルトでは、entrySavedState[entry] が false になっており、ここで viewModel#clear が実行されて、entry に対応する ViewModel がクリアされますが、popUpTo { ... } で saveState を true に設定すると、entrySavedState[entry] が true になるので、ここでは ViewModel#clear は実行されません。

同時にrestoreStateをtrueに設定すると、次回同じタイプのDestinationがページに入ったときに、kはViewModleを通じて状態を復元できます。

//androidx/navigation/NavController.kt

private fun navigate(
    ...
) {
    
    

    ...
    //restoreState设置为true后,命中此处的 shouldRestoreState()
    if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
    
    
        navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
    } 
    ...
}

restoreStateInternal で、DestinationId に従って以前の対応する BackStackId を見つけ、その BackStackId を使用して ViewModel を取得し、状態を復元します。

ナビゲーション遷移アニメーション

Navigation-fragment を使用すると、次のようにリソース ファイルを通じてページにジャンプするときに特別なアニメーションを指定できます。

findNavController().navigate(
    R.id.action_fragmentOne_to_fragmentTwo,
    null,
    navOptions {
    
     
        anim {
    
    
            enter = android.R.animator.fade_in
            exit = android.R.animator.fade_out
        }
    }
)

Compose アニメーションはリソース ファイルに依存しないため、navigation-compose は上記の anim { … } をサポートしていませんが、それに応じて、navigation-compose は Compose アニメーション API に基づいたナビゲーション アニメーションを実現できます。

注: Navigation-Compose が依存する Comopse アニメーション API (AnimatedContent など) はまだ実験段階にあります。したがって、ナビゲーション アニメーションは、当面の間、comparison-navigation-animation を通じてのみ導入できます。アニメーション API が安定した後、将来的には、Navigation-Compose に移動される予定です。

dependencies {
    
    
    implementation "com.google.accompanist:accompanist-navigation-animation:<version>"
}

依存関係を追加した後、ナビゲーション作成ナビゲーション アニメーションの API フォームを事前にプレビューできます。

AnimatedNavHost(
    navController = navController,
    startDestination = AppScreen.main,
    enterTransition = {
    
    
        slideInHorizontally(
            initialOffsetX = {
    
     it },
            animationSpec = transSpec
        )
    },
    popExitTransition = {
    
    
        slideOutHorizontally(
            targetOffsetX = {
    
     it },
            animationSpec = transSpec
        )
    },
    exitTransition = {
    
    
        ...
    },
    popEnterTransition = {
    
    
        ...
    }

) {
    
    
    composable(
        AppScreen.splash,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Splash()
    }
    composable(
        AppScreen.login,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Login()
    }
    composable(
        AppScreen.register,
        enterTransition = null,
        exitTransition = null
    ) {
    
    
        Register()
    }
    ...
}

API は非常に直感的で、トランジション アニメーションは、コンポーザブル パラメータごとAnimatedNavHostに。

NavHost が Crossfade で呼び出されるDestination#contentこと。Compose アニメーションに精通している人は、AnimatedContent を使用して、コンテンツの切り替えにさまざまなアニメーション効果を指定できることを簡単に想像できます。これはまさにnavigatioin-composeが行うことです。

//com/google/accompanist/navigation/animation/AnimatedNavHost.kt

@Composable
public fun AnimatedNavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
        {
    
     fadeIn(animationSpec = tween(700)) },
    exitTransition: ...,
    popEnterTransition: ...,
    popExitTransition: ...,
) {
    
    

    ...
    
    val backStackEntry = visibleTransitionsInProgress.lastOrNull() ?: visibleBackStack.lastOrNull()

    if (backStackEntry != null) {
    
    
        val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
    
    
            ...
        }

        val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = {
    
    
            ...
        }

        val transition = updateTransition(backStackEntry, label = "entry")
        
        transition.AnimatedContent(
            modifier,
            transitionSpec = {
    
     finalEnter(this) with finalExit(this) },
            contentAlignment,
            contentKey = {
    
     it.id }
        ) {
    
    
            ...
            currentEntry?.LocalOwnersProvider(saveableStateHolder) {
    
    
                (currentEntry.destination as AnimatedComposeNavigator.Destination)
                    .content(this, currentEntry)
            }
        }
        ...
    }

    ...
}

上記のように、AnimatedNavHost と通常の NavHost の主な違いは、Crossfade が置き換えられることですTransition#AnimatedContentfinalEnterと は、 で指定されたパラメータに従って計算された Compose Transition アニメーションfinalExitですtransitionSpec具体的な実装を確認するには、finalEnter を例として取り上げます。

val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
    
    
    val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination

    if (composeNavigator.isPop.value) {
    
    
        //当前页面即将出栈,执行pop动画
        targetDestination.hierarchy.firstNotNullOfOrNull {
    
     destination ->
            //popEnterTransitions 中存储着通过 composable 参数指定的动画
            popEnterTransitions[destination.route]?.invoke(this)
        } ?: popEnterTransition.invoke(this)
    } else {
    
    
        //当前页面即将入栈,执行enter动画
        targetDestination.hierarchy.firstNotNullOfOrNull {
    
     destination ->
            enterTransitions[destination.route]?.invoke(this)
        } ?: enterTransition.invoke(this)
    }
}

上記の通り、popEnterTransitions[destination.route]composable(…)パラメータで指定したアニメーションであるため、composableパラメータで指定したアニメーションの優先度はAnimatedNavHostよりも高くなります。

ヒルトとナビゲーション

各 BackStackEntry は ViewModelStoreOwner であるため、ナビゲーション ページ レベルで ViewModel を取得できます。hilt-viewmodle-navigationを使用すると、Hilt を通じて必要な依存関係を ViewModel に注入でき、ViewModel の構築コストを削減できます。

dependencies {
    
    
    implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
}

hilt に基づいて ViewModel を取得する効果は次のとおりです。

// import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun MyApp() {
    
    
    NavHost(navController, startDestination = startRoute) {
    
    
        composable("example") {
    
     backStackEntry ->
            // 通过 hiltViewModel() 获取 MyViewModel,
            val viewModel = hiltViewModel<MyViewModel>()
            MyScreen(viewModel)
        }
        /* ... */
    }
}

MyViewModel追加する必要があるのは@HiltViewModel@Injectannotationsだけであり、パラメータに依存するものはrepositoryHilt を通じて自動的に挿入できるため、ViewModelFactory をカスタマイズする手間が省けます。

@HiltViewModel
class MyViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repository: ExampleRepository
) : ViewModel() {
    
     /* ... */ }

hiltViewModel のソース コードを簡単に見てみましょう。

@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    
    
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
): VM {
    
    
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, factory = factory)
}

@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is NavBackStackEntry) {
    
    
    HiltViewModelFactory(
        context = LocalContext.current,
        navBackStackEntry = viewModelStoreOwner
    )
} else {
    
    
    null
}

前述したように、 は現在の BackStackEntryLocalViewModelStoreOwnerであり、viewModelStoreOwner を取得した後、HiltViewModelFactory()を通じて ViewModelFactory を取得します。HiltViewModelFactory はhilt-navigationのスコープであるため、ここでは詳しく説明しません。

やっと

ディープリンクや引数など、 navigation-composeのその他の機能は、実装時に Compose に特別な処理を行わないため、ここでは紹介しません。興味がある場合は、 navigation-commonのソース コードを読むことができます。 。この記事の一連の紹介を通じて、 navigation-compose がAPI 設計と実装の両方において宣言型の基本的な考え方に従っていることがわかります。独自の Compose 3 部構成ライブラリを開発する必要がある場合は、参照して学ぶことができます。

おすすめ

転載: blog.csdn.net/vitaviva/article/details/126635257