溫馨提示×

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

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Compose?Navigation的實(shí)現(xiàn)原理是什么

發(fā)布時(shí)間:2022-08-25 16:58:37 來源:億速云 閱讀:126 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“Compose Navigation的實(shí)現(xiàn)原理是什么”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“Compose Navigation的實(shí)現(xiàn)原理是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識(shí)吧。

    1. 從 Jetpack Navigation 說起

    Jetpack Navigatioin 是一個(gè)通用的頁(yè)面導(dǎo)航框架,navigation-compose 只是其針對(duì) Compose 的的一個(gè)具體實(shí)現(xiàn)。

    拋開具體實(shí)現(xiàn),Navigation 在核心公共層定義了以下重要角色:

    角色說明
    NavHost定義導(dǎo)航的入口,同時(shí)也是承載導(dǎo)航頁(yè)面的容器
    NavController導(dǎo)航的全局管理者,維護(hù)著導(dǎo)航的靜態(tài)和動(dòng)態(tài)信息,靜態(tài)信息指 NavGraph,動(dòng)態(tài)信息即導(dǎo)航過長(zhǎng)中產(chǎn)生的回退棧 NavBackStacks
    NavGraph定義導(dǎo)航時(shí),需要收集各個(gè)節(jié)點(diǎn)的導(dǎo)航信息,并統(tǒng)一注冊(cè)到導(dǎo)航圖中
    NavDestination導(dǎo)航中的各個(gè)節(jié)點(diǎn),攜帶了 route,arguments 等信息
    Navigator導(dǎo)航的具體執(zhí)行者,NavController 基于導(dǎo)航圖獲取目標(biāo)節(jié)點(diǎn),并通過 Navigator 執(zhí)行跳轉(zhuǎn)

    Compose?Navigation的實(shí)現(xiàn)原理是什么

    上述角色中的 NavHost、NavigatotNavDestination 等在不同場(chǎng)景中都有對(duì)應(yīng)的實(shí)現(xiàn)。例如在傳統(tǒng)視圖中,我們使用 Activity 或者 Fragment 承載頁(yè)面,以 navigation-fragment 為例:

    • Frament 就是導(dǎo)航圖中的一個(gè)個(gè) NavDestination,我們通過 DSL 或者 XMlL 方式定義 NavGraph ,將 Fragment 信息以 NavDestination 的形式收集到導(dǎo)航圖

    • NavHostFragment 作為 NavHost 為 Fragment 頁(yè)面的展現(xiàn)提供容器

    • 我們通過 FragmentNavigator 實(shí)現(xiàn)具體頁(yè)面跳轉(zhuǎn)邏輯,F(xiàn)ragmentNavigator#navigate 的實(shí)現(xiàn)中基于 FragmentTransaction#replace 實(shí)現(xiàn)頁(yè)面替換,通過 NavDestination 關(guān)聯(lián)的的 Fragment 類信息,實(shí)例化 Fragment 對(duì)象,完成 replace。

    再看一下我們今天的主角 navigation-compose。像 navigation-fragment 一樣,Compose 針對(duì) Navigator 以及 NavDestination 都是自己的具體實(shí)現(xiàn),有點(diǎn)特殊的是 NavHost,它只是一個(gè) Composable 函數(shù),所以與公共庫(kù)沒有繼承關(guān)系:

    Compose?Navigation的實(shí)現(xiàn)原理是什么

    不同于 Fragment 這樣對(duì)象組件,Compose 使用函數(shù)定義頁(yè)面,那么 navigation-compose 是如何將 Navigation 落地到 Compose 這樣的聲明式框架中的呢?接下來我們分場(chǎng)景進(jìn)行介紹。

    2. 定義導(dǎo)航

    NavHost(navController = navController, startDestination = "profile") {
        composable("profile") { Profile(/*...*/) }
        composable("friendslist") { FriendsList(/*...*/) }
        /*...*/
    }

    Compose 中的 NavHost 本質(zhì)上是一個(gè) Composable 函數(shù),與 navigation-runtime 中的同名接口沒有派生關(guān)系,但職責(zé)是相似的,主要目的都是構(gòu)建 NavGraph。 NavGraph 創(chuàng)建后會(huì)被 NavController 持有并在導(dǎo)航中使用,因此 NavHost 接受一個(gè) 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
    ) {
    
        //...
        //設(shè)置 NavGraph
        navController.graph = graph
        //...
        
    }

    如上,在 NavHost 及其同名函數(shù)中完成對(duì) NavController 的 NavGraph 賦值。

    代碼中 NavGraph 通過 navController#createGraph 進(jìn)行創(chuàng)建,內(nèi)部會(huì)基于 NavGraphBuilder 創(chuàng)建 NavGraph 對(duì)象,在 build 過程中,調(diào)用 NavHost{...} 參數(shù)中的 builder 完成初始化。這個(gè) builder 是 NavGraphBuilder 的擴(kuò)展函數(shù),我們?cè)谑褂?nbsp;NavHost{...} 定義導(dǎo)航時(shí),會(huì)在 {...} 這里面通過一系列 · 定義 Compose 中的導(dǎo)航頁(yè)面。· 也是 NavGraphBuilder 的擴(kuò)展函數(shù),通過參數(shù)傳入頁(yè)面在導(dǎo)航中的唯一 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(...) 的具體實(shí)現(xiàn)如上,創(chuàng)建一個(gè) ComposeNavigator.Destination 并通過 NavGraphBuilder#addDestination 添加到 NavGraph 的 nodes 中。 在構(gòu)建 Destination 時(shí)傳入兩個(gè)成員:

    • provider[ComposeNavigator::class] :通過 NavigatorProvider 獲取的 ComposeNavigator

    • content : 當(dāng)前頁(yè)面對(duì)應(yīng)的 Composable 函數(shù)

    當(dāng)然,這里還會(huì)為 Destination 傳入 route,arguments,deeplinks 等信息。

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

    非常簡(jiǎn)單,就是在繼承自 NavDestination 之外,多存儲(chǔ)了一個(gè) Compsoable 的 content。Destination 通過調(diào)用這個(gè) content,顯示當(dāng)前導(dǎo)航節(jié)點(diǎn)對(duì)應(yīng)的頁(yè)面,后文會(huì)看到這個(gè) content 是如何被調(diào)用的。

    3. 導(dǎo)航跳轉(zhuǎn)

    跟 Fragment 導(dǎo)航一樣,Compose 當(dāng)好也是通過 NavController#navigate 指定 route 進(jìn)行頁(yè)面跳轉(zhuǎn)

    navController.navigate("friendslist")

    如前所述 NavController&middot; 最終通過 Navigator 實(shí)現(xiàn)具體的跳轉(zhuǎn)邏輯,比如 FragmentNavigator 通過 FragmentTransaction#replace 實(shí)現(xiàn) Fragment 頁(yè)面的切換,那我們看一下 ComposeNavigator#navigate 的具體實(shí)現(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)
            }
        }
        //...
    
    }

    這里的處理非常簡(jiǎn)單,沒有 FragmentNavigator 那樣的具體處理。 NavBackStackEntry 代表導(dǎo)航過程中回退棧中的一個(gè)記錄,entries 就是當(dāng)前頁(yè)面導(dǎo)航的回退棧。state 是一個(gè) NavigatorState 對(duì)象,這是 Navigation 2.4.0 之后新引入的類型,用來封裝導(dǎo)航過程中的狀態(tài)供 NavController 等使用,比如 backStack 就是存儲(chǔ)在 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
            }
        }
        
        //...
    }

    當(dāng) Compose 頁(yè)面發(fā)生跳轉(zhuǎn)時(shí),會(huì)基于目的地 Destination 創(chuàng)建對(duì)應(yīng)的 NavBackStackEntry ,然后經(jīng)過 pushWithTransition 壓入回退棧。backStack 是一個(gè) StateFlow 類型,所以回退棧的變化可以被監(jiān)聽?;乜?nbsp;NavHost{...} 函數(shù)的實(shí)現(xiàn),我們會(huì)發(fā)現(xiàn)原來在這里監(jiān)聽了 backState 的變化,根據(jù)棧頂?shù)淖兓{(diào)用對(duì)應(yīng)的 Composable 函數(shù)實(shí)現(xiàn)了頁(yè)面的切換。

    //androidx/navigation/compose/ComposeNavigator.kt
    @Composable
    public fun NavHost(
        navController: NavHostController,
        graph: NavGraph,
        modifier: Modifier = Modifier
    ) {
        //...
    
        // 為 NavController 設(shè)置 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 顯示當(dāng)前導(dǎo)航對(duì)應(yīng)的頁(yè)面
                    (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
                }
            }
        }
    
        //...
    }

    如上,NavHost 中除了為 NavController 設(shè)置 NavGraph,更重要的工作是監(jiān)聽 backStack 的變化刷新頁(yè)面。

    navigation-framgent 中的頁(yè)面切換在 FragmentNavigator 中命令式的完成的,而 navigation-compose 的頁(yè)面切換是在 NavHost 中用響應(yīng)式的方式進(jìn)行刷新,這也體現(xiàn)了聲明式 UI與命令式 UI 在實(shí)現(xiàn)思路上的不同。

    visibleEntries 是基于 NavigatorState#backStack 得到的需要顯示的 Entry,它是一個(gè) State,所以當(dāng)其變化時(shí) NavHost 會(huì)發(fā)生重組,Crossfade 會(huì)根據(jù) visibleEntries 顯示對(duì)應(yīng)的頁(yè)面。頁(yè)面顯示的具體實(shí)現(xiàn)也非常簡(jiǎn)單,在 NavHost 中調(diào)用 BackStack 應(yīng)的 Destination#content 即可,這個(gè) content 就是我們?cè)?nbsp;NavHost{...} 中為每個(gè)頁(yè)面定義的 Composable 函數(shù)。

    Compose?Navigation的實(shí)現(xiàn)原理是什么

    4. 保存狀態(tài)

    前面我們了解了導(dǎo)航定義和導(dǎo)航跳轉(zhuǎn)的具體實(shí)現(xiàn)原理,接下來看一下導(dǎo)航過程中的狀態(tài)保存。 navigation-compose 的狀態(tài)保存主要發(fā)生在以下兩個(gè)場(chǎng)景中:

    • 點(diǎn)擊系統(tǒng) back 鍵或者調(diào)用 NavController#popup 時(shí),導(dǎo)航棧頂?shù)?backStackEntry 彈出,導(dǎo)航返回前一頁(yè)面,此時(shí)我們希望前一頁(yè)面的狀態(tài)得到保持

    • 在配合底部導(dǎo)航欄使用時(shí),點(diǎn)擊 nav bar 的 Item 可以在不同頁(yè)面間切換,此時(shí)我們希望切換回來的頁(yè)面保持之前的狀態(tài)

    上述場(chǎng)景中,我們希望在頁(yè)面切換過程中,不會(huì)丟失例如滾動(dòng)條位置等的頁(yè)面狀態(tài),但是通過前面的代碼分析,我們也知道了 Compose 導(dǎo)航的頁(yè)面切換本質(zhì)上就是在重組調(diào)用不同的 Composable。默認(rèn)情況下,Composable 的狀態(tài)隨著其從 Composition 中的離開(即重組中不再被執(zhí)行)而丟失。那么 navigation-compose 是如何避免狀態(tài)丟失的呢?這里的關(guān)鍵就是前面代碼中出現(xiàn)的 SaveableStateHolder 了。

    SaveableStateHolder & rememberSaveable

    SaveableStateHolder 來自 compose-runtime ,定義如下:

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

    從名字上不難理解 SaveableStateHolder 維護(hù)著可保存的狀態(tài)(Saveable State),我們可以在它提供的 SaveableStateProvider 內(nèi)部調(diào)用 Composable 函數(shù),Composable 調(diào)用過程中使用 rememberSaveable 定義的狀態(tài)都會(huì)通過 key 進(jìn)行保存,不會(huì)隨著 Composable 的生命周期的結(jié)束而丟棄,當(dāng)下次 SaveableStateProvider 執(zhí)行時(shí),可以通過 key 恢復(fù)保存的狀態(tài)。我們通過一個(gè)實(shí)驗(yàn)來了解一下 SaveableStateHolder 的作用:

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

    上述代碼,我們可以通過傳入不同 flag 實(shí)現(xiàn) Screen1 和 Screen2 之前的切換,saveableStateHolder.SaveableStateProvider 可以保證 Screen 內(nèi)部狀態(tài)被保存。例如你在 Screen1 中使用 rememberScrollState() 定義了一個(gè)滾動(dòng)條狀態(tài),當(dāng) Screen1 再次顯示時(shí)滾動(dòng)條仍然處于消失時(shí)的位置,因?yàn)?rememberScrollState 內(nèi)部使用 rememberSaveable 保存了滾動(dòng)條的位置。

    remember, rememberSaveable 可以跨越 Composable 的生命周期更長(zhǎng)久的保存狀態(tài),在橫豎屏切換甚至進(jìn)程重啟的場(chǎng)景中可以實(shí)現(xiàn)狀態(tài)恢復(fù)。

    需要注意的是,如果我們?cè)?SaveableStateProvider 之外使用 rememberSaveable ,雖然可以在橫豎屏切換時(shí)保存狀態(tài),但是在導(dǎo)航場(chǎng)景中是無法保存狀態(tài)的。因?yàn)槭褂?rememberSaveable 定義的狀態(tài)只有在配置變化時(shí)會(huì)被自動(dòng)保存,但是在普通的 UI 結(jié)構(gòu)變化時(shí)不會(huì)觸發(fā)保存,而 SaveableStateProvider 主要作用就是能夠在 onDispose 的時(shí)候?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 進(jìn)行保存,上面代碼中可以看到在 onDispose 生命周期中,通過 registryHolder#saveTo 將狀態(tài)保存到了 savedStates,savedStates 用于下次進(jìn)入 Composition 時(shí)的狀態(tài)恢復(fù)。

    順便提一下,這里使用 ReusableContent{...} 可以基于 key 復(fù)用 LayoutNode,有利于 UI 更快速地重現(xiàn)。

    導(dǎo)航回退時(shí)的狀態(tài)保存

    簡(jiǎn)單介紹了一下 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 顯示當(dāng)前導(dǎo)航對(duì)應(yīng)的頁(yè)面
                    (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
                }
                
            }
    
        ...
    }

    lastEntry.LocalOwnersProvider(saveableStateHolder) 內(nèi)部調(diào)用了 Destination#content, LocalOwnersProvider 內(nèi)部其實(shí)就是對(duì) 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 的實(shí)現(xiàn)都是 this,即指向當(dāng)前的 NavBackStackEntry

    • LocalViewModelStoreOwner : 可以基于 BackStackEntry 的創(chuàng)建和管理 ViewModel

    • LocalLifecycleOwner:提供 LifecycleOwner,便于進(jìn)行基于 Lifecycle 訂閱等操作

    • LocalSavedStateRegistryOwner:通過 SavedStateRegistry 注冊(cè)狀態(tài)保存的回調(diào),例如 rememberSaveable 中的狀態(tài)保存其實(shí)通過 SavedStateRegistry 進(jìn)行注冊(cè),并在特定時(shí)間點(diǎn)被回調(diào)

    可見,在基于導(dǎo)航的單頁(yè)面架構(gòu)中,NavBackStackEntry 承載了類似 Fragment 一樣的責(zé)任,例如提供頁(yè)面級(jí)的 ViewModel 等等。

    前面提到,SaveableStateProvider 需要通過 key 恢復(fù)狀態(tài),那么這個(gè) key 是如何指定的呢。

    LocalOwnersProvider 中調(diào)用的 SaveableStateProvider 沒有指定參數(shù) key,原來它是對(duì)內(nèi)部調(diào)用的包裝:

    @Composable
    private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
        val viewModel = viewModel<BackStackEntryIdViewModel>()
        
        //設(shè)置 saveableStateHolder,后文介紹
        viewModel.saveableStateHolder = this
        
        //
        SaveableStateProvider(viewModel.id, content)
        
        DisposableEffect(viewModel) {
            onDispose {
                viewModel.saveableStateHolder = null
            }
        }
    }

    真正的 SaveableStateProvider 調(diào)用在這里,而 key 是通過 ViewModel 管理的。因?yàn)?NavBackStackEntry 本身就是 ViewModelStoreOwner,新的 NavBackStackEntry 被壓棧時(shí),下面的 NavBackStackEntry 以及其所轄的 ViewModel 依然存在。當(dāng) NavBackStackEntry 重新回到棧頂時(shí),可以從 BackStackEntryIdViewModel 中獲取之前保存的 id,傳入 SaveableStateProvider。

    BackStackEntryIdViewModel 的實(shí)現(xiàn)如下:

    //androidx/navigation/compose/BackStackEntryIdViewModel.kt
    internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
        private val IdKey = "SaveableStateHolder_BackStackEntryKey"
    
        // 唯一 ID,可通過 SavedStateHandle 保存和恢復(fù)
        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 的,但其實(shí)它也是當(dāng)前 BackStackEntry 的 saveableStateHolder 的持有者,ViewModel 在 SaveableStateProvider 中被傳入 saveableStateHolder,只要 ViewModel 存在,UI 狀態(tài)就不會(huì)丟失。當(dāng)前 NavBackStackEntry 出棧后,對(duì)應(yīng) ViewModel 發(fā)生 onCleared ,此時(shí)會(huì)通過 saveableStateHolder#removeState removeState 清空狀態(tài),后續(xù)再次導(dǎo)航至此 Destination 時(shí),不會(huì)遺留之前的狀態(tài)。

    Compose?Navigation的實(shí)現(xiàn)原理是什么

    底部導(dǎo)航欄切換時(shí)的狀態(tài)保存

    navigation-compose 常用來配合 BottomNavBar 實(shí)現(xiàn)多Tab頁(yè)的切換。如果我們直接使用 NavController#navigate 切換 Tab 頁(yè),會(huì)造成 NavBackStack 的無限增長(zhǎng),所以我們需要在頁(yè)面切換后,從棧里及時(shí)移除不需要顯示的頁(yè)面,例如下面這樣:

    val navController = rememberNavController()
    
    Scaffold(
      bottomBar = {
        BottomNavigation {
          ...
          items.forEach { screen ->
            BottomNavigationItem(
              ...
              onClick = {
                navController.navigate(screen.route) {
                  // 避免 BackStack 增長(zhǎng),跳轉(zhuǎn)頁(yè)面時(shí),將棧內(nèi) startDestination 之外的頁(yè)面彈出
                  popUpTo(navController.graph.findStartDestination().id) {
                    //出棧的 BackStack 保存狀態(tài)
                    saveState = true
                  }
                  // 避免點(diǎn)擊同一個(gè) Item 時(shí)反復(fù)入棧
                  launchSingleTop = true
                  
                  // 如果之前出棧時(shí)保存狀態(tài)了,那么重新入棧時(shí)恢復(fù)狀態(tài)
                  restoreState = true
                }
              }
            )
          }
        }
      }
    ) { 
      NavHost(...) {
        ...
      }
    }

    上面代碼的關(guān)鍵是通過設(shè)置 saveState 和 restoreState,保證了 NavBackStack 出棧時(shí),保存對(duì)應(yīng) Destination 的狀態(tài),當(dāng) Destination 再次被壓棧時(shí)可以恢復(fù)。

    狀態(tài)想要保存就意味著相關(guān)的 ViewModle 不能銷毀,而前面我們知道了 NavBackStack 是 ViewModelStoreOwner,如何在 NavBackStack 出棧后繼續(xù)保存 ViewModel 呢?其實(shí) NavBackStack 所轄的 ViewModel 是存在 NavController 中管理的

    Compose?Navigation的實(shí)現(xiàn)原理是什么

    從上面的類圖可以看清他們的關(guān)系, NavController 持有一個(gè) NavControllerViewModel,它是 NavViewModelStoreProvider 的實(shí)現(xiàn),通過 Map 管理著各 NavController 對(duì)應(yīng)的 ViewModelStore。NavBackStackEntry 的 ViewModelStore 就取自 NavViewModelStoreProvider 。

    當(dāng) NavBackStackEntry 出棧時(shí),其對(duì)應(yīng)的 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
            }
            ...
        } 
        
        ...
    }

    默認(rèn)情況下, entrySavedState[entry] 為 false,這里會(huì)執(zhí)行 viewModel#clear 清空 entry 對(duì)應(yīng)的 ViewModel,但是當(dāng)我們?cè)?popUpTo { ... } 中設(shè)置 saveState 為 true 時(shí),entrySavedState[entry] 就為 true,因此此處就不會(huì)執(zhí)行 ViewModel#clear。

    如果我們同時(shí)設(shè)置了 restoreState 為 true,當(dāng)下次同類型 Destination 進(jìn)入頁(yè)面時(shí),k可以通過 ViewModle 恢復(fù)狀態(tài)。

    //androidx/navigation/NavController.kt
    
    private fun navigate(
        ...
    ) {
    
        ...
        //restoreState設(shè)置為true后,命中此處的 shouldRestoreState()
        if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
            navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
        } 
        ...
    }

    restoreStateInternal 中根據(jù) DestinationId 找到之前對(duì)應(yīng)的 BackStackId,進(jìn)而通過 BackStackId 找回 ViewModel,恢復(fù)狀態(tài)。

    5. 導(dǎo)航轉(zhuǎn)場(chǎng)動(dòng)畫

    navigation-fragment 允許我們可以像下面這樣,通過資源文件指定跳轉(zhuǎn)頁(yè)面時(shí)的專場(chǎng)動(dòng)畫

    findNavController().navigate(
        R.id.action_fragmentOne_to_fragmentTwo,
        null,
        navOptions { 
            anim {
                enter = android.R.animator.fade_in
                exit = android.R.animator.fade_out
            }
        }
    )

    由于 Compose 動(dòng)畫不依靠資源文件,navigation-compose 不支持上面這樣的 anim { ... } ,但相應(yīng)地, navigation-compose 可以基于 Compose 動(dòng)畫 API 實(shí)現(xiàn)導(dǎo)航動(dòng)畫。

    注意:navigation-compose 依賴的 Comopse 動(dòng)畫 API 例如 AnimatedContent 等目前尚處于實(shí)驗(yàn)狀態(tài),因此導(dǎo)航動(dòng)畫暫時(shí)只能通過 accompanist-navigation-animation 引入,待動(dòng)畫 API 穩(wěn)定后,未來會(huì)移入 navigation-compose。

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

    添加依賴后可以提前預(yù)覽 navigation-compose 導(dǎo)航動(dòng)畫的 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 動(dòng)畫,也可以在各個(gè) composable 參數(shù)中分別指定。

    回想一下,NavHost 中的 Destination#content 是在 Crossfade 中調(diào)用的,熟悉 Compose 動(dòng)畫的就不難聯(lián)想到,可以在此處使用 AnimatedContent 為 content 的切換指定不同的動(dòng)畫效果,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#AnimatedContentfinalEnter 和 finalExit 是根據(jù)參數(shù)計(jì)算得到的 Compose Transition 動(dòng)畫,通過 transitionSpec 進(jìn)行指定。以 finalEnter 為例看一下具體實(shí)現(xiàn)

    val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
        val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination
    
        if (composeNavigator.isPop.value) {
            //當(dāng)前頁(yè)面即將出棧,執(zhí)行pop動(dòng)畫
            targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
                //popEnterTransitions 中存儲(chǔ)著通過 composable 參數(shù)指定的動(dòng)畫
                popEnterTransitions[destination.route]?.invoke(this)
            } ?: popEnterTransition.invoke(this)
        } else {
            //當(dāng)前頁(yè)面即將入棧,執(zhí)行enter動(dòng)畫
            targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
                enterTransitions[destination.route]?.invoke(this)
            } ?: enterTransition.invoke(this)
        }
    }

    如上,popEnterTransitions[destination.route] 是 composable(...) 參數(shù)中指定的動(dòng)畫,所以 composable 參數(shù)指定的動(dòng)畫優(yōu)先級(jí)高于 AnimatedNavHost 。

    6. Hilt & Navigation

    由于每個(gè) BackStackEntry 都是一個(gè) ViewModelStoreOwner,我們可以獲取導(dǎo)航頁(yè)面級(jí)別的 ViewModel。使用 hilt-viewmodle-navigation 可以通過 Hilt 為 ViewModel 注入必要的依賴,降低 ViewModel 構(gòu)造成本。

    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 自動(dòng)注入,省去我們自定義 ViewModelFactory 的麻煩。

    @HiltViewModel
    class MyViewModel @Inject constructor(
        private val savedStateHandle: SavedStateHandle,
        private val repository: ExampleRepository
    ) : ViewModel() { /* ... */ }

    簡(jiǎn)單看一下 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 就是當(dāng)前的 BackStackEntry,拿到 viewModelStoreOwner 之后,通過 HiltViewModelFactory() 獲取 ViewModelFactory。 HiltViewModelFactory 是 hilt-navigation 的范圍,這里就不深入研究了。

    讀到這里,這篇“Compose Navigation的實(shí)現(xiàn)原理是什么”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI