一文详解 Compose Navigation 的实现原理

目录
  • 前言
  • 1. 从 Jetpack Navigation 说起
  • 2. 定义导航
  • 3. 导航跳转
  • 4. 保存状态
    • SaveableStateHolder & rememberSaveable
    • 导航回退时的状态保存
    • 底部导航栏切换时的状态保存
  • 5. 导航转场动画
  • 6. Hilt & Navigation
  • 7. 总结

前言

一个纯 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 执行跳转

上述角色中的 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 函数,所以与公共库没有继承关系:

不同于 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 函数。

4. 保存状态

前面我们了解了导航定义和导航跳转的具体实现原理,接下来看一下导航过程中的状态保存。 navigation-compose 的状态保存主要发生在以下两个场景中:

  • 点击系统 back 键或者调用 NavController#popup 时,导航栈顶的 backStackEntry 弹出,导航返回前一页面,此时我们希望前一页面的状态得到保持
  • 在配合底部导航栏使用时,点击 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 保存了滚动条的位置。

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 时,不会遗留之前的状态。

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

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 中管理的

从上面的类图可以看清他们的关系, 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#AnimatedContentfinalEnter 和 finalExit 是根据参数计算得到的 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 三方库时,可以从中参考和借鉴。

到此这篇关于一文详解 Compose Navigation 的实现原理的文章就介绍到这了,更多相关 Compose Navigation 实现内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android Jetpack结构运用Compose实现微博长按点赞彩虹效果

    目录 原版 1. Compose 动画 API 概览 2. 长按点赞动画分解 3. 彩虹动画 3.1 状态管理 AnimatedRainbow animatedRainbows 列表 3.2 内容绘制 4. 表情动画 4.1 状态管理 AnimatedEmoji infiniteRepeatable CubicBezierEasing 抛物线动画 animatedEmojis 列表 4.2 内容绘制 5. 烟花动画 5.1 状态管理 AnimatedFlower keyframes animat

  • Android开发Compose集成高德地图实例

    目录 正文 高德地图官网开发者建议 初始化MapView并添加到AndroidView里面 MapView增加一个管理地图生命周期的扩展 给MapView添加生命周期观察者 添加MapView的生命周期控制 正文 Compose中我们应该怎么使用地图呢?像之前我们在xml里面创建MapView,都是在Activity里面,管理MapView生命周期,和其他的监听器,Compose里面怎么搞? 下面我们以高德地图为例,在Compose中创建地图MapView,然后用AndroidView添加Map

  • Android隐藏和沉浸式虚拟按键NavigationBar的实现方法

    有的时候我们在做全屏显示或者视频全屏播放时候,有些手机有底部的虚拟按键,如下图所示: 在开发中我们会遇到需要隐藏虚拟按键或者沉浸式虚拟按键的需求. 上图为沉浸式虚拟按键效果. 上图为隐藏虚拟按键效果. 那我们先说如何隐藏虚拟按键: public static void hideNavKey(Context context) { if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { View v = ((A

  • Android BottomNavigationBar底部导航的使用方法

    简介:Google推出的BottomNavigationBar底部导航栏 1 .基本的使用(add和replace方式) 2.扩展添加消息和图形 3.修改图片大小与文字间距 版本更新:2019-5-13 补充布局文件activity_main <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/

  • Android Jetpack组件Navigation导航组件的基本使用

    目录 1.Navigation 基本概念 2.Navigation 使用入门 2.1 添加Navigation依赖 2.2 创建导航图 2.3 导航图中添加目的地Fragment 2.4 Activity添加 NavHost 2.5 LoginFragment 代码编写 2.6 welcomeFragment 代码编写 总结 本篇主要介绍一下 Android Jetpack 组件 Navigation 导航组件的 基本使用 当看到 Navigation单词的时候 应该就大概知道 这是一个关于导航

  • Android中导航组件Navigation的实现原理

    对于导航组件的使用方式不是本文的重点,具体使用可以参考官方文档,导航组件框架是通过fragment来实现的,其核心类主要可以分为三个NavGraph.NavHostController.NavHostFragment,这三个类的作用分别是: NavGraph: 解析导航图xml获取到的对象,其内部主要维护了一个集合用来存储目的地,当导航到目的地时,会传递进来一个id,这个id可能导航图xml中fragment的id,也有可能是fragment节点下action节点的id,如果是action节点的

  • Android关于BottomNavigationView使用指南

    目录 前言 一.初识BottomNavigationView 二.BottomNavigationView中的颜色关键实现代码解析(举例) 三.开始解决问题 1.如何修改图标颜色 2.如何使图标点击颜色不改变 3.如何使点击时字体不改变大小 4.当你的图标是多色系时 5.不想要ActionBar 四.总结 前言 好久不见,计蒙回来了,最近有粉丝投稿了几个关于BottomNavigationView的一些问题,今天发篇比较详细的文章总结一下,希望能够对你有所帮助. 提示:以下是本篇文章正文内容,下

  • Android使用BottomNavigationBar实现导航栏功能

    基本属性 setActiveColor //选中item的字体颜色 setInActiveColor //未选中Item中的颜色 setBarBackgroundColor//背景颜色 setMode(BottomNavigationBar.MODE_FIXED) //填充模式,未选中的Item会显示文字,没有换挡动画 setMode(BottomNavigationBar.MODE_SHIFTING) //换挡模式,未选中的Item不会显示文字,选中的会显示文字 setBackgroundSt

  • 一文详解 Compose Navigation 的实现原理

    目录 前言 1. 从 Jetpack Navigation 说起 2. 定义导航 3. 导航跳转 4. 保存状态 SaveableStateHolder & rememberSaveable 导航回退时的状态保存 底部导航栏切换时的状态保存 5. 导航转场动画 6. Hilt & Navigation 7. 总结 前言 一个纯 Compose 项目少不了页面导航的支持,而 navigation-compose 几乎是这方面的唯一选择,这也使得它成为 Compose 工程的标配二方库.介绍 

  • 一文详解Python中生成器的原理与使用

    目录 什么是生成器 迭代器和生成器的区别 创建方式 生成器表达式 基本语法 生成器函数 yield关键字 yield和return yield的使用方法 生成器函数的基本使用 send的使用 可迭代对象的优化 总结 我们学习完推导式之后发现,推导式就是在容器中使用一个for循环而已,为什么没有元组推导式? 原因就是“元组推导式”的名字不是这样的,而是叫做生成器表达式. 什么是生成器 生成器表达式本质上就是一个迭代器,是定义迭代器的一种方式,是允许自定义逻辑的迭代器.生成器使用generator表

  • 一文详解凯撒密码的原理及Python实现

    目录 一.什么是恺撒密码 二.程序运行环境 三.恺撒密码:加密 3.1 恺撒密码加密实例程序 3.2 恺撒密码加密实例程序运行结果 四.恺撒密码:解密 4.1 恺撒密码解密实例程序 4.2 恺撒密码解密实例程序运行结果 五.完整程序 六.总结 一.什么是恺撒密码 恺撒密码是古罗马恺撒大帝用来对军事情报进行加密的算法,它采用了替换方法对信息中的每一个英文字符循环替换为字母表序列该字符后面第三个字符: 原文:A B C D E F G H I J K L M N O P Q R S T U V W

  • 一文详解Go语言单元测试的原理与使用

    目录 前言 为什么要引用单元测试类 单元测试基本介绍 优点 Testing规范 基本使用 Golang运行 命令行 案例 前言 为什么要引用单元测试类 传统方法的缺点分析 不方便,我们需要在main函数中去调用,这样就需要去修改main函数,如果现在项目正在运行,就可能去停止项目 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在main函数,不利于我们管理和清晰我们的思路 单元测试基本介绍 Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性

  • 一文详解React Redux使用方法

    目录 一.理解JavaScript纯函数 1.1 纯函数的概念 1.2 副作用概念的理解 1.3 纯函数在函数式编程的重要性 二.Redux的核心思想 2.1 为什么需要 Redux 2.2 Redux的核心概念 2.2.1 store 2.2.2 action 2.2.3 reducer 2.3 Redux的三大原则 2.3.1 单一数据源 2.3.2 State是只读的 2.3.3 使用纯函数来执行修改 2.4 Redux 工作流程 三.Redux基本使用 3.1 创建Store的过程 3.

  • 一文详解 OpenGL ES 纹理颜色混合的方法

    目录 一.混合API 二.参数含义 2.1 举个栗子 2.2 参数含义 三. 几种常用混合方式效果 3.1 混合(GL_ONE, GL_ZERO) 3.2 混合(GL_ONE, GL_ONE) 3.3 混合(GL_ONE, GL_ONE_MINUS_DST_ALPHA) 3.4 混合 (GL_SRC_ALPHA, GL_ONE) 3.5 混合 (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 在OpenGL中绘制的时候,有时候想使新画的颜色和已经有的颜色按照一定的方式

  • 一文详解JS私有属性的6种实现方式

    目录 _prop Proxy Symbol WeakMap #prop ts private 总结 class 是创建对象的模版,由一系列属性和方法构成,用于表示对同一概念的数据和操作. 有的属性和方法是对外的,但也有的是只想内部用的,也就是私有的,那怎么实现私有属性和方法呢? 不知道大家会怎么实现,我梳理了下,我大概用过 6 种方式,我们分别来看一下: _prop 区分私有和公有最简单的方式就是加个下划线 _,从命名上来区分. 比如: class Dong { constructor() {

  • 一文详解JS中的事件循环机制

    目录 前言 1.JavaScript是单线程的 2.同步和异步 3.事件循环 前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制. 1.JavaScript是单线程的 JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事.在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行.在

  • 一文详解如何用原型链的方式实现JS继承

    目录 原型链是什么 通过构造函数创建实例对象 用原型链的方式实现继承 方法1:Object.create 方法2:直接修改 [[prototype]] 方法3:使用父类的实例 总结 今天讲一道经典的原型链面试题. 原型链是什么 JavaScript 中,每当创建一个对象,都会给这个对象提供一个内置对象 [[Prototype]] .这个对象就是原型对象,[[Prototype]] 的层层嵌套就形成了原型链. 当我们访问一个对象的属性时,如果自身没有,就会通过原型链向上追溯,找到第一个存在该属性原

  • 一文详解Java拦截器与过滤器的使用

    目录 流程图 拦截器vs过滤器 SpringMVC技术架构图 项目Demo 依赖 Interceptor拦截器 Filter过滤器 1.多Filter不指定过滤顺序 2.多Filter指定过滤顺序 流程图 拦截器vs过滤器 拦截器是SpringMVC的技术 过滤器的Servlet的技术 先过过滤器,过滤器过完才到DispatcherServlet: 拦截器归属于SpringMVC,只可能拦SpringMVC的东西: 拦截器说白了就是为了增强,可以在请求前进行增强,也可以在请求后进行增强,但是不一

随机推荐