前言

一个纯 Compose 项目少不了页面导航的支撑,而 navigation-compose 几乎是这方面的仅有选择,这也使得它成为 Compose 工程的标配二方库。介绍 navigation-compose 怎么运用的文章许多了,然而在代码规划上 Navigation 也十分值得咱们学习,那么本文就带咱们深挖一下其完结原理

1. 从 Jetpack Navigation 说起

Jetpack Navigatioin 是一个通用的页面导航结构,navigation-compose 仅仅其针对 Compose 的的一个详细完结。抛开详细完结,Navigation 在中心公共层界说了以下重要人物:

人物 阐明
NavHost 界说导航的进口,一起也是承载导航页面的容器
NavController 导航的大局办理者,维护着导航的静态和动态信息,静态信息指 NavGraph,动态信息即导航过长中产生的回退栈 NavBackStacks
NavGraph 界说导航时,需求搜集各个节点的导航信息,并一致注册到导航图中
NavDestination 导航中的各个节点,携带了 route,arguments 等信息
Navigator 导航的详细履行者,NavController 依据导航图获取方针节点,并经过 Navigator 履行跳转

一文看懂 Compose Navigation 实现原理

上述人物中的 NavHostNavigatotNavDestination 等在不同场景中都有对应的完结。例如在传统视图中,咱们运用 Activity 或许 Fragment 承载页面,以 navigation-fragment 为例:

  • Frament 便是导航图中的一个个 NavDestination,咱们经过 DSL 或许 XMlL 方式界说 NavGraph ,将 Fragment 信息以 NavDestination 的方式搜集到导航图
  • NavHostFragment 作为 NavHost 为 Fragment 页面的展示提供容器
  • 咱们经过 FragmentNavigator 完结详细页面跳转逻辑,FragmentNavigator#navigate 的完结中依据 FragmentTransaction#replace 完结页面替换,经过 NavDestination 相关的的 Fragment 类信息,实例化 Fragment 目标,完结 replace。

再看一下咱们今日的主角 navigation-compose。像 navigation-fragment 一样,Compose 针对 Navigator 以及 NavDestination 都是自己的详细完结,有点特别的是 NavHost,它仅仅一个 Composable 函数,所以与公共库没有继承联系:

一文看懂 Compose Navigation 实现原理

不同于 Fragment 这样目标组件,Compose 运用函数界说页面,那么 navigation-compose 是怎么将 Navigation 落地到 Compose 这样的声明式结构中的呢?接下来咱们分场景进行介绍。

2. 界说导航

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 进行创立,内部会依据 NavGraphBuilder 创立 NavGraph 目标,在 build 过程中,调用 NavHost{...} 参数中的 builder 完结初始化。这个 builder 是 NavGraphBuilder 的扩展函数,咱们在运用 NavHost{...} 界说导航时,会在 {…} 这里面经过一系列 界说 Compose 中的导航页面。 也是 NavGraphBuilder 的扩展函数,经过参数传入页面在导航中的仅有 route。

//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.Destination 并经过 NavGraphBuilder#addDestination 添加到 NavGraph 的 nodes 中。 在构建 Destination 时传入两个成员:

  • provider[ComposeNavigator::class] :经过 NavigatorProvider 获取的 ComposeNavigator
  • content : 当时页面对应的 Composable 函数

当然,这里还会为 Destination 传入 route,arguments,deeplinks 等信息。

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

十分简略,便是在继承自 NavDestination 之外,多存储了一个 Compsoable 的 content。Destination 经过调用这个 content,显现当时导航节点对应的页面,后文会看到这个 content 是怎么被调用的。

3. 导航跳转

跟 Fragment 导航一样,Compose 当好也是经过 NavController#navigate 指定 route 进行页面跳转

navController.navigate("friendslist")

如前所述 NavController 最终经过 Navigator 完结详细的跳转逻辑,比方 FragmentNavigator 经过 FragmentTransaction#replace 完结 Fragment 页面的切换,那咱们看一下 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 代表导航过程中回退栈中的一个记载,entries 便是当时页面导航的回退栈。state 是一个 NavigatorState 目标,这是 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 页面产生跳转时,会依据意图地 Destination 创立对应的 NavBackStackEntry ,然后经过 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 在完结思路上的不同。

visibleEntries 是依据 NavigatorState#backStack 得到的需求显现的 Entry,它是一个 State,所以当其改变时 NavHost 会产生重组,Crossfade 会依据 visibleEntries 显现对应的页面。页面显现的详细完结也十分简略,在 NavHost 中调用 BackStack 应的 Destination#content 即可,这个 content 便是咱们在 NavHost{...} 中为每个页面界说的 Composable 函数。

一文看懂 Compose Navigation 实现原理

4. 保存状况

前面咱们了解了导航界说和导航跳转的详细完结原理,接下来看一下导航过程中的状况保存。 navigation-compose 的状况保存首要产生在以下两个场景中:

  1. 点击系统 back 键或许调用 NavController#popup 时,导航栈顶的 backStackEntry 弹出,导航回来前一页面,此刻咱们期望前一页面的状况得到坚持
  2. 在合作底部导航栏运用时,点击 nav bar 的 Item 能够在不同页面间切换,此刻咱们期望切换回来的页面坚持之前的状况

上述场景中,咱们期望在页面切换过程中,不会丢掉例如滚动条方位等的页面状况,但是经过前面的代码分析,咱们也知道了 Compose 导航的页面切换本质上便是在重组调用不同的 Composable。默认情况下,Composable 的状况跟着其从 Composition 中的脱离(即重组中不再被履行)而丢掉。那么 navigation-compose 是怎么防止状况丢掉的呢?这里的关键便是前面代码中出现的 SaveableStateHolder 了。

SaveableStateHolder & rememberSaveable

SaveableStateHolder 来自 compose-runtime ,界说如下:

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

从姓名上不难理解 SaveableStateHolder 维护着可保存的状况(Saveable State),咱们能够在它提供的 SaveableStateProvider 内部调用 Composable 函数,Composable 调用过程中运用 rememberSaveable 界说的状况都会经过 key 进行保存,不会跟着 Composable 的生命周期的完毕而丢掉,当下次 SaveableStateProvider 履行时,能够经过 key 康复保存的状况。咱们经过一个试验来了解一下 SaveableStateHolder 的效果:

@Composable
fun SaveableStateHolderDemo(flag: Boolean) {
    val saveableStateHolder = rememberSaveableStateHolder()
    Box {
        if (flag) {
             saveableStateHolder.SaveableStateProvider(true) {
                    Screen1()
            }
        } else {
            saveableStateHolder.SaveableStateProvider(false) {
                    Screen2()
        }
    }
}

上述代码,咱们能够经过传入不同 flag 完结 Screen1 和 Screen2 之前的切换,saveableStateHolder.SaveableStateProvider 能够确保 Screen 内部状况被保存。例如你在 Screen1 中运用 rememberScrollState() 界说了一个滚动条状况,当 Screen1 再次显现时滚动条仍然处于消失时的方位,因为 rememberScrollState 内部运用 rememberSaveable 保存了滚动条的方位。

如果不了解 rememberSaveable 能够参阅 developer.android.com/jetpack/com… remember, rememberSaveable 能够跨过 Composable 的生命周期更持久的保存状况,在横竖屏切换甚至进程重启的场景中能够完结状况康复。

需求注意的是,如果咱们在 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)
                ...
            }
        }
    }

rememberSaveable 中的经过 SaveableStateRegistry 进行保存,上面代码中能够看到在 onDispose 生命周期中,经过 registryHolder#saveTo 将状况保存到了 savedStates,savedStates 用于下次进入 Composition 时的状况康复。

顺便提一下,这里运用 ReusableContent{...} 能够依据 key 复用 LayoutNode,有利于 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) 内部调用了 Destination#content, LocalOwnersProvider 内部其实便是对 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 的完结都是 this,即指向当时的 NavBackStackEntry

  • LocalViewModelStoreOwner : 能够依据 BackStackEntry 的创立和办理 ViewModel
  • LocalLifecycleOwner:提供 LifecycleOwner,便于进行依据 Lifecycle 订阅等操作
  • LocalSavedStateRegistryOwner:经过 SavedStateRegistry 注册状况保存的回调,例如 rememberSaveable 中的状况保存其实经过 SavedStateRegistry 进行注册,并在特定时刻点被回调

可见,在依据导航的单页面架构中,NavBackStackEntry 承载了相似 Fragment 一样的职责,例如提供页面级的 ViewModel 等等。

前面提到,SaveableStateProvider 需求经过 key 康复状况,那么这个 key 是怎么指定的呢。

LocalOwnersProvider 中调用的 SaveableStateProvider 没有指定参数 key,本来它是对内部调用的包装:

@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 调用在这里,而 key 是经过 ViewModel 办理的。因为 NavBackStackEntry 自身便是 ViewModelStoreOwner,新的 NavBackStackEntry 被压栈时,下面的 NavBackStackEntry 以及其所辖的 ViewModel 依然存在。当 NavBackStackEntry 从头回到栈顶时,能够从 BackStackEntryIdViewModel 中获取之前保存的 id,传入 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 时,不会遗留之前的状况。

一文看懂 Compose Navigation 实现原理

底部导航栏切换时的状况保存

navigation-compose 常用来合作 BottomNavBar 完结多Tab页的切换。如果咱们直接运用 NavController#navigate 切换 Tab 页,会形成 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 再次被压栈时能够康复。

状况想要保存就意味着相关的 ViewModle 不能销毁,而前面咱们知道了 NavBackStack 是 ViewModelStoreOwner,怎么在 NavBackStack 出栈后继续保存 ViewModel 呢?其实 NavBackStack 所辖的 ViewModel 是存在 NavController 中办理的

一文看懂 Compose Navigation 实现原理

从上面的类图能够看清他们的联系, NavController 持有一个 NavControllerViewModel,它是 NavViewModelStoreProvider 的完结,经过 Map 办理着各 NavController 对应的 ViewModelStore。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,康复状况。

5. 导航转场动画

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 等目前尚处于试验状况,因而导航动画暂时只能经过 accompanist-navigation-animation 引入,待动画 API 安稳后,未来会移入 navigation-compose。

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

添加依靠后能够提前预览 navigation-compose 导航动画的 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 中一致指定 Transition 动画,也能够在各个 composable 参数中分别指定。

回想一下,NavHost 中的 Destination#content 是在 Crossfade 中调用的,了解 Compose 动画的就不难联想到,能够在此处运用 AnimatedContent 为 content 的切换指定不同的动画效果,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#AnimatedContentfinalEnterfinalExit 是依据参数计算得到的 Compose Transition 动画,经过 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 。

6. Hilt & Navigation

因为每个 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@Inject 注解,其参数依靠的 repository 能够经过 Hilt 主动注入,省去咱们自界说 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
}

前面介绍过 LocalViewModelStoreOwner 便是当时的 BackStackEntry,拿到 viewModelStoreOwner 之后,经过 HiltViewModelFactory() 获取 ViewModelFactory。 HiltViewModelFactory 是 hilt-navigation 的范围,这里就不深入研究了。

7. 最后

navigation-compose 的其他一些功能例如 Deeplinks,Arguments 等等,在完结上针对 Compose 没有什么特别处理,这里就不特别介绍了,有兴趣能够翻阅 navigation-common 的源码。经过本文的一系列介绍,咱们能够看出 navigation-compose 无论在 API 的规划上还是在详细完结上,都遵从了声明式的基本思想,当咱们需求开发自己的 Compose 三方库时,能够从中参阅和学习。