一文詳解?Compose?Navigation?的實現(xiàn)原理
前言
一個純 Compose 項目少不了頁面導航的支持,而 navigation-compose 幾乎是這方面的唯一選擇,這也使得它成為 Compose 工程的標配二方庫。介紹 navigation-compose 如何使用的文章很多了,然而在代碼設計上 Navigation 也非常值得大家學習,那么本文就帶大家深挖一下其實現(xiàn)原理
1. 從 Jetpack Navigation 說起
Jetpack Navigatioin 是一個通用的頁面導航框架,navigation-compose 只是其針對 Compose 的的一個具體實現(xiàn)。
拋開具體實現(xiàn),Navigation 在核心公共層定義了以下重要角色:
角色 | 說明 |
---|---|
NavHost | 定義導航的入口,同時也是承載導航頁面的容器 |
NavController | 導航的全局管理者,維護著導航的靜態(tài)和動態(tài)信息,靜態(tài)信息指 NavGraph,動態(tài)信息即導航過長中產(chǎn)生的回退棧 NavBackStacks |
NavGraph | 定義導航時,需要收集各個節(jié)點的導航信息,并統(tǒng)一注冊到導航圖中 |
NavDestination | 導航中的各個節(jié)點,攜帶了 route,arguments 等信息 |
Navigator | 導航的具體執(zhí)行者,NavController 基于導航圖獲取目標節(jié)點,并通過 Navigator 執(zhí)行跳轉 |
上述角色中的 NavHost
、Navigatot
、NavDestination
等在不同場景中都有對應的實現(xiàn)。例如在傳統(tǒng)視圖中,我們使用 Activity 或者 Fragment 承載頁面,以 navigation-fragment 為例:
- Frament 就是導航圖中的一個個 NavDestination,我們通過 DSL 或者 XMlL 方式定義 NavGraph ,將 Fragment 信息以 NavDestination 的形式收集到導航圖
- NavHostFragment 作為 NavHost 為 Fragment 頁面的展現(xiàn)提供容器
- 我們通過 FragmentNavigator 實現(xiàn)具體頁面跳轉邏輯,F(xiàn)ragmentNavigator#navigate 的實現(xiàn)中基于 FragmentTransaction#replace 實現(xiàn)頁面替換,通過 NavDestination 關聯(lián)的的 Fragment 類信息,實例化 Fragment 對象,完成 replace。
再看一下我們今天的主角 navigation-compose。像 navigation-fragment 一樣,Compose 針對 Navigator 以及 NavDestination 都是自己的具體實現(xiàn),有點特殊的是 NavHost,它只是一個 Composable 函數(shù),所以與公共庫沒有繼承關系:
不同于 Fragment 這樣對象組件,Compose 使用函數(shù)定義頁面,那么 navigation-compose 是如何將 Navigation 落地到 Compose 這樣的聲明式框架中的呢?接下來我們分場景進行介紹。
2. 定義導航
NavHost(navController = navController, startDestination = "profile") { composable("profile") { Profile(/*...*/) } composable("friendslist") { FriendsList(/*...*/) } /*...*/ }
Compose 中的 NavHost 本質(zhì)上是一個 Composable 函數(shù),與 navigation-runtime 中的同名接口沒有派生關系,但職責是相似的,主要目的都是構建 NavGraph。 NavGraph 創(chuàng)建后會被 NavController 持有并在導航中使用,因此 NavHost 接受一個 NavController 參數(shù),并為其賦值 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 及其同名函數(shù)中完成對 NavController 的 NavGraph 賦值。
代碼中 NavGraph 通過 navController#createGraph
進行創(chuàng)建,內(nèi)部會基于 NavGraphBuilder 創(chuàng)建 NavGraph 對象,在 build 過程中,調(diào)用 NavHost{...}
參數(shù)中的 builder 完成初始化。這個 builder 是 NavGraphBuilder 的擴展函數(shù),我們在使用 NavHost{...}
定義導航時,會在 {...} 這里面通過一系列 · 定義 Compose 中的導航頁面。· 也是 NavGraphBuilder 的擴展函數(shù),通過參數(shù)傳入頁面在導航中的唯一 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(...)
的具體實現(xiàn)如上,創(chuàng)建一個 ComposeNavigator.Destination
并通過 NavGraphBuilder#addDestination
添加到 NavGraph 的 nodes 中。 在構建 Destination 時傳入兩個成員:
provider[ComposeNavigator::class]
:通過 NavigatorProvider 獲取的 ComposeNavigatorcontent
: 當前頁面對應的 Composable 函數(shù)
當然,這里還會為 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 通過調(diào)用這個 content,顯示當前導航節(jié)點對應的頁面,后文會看到這個 content 是如何被調(diào)用的。
3. 導航跳轉
跟 Fragment 導航一樣,Compose 當好也是通過 NavController#navigate
指定 route 進行頁面跳轉
navController.navigate("friendslist")
如前所述 NavController· 最終通過 Navigator 實現(xiàn)具體的跳轉邏輯,比如 FragmentNavigator
通過 FragmentTransaction#replace
實現(xiàn) Fragment 頁面的切換,那我們看一下 ComposeNavigator#navigate
的具體實現(xiàn):
//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 之后新引入的類型,用來封裝導航過程中的狀態(tài)供 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 頁面發(fā)生跳轉時,會基于目的地 Destination 創(chuàng)建對應的 NavBackStackEntry ,然后經(jīng)過 pushWithTransition
壓入回退棧。backStack 是一個 StateFlow 類型,所以回退棧的變化可以被監(jiān)聽?;乜?nbsp;NavHost{...}
函數(shù)的實現(xiàn),我們會發(fā)現(xiàn)原來在這里監(jiān)聽了 backState 的變化,根據(jù)棧頂?shù)淖兓{(diào)用對應的 Composable 函數(shù)實現(xiàn)了頁面的切換。
//androidx/navigation/compose/ComposeNavigator.kt @Composable public fun NavHost( navController: NavHostController, graph: NavGraph, modifier: Modifier = Modifier ) { //... // 為 NavController 設置 NavGraph navController.graph = graph //SaveableStateHolder 用于記錄 Composition 的局部狀態(tài),后文介紹 val saveableStateHolder = rememberSaveableStateHolder() //... // 最新的 visibleEntries 來自 backStack 的變化 val visibleEntries = //... val backStackEntry = visibleEntries.lastOrNull() if (backStackEntry != null) { Crossfade(backStackEntry.id, modifier) { //... val lastEntry = backStackEntry lastEntry.LocalOwnersProvider(saveableStateHolder) { //調(diào)用 Destination#content 顯示當前導航對應的頁面 (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry) } } } //... }
如上,NavHost 中除了為 NavController 設置 NavGraph,更重要的工作是監(jiān)聽 backStack 的變化刷新頁面。
navigation-framgent 中的頁面切換在 FragmentNavigator 中命令式的完成的,而 navigation-compose 的頁面切換是在 NavHost 中用響應式的方式進行刷新,這也體現(xiàn)了聲明式 UI與命令式 UI 在實現(xiàn)思路上的不同。
visibleEntries
是基于 NavigatorState#backStack
得到的需要顯示的 Entry,它是一個 State,所以當其變化時 NavHost 會發(fā)生重組,Crossfade
會根據(jù) visibleEntries 顯示對應的頁面。頁面顯示的具體實現(xiàn)也非常簡單,在 NavHost 中調(diào)用 BackStack 應的 Destination#content
即可,這個 content 就是我們在 NavHost{...}
中為每個頁面定義的 Composable 函數(shù)。
4. 保存狀態(tài)
前面我們了解了導航定義和導航跳轉的具體實現(xiàn)原理,接下來看一下導航過程中的狀態(tài)保存。 navigation-compose 的狀態(tài)保存主要發(fā)生在以下兩個場景中:
- 點擊系統(tǒng) back 鍵或者調(diào)用 NavController#popup 時,導航棧頂?shù)?backStackEntry 彈出,導航返回前一頁面,此時我們希望前一頁面的狀態(tài)得到保持
- 在配合底部導航欄使用時,點擊 nav bar 的 Item 可以在不同頁面間切換,此時我們希望切換回來的頁面保持之前的狀態(tài)
上述場景中,我們希望在頁面切換過程中,不會丟失例如滾動條位置等的頁面狀態(tài),但是通過前面的代碼分析,我們也知道了 Compose 導航的頁面切換本質(zhì)上就是在重組調(diào)用不同的 Composable。默認情況下,Composable 的狀態(tài)隨著其從 Composition 中的離開(即重組中不再被執(zhí)行)而丟失。那么 navigation-compose 是如何避免狀態(tài)丟失的呢?這里的關鍵就是前面代碼中出現(xiàn)的 SaveableStateHolder
了。
SaveableStateHolder & rememberSaveable
SaveableStateHolder 來自 compose-runtime ,定義如下:
interface SaveableStateHolder { @Composable fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) fun removeState(key: Any) }
從名字上不難理解 SaveableStateHolder
維護著可保存的狀態(tài)(Saveable State),我們可以在它提供的 SaveableStateProvider
內(nèi)部調(diào)用 Composable 函數(shù),Composable 調(diào)用過程中使用 rememberSaveable
定義的狀態(tài)都會通過 key 進行保存,不會隨著 Composable 的生命周期的結束而丟棄,當下次 SaveableStateProvider 執(zhí)行時,可以通過 key 恢復保存的狀態(tài)。我們通過一個實驗來了解一下 SaveableStateHolder 的作用:
@Composable fun SaveableStateHolderDemo(flag: Boolean) { val saveableStateHolder = rememberSaveableStateHolder() Box { if (flag) { saveableStateHolder.SaveableStateProvider(true) { Screen1() } } else { saveableStateHolder.SaveableStateProvider(false) { Screen2() } } }
上述代碼,我們可以通過傳入不同 flag 實現(xiàn) Screen1 和 Screen2 之前的切換,saveableStateHolder.SaveableStateProvider
可以保證 Screen 內(nèi)部狀態(tài)被保存。例如你在 Screen1 中使用 rememberScrollState()
定義了一個滾動條狀態(tài),當 Screen1 再次顯示時滾動條仍然處于消失時的位置,因為 rememberScrollState 內(nèi)部使用 rememberSaveable 保存了滾動條的位置。
remember, rememberSaveable 可以跨越 Composable 的生命周期更長久的保存狀態(tài),在橫豎屏切換甚至進程重啟的場景中可以實現(xiàn)狀態(tài)恢復。
需要注意的是,如果我們在 SaveableStateProvider 之外使用 rememberSaveable ,雖然可以在橫豎屏切換時保存狀態(tài),但是在導航場景中是無法保存狀態(tài)的。因為使用 rememberSaveable 定義的狀態(tài)只有在配置變化時會被自動保存,但是在普通的 UI 結構變化時不會觸發(fā)保存,而 SaveableStateProvider 主要作用就是能夠在 onDispose
的時候?qū)崿F(xiàn)狀態(tài)保存,
主要代碼如下:
//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 保存狀態(tài) registryHolder.saveTo(savedStates) ... } } }
rememberSaveable 中的通過 SaveableStateRegistry
進行保存,上面代碼中可以看到在 onDispose 生命周期中,通過 registryHolder#saveTo
將狀態(tài)保存到了 savedStates,savedStates 用于下次進入 Composition 時的狀態(tài)恢復。
順便提一下,這里使用 ReusableContent{...}
可以基于 key 復用 LayoutNode,有利于 UI 更快速地重現(xiàn)。
導航回退時的狀態(tài)保存
簡單介紹了一下 SaveableStateHolder 的作用之后,我們看一下在 NavHost 中它是如何發(fā)揮作用的:
@Composable public fun NavHost( ... ) { ... //SaveableStateHolder 用于記錄 Composition 的局部狀態(tài),后文介紹 val saveableStateHolder = rememberSaveableStateHolder() ... Crossfade(backStackEntry.id, modifier) { ... lastEntry.LocalOwnersProvider(saveableStateHolder) { //調(diào)用 Destination#content 顯示當前導航對應的頁面 (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry) } } ... }
lastEntry.LocalOwnersProvider(saveableStateHolder)
內(nèi)部調(diào)用了 Destination#content
, LocalOwnersProvider 內(nèi)部其實就是對 SaveableStateProvider 的調(diào)用:
@Composable public fun NavBackStackEntry.LocalOwnersProvider( saveableStateHolder: SaveableStateHolder, content: @Composable () -> Unit ) { CompositionLocalProvider( LocalViewModelStoreOwner provides this, LocalLifecycleOwner provides this, LocalSavedStateRegistryOwner provides this ) { // 調(diào)用 SaveableStateProvider saveableStateHolder.SaveableStateProvider(content) } }
如上,在調(diào)用 SaveableStateProvider 之前,通過 CompositonLocal 注入了很多 Owner,這些 Owner 的實現(xiàn)都是 this,即指向當前的 NavBackStackEntry
- LocalViewModelStoreOwner : 可以基于 BackStackEntry 的創(chuàng)建和管理 ViewModel
- LocalLifecycleOwner:提供 LifecycleOwner,便于進行基于 Lifecycle 訂閱等操作
- LocalSavedStateRegistryOwner:通過 SavedStateRegistry 注冊狀態(tài)保存的回調(diào),例如 rememberSaveable 中的狀態(tài)保存其實通過 SavedStateRegistry 進行注冊,并在特定時間點被回調(diào)
可見,在基于導航的單頁面架構中,NavBackStackEntry 承載了類似 Fragment 一樣的責任,例如提供頁面級的 ViewModel 等等。
前面提到,SaveableStateProvider 需要通過 key 恢復狀態(tài),那么這個 key 是如何指定的呢。
LocalOwnersProvider 中調(diào)用的 SaveableStateProvider 沒有指定參數(shù) key,原來它是對內(nèi)部調(diào)用的包裝:
@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 調(diào)用在這里,而 key 是通過 ViewModel 管理的。因為 NavBackStackEntry 本身就是 ViewModelStoreOwner,新的 NavBackStackEntry 被壓棧時,下面的 NavBackStackEntry 以及其所轄的 ViewModel 依然存在。當 NavBackStackEntry 重新回到棧頂時,可以從 BackStackEntryIdViewModel 中獲取之前保存的 id,傳入 SaveableStateProvider。
BackStackEntryIdViewModel 的實現(xiàn)如下:
//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 狀態(tài)就不會丟失。當前 NavBackStackEntry 出棧后,對應 ViewModel 發(fā)生 onCleared ,此時會通過 saveableStateHolder#removeState removeState 清空狀態(tài),后續(xù)再次導航至此 Destination 時,不會遺留之前的狀態(tài)。
底部導航欄切換時的狀態(tài)保存
navigation-compose 常用來配合 BottomNavBar 實現(xiàn)多Tab頁的切換。如果我們直接使用 NavController#navigate 切換 Tab 頁,會造成 NavBackStack 的無限增長,所以我們需要在頁面切換后,從棧里及時移除不需要顯示的頁面,例如下面這樣:
val navController = rememberNavController() Scaffold( bottomBar = { BottomNavigation { ... items.forEach { screen -> BottomNavigationItem( ... onClick = { navController.navigate(screen.route) { // 避免 BackStack 增長,跳轉頁面時,將棧內(nèi) startDestination 之外的頁面彈出 popUpTo(navController.graph.findStartDestination().id) { //出棧的 BackStack 保存狀態(tài) saveState = true } // 避免點擊同一個 Item 時反復入棧 launchSingleTop = true // 如果之前出棧時保存狀態(tài)了,那么重新入棧時恢復狀態(tài) restoreState = true } } ) } } } ) { NavHost(...) { ... } }
上面代碼的關鍵是通過設置 saveState 和 restoreState,保證了 NavBackStack 出棧時,保存對應 Destination 的狀態(tài),當 Destination 再次被壓棧時可以恢復。
狀態(tài)想要保存就意味著相關的 ViewModle 不能銷毀,而前面我們知道了 NavBackStack 是 ViewModelStoreOwner,如何在 NavBackStack 出棧后繼續(xù)保存 ViewModel 呢?其實 NavBackStack 所轄的 ViewModel 是存在 NavController 中管理的
從上面的類圖可以看清他們的關系, NavController 持有一個 NavControllerViewModel,它是 NavViewModelStoreProvider 的實現(xiàn),通過 Map 管理著各 NavController 對應的 ViewModelStore。NavBackStackEntry 的 ViewModelStore 就取自 NavViewModelStoreProvider 。
當 NavBackStackEntry 出棧時,其對應的 Destination#content 移出畫面,執(zhí)行 onDispose,
Crossfade(backStackEntry.id, modifier) { ... DisposableEffect(Unit) { ... onDispose { visibleEntries.forEach { entry -> //顯示中的 Entry 移出屏幕,調(diào)用 onTransitionComplete composeNavigator.onTransitionComplete(entry) } } } lastEntry.LocalOwnersProvider(saveableStateHolder) { (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry) } }
onTransitionComplete 中調(diào)用 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,這里會執(zhí)行 viewModel#clear 清空 entry 對應的 ViewModel,但是當我們在 popUpTo { ... } 中設置 saveState 為 true 時,entrySavedState[entry] 就為 true,因此此處就不會執(zhí)行 ViewModel#clear。
如果我們同時設置了 restoreState 為 true,當下次同類型 Destination 進入頁面時,k可以通過 ViewModle 恢復狀態(tài)。
//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 中根據(jù) DestinationId 找到之前對應的 BackStackId,進而通過 BackStackId 找回 ViewModel,恢復狀態(tài)。
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 實現(xiàn)導航動畫。
注意:navigation-compose 依賴的 Comopse 動畫 API 例如 AnimatedContent 等目前尚處于實驗狀態(tài),因此導航動畫暫時只能通過 accompanist-navigation-animation 引入,待動畫 API 穩(wěn)定后,未來會移入 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
中統(tǒng)一指定 Transition 動畫,也可以在各個 composable 參數(shù)中分別指定。
回想一下,NavHost 中的 Destination#content
是在 Crossfade 中調(diào)用的,熟悉 Compose 動畫的就不難聯(lián)想到,可以在此處使用 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 的主要區(qū)別就是將 Crossfade 換成了 Transition#AnimatedContent
。finalEnter
和 finalExit
是根據(jù)參數(shù)計算得到的 Compose Transition 動畫,通過 transitionSpec
進行指定。以 finalEnter 為例看一下具體實現(xiàn)
val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = { val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination if (composeNavigator.isPop.value) { //當前頁面即將出棧,執(zhí)行pop動畫 targetDestination.hierarchy.firstNotNullOfOrNull { destination -> //popEnterTransitions 中存儲著通過 composable 參數(shù)指定的動畫 popEnterTransitions[destination.route]?.invoke(this) } ?: popEnterTransition.invoke(this) } else { //當前頁面即將入棧,執(zhí)行enter動畫 targetDestination.hierarchy.firstNotNullOfOrNull { destination -> enterTransitions[destination.route]?.invoke(this) } ?: enterTransition.invoke(this) } }
如上,popEnterTransitions[destination.route]
是 composable(...) 參數(shù)中指定的動畫,所以 composable 參數(shù)指定的動畫優(yōu)先級高于 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
注解,其參數(shù)依賴的 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 等等,在實現(xiàn)上針對 Compose 沒有什么特殊處理,這里就不特別介紹了,有興趣可以翻閱 navigation-common 的源碼。通過本文的一系列介紹,我們可以看出 navigation-compose 無論在 API 的設計上還是在具體實現(xiàn)上,都遵循了聲明式的基本思想,當我們需要開發(fā)自己的 Compose 三方庫時,可以從中參考和借鑒。
到此這篇關于一文詳解 Compose Navigation 的實現(xiàn)原理的文章就介紹到這了,更多相關 Compose Navigation 實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!