<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
一個純 Compose 專案少不了頁面導航的支援,而 navigation-compose 幾乎是這方面的唯一選擇,這也使得它成為 Compose 工程的標配二方庫。介紹 navigation-compose 如何使用的文章很多了,然而在程式碼設計上 Navigation 也非常值得大家學習,那麼本文就帶大家深挖一下其實現原理
Jetpack Navigatioin 是一個通用的頁面導航框架,navigation-compose 只是其針對 Compose 的的一個具體實現。
拋開具體實現,Navigation 在核心公共層定義了以下重要角色:
角色 | 說明 |
---|---|
NavHost | 定義導航的入口,同時也是承載導航頁面的容器 |
NavController | 導航的全域性管理者,維護著導航的靜態和動態資訊,靜態資訊指 NavGraph,動態資訊即導航過長中產生的回退棧 NavBackStacks |
NavGraph | 定義導航時,需要收集各個節點的導航資訊,並統一註冊到導航圖中 |
NavDestination | 導航中的各個節點,攜帶了 route,arguments 等資訊 |
Navigator | 導航的具體執行者,NavController 基於導航圖獲取目標節點,並通過 Navigator 執行跳轉 |
上述角色中的 NavHost
、Navigatot
、NavDestination
等在不同場景中都有對應的實現。例如在傳統檢視中,我們使用 Activity 或者 Fragment 承載頁面,以 navigation-fragment 為例:
再看一下我們今天的主角 navigation-compose。像 navigation-fragment 一樣,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
進行建立,內部會基於 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 獲取的 ComposeNavigatorcontent
: 當前頁面對應的 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 是如何被呼叫的。
跟 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 函數。
前面我們瞭解了導航定義和導航跳轉的具體實現原理,接下來看一下導航過程中的狀態儲存。 navigation-compose 的狀態儲存主要發生在以下兩個場景中:
上述場景中,我們希望在頁面切換過程中,不會丟失例如卷軸位置等的頁面狀態,但是通過前面的程式碼分析,我們也知道了 Compose 導航的頁面切換本質上就是在重組呼叫不同的 Composable。預設情況下,Composable 的狀態隨著其從 Composition 中的離開(即重組中不再被執行)而丟失。那麼 navigation-compose 是如何避免狀態丟失的呢?這裡的關鍵就是前面程式碼中出現的 SaveableStateHolder
了。
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
可見,在基於導航的單頁面架構中,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,恢復狀態。
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#AnimatedContent
。finalEnter
和 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 。
由於每個 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 的範圍,這裡就不深入研究了。
navigation-compose 的其他一些功能例如 Deeplinks,Arguments 等等,在實現上針對 Compose 沒有什麼特殊處理,這裡就不特別介紹了,有興趣可以翻閱 navigation-common 的原始碼。通過本文的一系列介紹,我們可以看出 navigation-compose 無論在 API 的設計上還是在具體實現上,都遵循了宣告式的基本思想,當我們需要開發自己的 Compose 三方庫時,可以從中參考和借鑑。
到此這篇關於一文詳解 Compose Navigation 的實現原理的文章就介紹到這了,更多相關 Compose Navigation 實現內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45