中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Compose?Navigation的實現原理是什么

發布時間:2022-08-25 16:58:37 來源:億速云 閱讀:132 作者:iii 欄目:開發技術

本文小編為大家詳細介紹“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&middot; 最終通過 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 的狀態保存主要發生在以下兩個場景中:

    • 點擊系統 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 時,不會遺留之前的狀態。

    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#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 的范圍,這里就不深入研究了。

    讀到這里,這篇“Compose Navigation的實現原理是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    临泉县| 旺苍县| 五华县| 黎川县| 连城县| 宁安市| 治县。| 康乐县| 大邑县| 东乌珠穆沁旗| 碌曲县| 莎车县| 东兰县| 光泽县| 万盛区| 辽宁省| 北海市| 仙桃市| 长寿区| 延边| 安泽县| 辽宁省| 斗六市| 乌恰县| 黄陵县| 合山市| 兰溪市| 财经| 泾川县| 深圳市| 海盐县| 襄垣县| 商洛市| 淮南市| 庄浪县| 临海市| 陕西省| 洪江市| 岳普湖县| 黄浦区| 四会市|