一文詳解 Compose Navigation 的實現原理

前言

一個純 Compose 項目少不瞭頁面導航的支持,而 navigation-compose 幾乎是這方面的唯一選擇,這也使得它成為 Compose 工程的標配二方庫。介紹 navigation-compose 如何使用的文章很多瞭,然而在代碼設計上 Navigation 也非常值得大傢學習,那麼本文就帶大傢深挖一下其實現原理

1. 從 Jetpack Navigation 說起

Jetpack Navigatioin 是一個通用的頁面導航框架,navigation-compose 隻是其針對 Compose 的的一個具體實現。

拋開具體實現,Navigation 在核心公共層定義瞭以下重要角色:

角色 說明
NavHost 定義導航的入口,同時也是承載導航頁面的容器
NavController 導航的全局管理者,維護著導航的靜態和動態信息,靜態信息指 NavGraph,動態信息即導航過長中產生的回退棧 NavBackStacks
NavGraph 定義導航時,需要收集各個節點的導航信息,並統一註冊到導航圖中
NavDestination 導航中的各個節點,攜帶瞭 route,arguments 等信息
Navigator 導航的具體執行者,NavController 基於導航圖獲取目標節點,並通過 Navigator 執行跳轉

上述角色中的 NavHostNavigatotNavDestination 等在不同場景中都有對應的實現。例如在傳統視圖中,我們使用 Activity 或者 Fragment 承載頁面,以 navigation-fragment 為例:

  • Frament 就是導航圖中的一個個 NavDestination,我們通過 DSL 或者 XMlL 方式定義 NavGraph ,將 Fragment 信息以 NavDestination 的形式收集到導航圖
  • NavHostFragment 作為 NavHost 為 Fragment 頁面的展現提供容器
  • 我們通過 FragmentNavigator 實現具體頁面跳轉邏輯,FragmentNavigator#navigate 的實現中基於 FragmentTransaction#replace 實現頁面替換,通過 NavDestination 關聯的的 Fragment 類信息,實例化 Fragment 對象,完成 replace。

再看一下我們今天的主角 navigation-compose。像 navigation-fragment 一樣,Compose 針對 Navigator 以及 NavDestination 都是自己的具體實現,有點特殊的是 NavHost,它隻是一個 Composable 函數,所以與公共庫沒有繼承關系:

不同於 Fragment 這樣對象組件,Compose 使用函數定義頁面,那麼 navigation-compose 是如何將 Navigation 落地到 Compose 這樣的聲明式框架中的呢?接下來我們分場景進行介紹。

2. 定義導航

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

Compose 中的 NavHost 本質上是一個 Composable 函數,與 navigation-runtime 中的同名接口沒有派生關系,但職責是相似的,主要目的都是構建 NavGraph。 NavGraph 創建後會被 NavController 持有並在導航中使用,因此 NavHost 接受一個 NavController 參數,並為其賦值 NavGraph

//androidx/navigation/compose/NavHost.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        navController,
        remember(route, startDestination, builder) {
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}

@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {

    //...
    //設置 NavGraph
    navController.graph = graph
    //...
    
}

如上,在 NavHost 及其同名函數中完成對 NavController 的 NavGraph 賦值。

代碼中 NavGraph 通過 navController#createGraph 進行創建,內部會基於 NavGraphBuilder 創建 NavGraph 對象,在 build 過程中,調用 NavHost{...} 參數中的 builder 完成初始化。這個 builder 是 NavGraphBuilder 的擴展函數,我們在使用 NavHost{...} 定義導航時,會在 {…} 這裡面通過一系列 · 定義 Compose 中的導航頁面。· 也是 NavGraphBuilder 的擴展函數,通過參數傳入頁面在導航中的唯一 route。

//androidx/navigation/compose/NavGraphBuilder.kt
public fun NavGraphBuilder.composable(
    route: String,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(
        ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
            this.route = route
            arguments.forEach { (argumentName, argument) ->
                addArgument(argumentName, argument)
            }
            deepLinks.forEach { deepLink ->
                addDeepLink(deepLink)
            }
        }
    )
}

compose(...) 的具體實現如上,創建一個 ComposeNavigator.Destination 並通過 NavGraphBuilder#addDestination 添加到 NavGraph 的 nodes 中。 在構建 Destination 時傳入兩個成員:

  • provider[ComposeNavigator::class] :通過 NavigatorProvider 獲取的 ComposeNavigator
  • content : 當前頁面對應的 Composable 函數

當然,這裡還會為 Destination 傳入 route,arguments,deeplinks 等信息。

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

非常簡單,就是在繼承自 NavDestination 之外,多存儲瞭一個 Compsoable 的 content。Destination 通過調用這個 content,顯示當前導航節點對應的頁面,後文會看到這個 content 是如何被調用的。

3. 導航跳轉

跟 Fragment 導航一樣,Compose 當好也是通過 NavController#navigate 指定 route 進行頁面跳轉

navController.navigate("friendslist")

如前所述 NavController· 最終通過 Navigator 實現具體的跳轉邏輯,比如 FragmentNavigator 通過 FragmentTransaction#replace 實現 Fragment 頁面的切換,那我們看一下 ComposeNavigator#navigate 的具體實現:

//androidx/navigation/compose/ComposeNavigator.kt
public class ComposeNavigator : Navigator<Destination>() {

    //...
    override fun navigate(
        entries: List<NavBackStackEntry>,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ) {
        entries.forEach { entry ->
            state.pushWithTransition(entry)
        }
    }
    //...

}

這裡的處理非常簡單,沒有 FragmentNavigator 那樣的具體處理。 NavBackStackEntry 代表導航過程中回退棧中的一個記錄,entries 就是當前頁面導航的回退棧。state 是一個 NavigatorState 對象,這是 Navigation 2.4.0 之後新引入的類型,用來封裝導航過程中的狀態供 NavController 等使用,比如 backStack 就是存儲在 NavigatorState 中

//androidx/navigation/NavigatorState.kt
public abstract class NavigatorState {
    private val backStackLock = ReentrantLock(true)
    private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
    public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()
    //...   
    public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
        //...
        push(backStackEntry)
    }

    public open fun push(backStackEntry: NavBackStackEntry) {
        backStackLock.withLock {
            _backStack.value = _backStack.value + backStackEntry
        }
    }
    
    //...
}

當 Compose 頁面發生跳轉時,會基於目的地 Destination 創建對應的 NavBackStackEntry ,然後經過 pushWithTransition 壓入回退棧。backStack 是一個 StateFlow 類型,所以回退棧的變化可以被監聽。回看 NavHost{...} 函數的實現,我們會發現原來在這裡監聽瞭 backState 的變化,根據棧頂的變化,調用對應的 Composable 函數實現瞭頁面的切換。

//androidx/navigation/compose/ComposeNavigator.kt
@Composable
public fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier
) {
    //...

    // 為 NavController 設置 NavGraph
    navController.graph = graph

    //SaveableStateHolder 用於記錄 Composition 的局部狀態,後文介紹
    val saveableStateHolder = rememberSaveableStateHolder()

    //...

    // 最新的 visibleEntries 來自 backStack 的變化
    val visibleEntries = //...
    val backStackEntry = visibleEntries.lastOrNull()

    if (backStackEntry != null) {
        Crossfade(backStackEntry.id, modifier) {
            //...
            val lastEntry = backStackEntry
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
                //調用 Destination#content 顯示當前導航對應的頁面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
        }
    }

    //...
}

如上,NavHost 中除瞭為 NavController 設置 NavGraph,更重要的工作是監聽 backStack 的變化刷新頁面。

navigation-framgent 中的頁面切換在 FragmentNavigator 中命令式的完成的,而 navigation-compose 的頁面切換是在 NavHost 中用響應式的方式進行刷新,這也體現瞭聲明式 UI與命令式 UI 在實現思路上的不同。

visibleEntries 是基於 NavigatorState#backStack 得到的需要顯示的 Entry,它是一個 State,所以當其變化時 NavHost 會發生重組,Crossfade 會根據 visibleEntries 顯示對應的頁面。頁面顯示的具體實現也非常簡單,在 NavHost 中調用 BackStack 應的 Destination#content 即可,這個 content 就是我們在 NavHost{...} 中為每個頁面定義的 Composable 函數。

4. 保存狀態

前面我們瞭解瞭導航定義和導航跳轉的具體實現原理,接下來看一下導航過程中的狀態保存。 navigation-compose 的狀態保存主要發生在以下兩個場景中:

  • 點擊系統 back 鍵或者調用 NavController#popup 時,導航棧頂的 backStackEntry 彈出,導航返回前一頁面,此時我們希望前一頁面的狀態得到保持
  • 在配合底部導航欄使用時,點擊 nav bar 的 Item 可以在不同頁面間切換,此時我們希望切換回來的頁面保持之前的狀態

上述場景中,我們希望在頁面切換過程中,不會丟失例如滾動條位置等的頁面狀態,但是通過前面的代碼分析,我們也知道瞭 Compose 導航的頁面切換本質上就是在重組調用不同的 Composable。默認情況下,Composable 的狀態隨著其從 Composition 中的離開(即重組中不再被執行)而丟失。那麼 navigation-compose 是如何避免狀態丟失的呢?這裡的關鍵就是前面代碼中出現的 SaveableStateHolder 瞭。

SaveableStateHolder & rememberSaveable

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

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

    fun removeState(key: Any)
}

從名字上不難理解 SaveableStateHolder 維護著可保存的狀態(Saveable State),我們可以在它提供的 SaveableStateProvider 內部調用 Composable 函數,Composable 調用過程中使用 rememberSaveable 定義的狀態都會通過 key 進行保存,不會隨著 Composable 的生命周期的結束而丟棄,當下次 SaveableStateProvider 執行時,可以通過 key 恢復保存的狀態。我們通過一個實驗來瞭解一下 SaveableStateHolder 的作用:

@Composable
fun SaveableStateHolderDemo(flag: Boolean) {
    
    val saveableStateHolder = rememberSaveableStateHolder()

    Box {
        if (flag) {
             saveableStateHolder.SaveableStateProvider(true) {
                    Screen1()
            }
        } else {
            saveableStateHolder.SaveableStateProvider(false) {
                    Screen2()
        }
    }
}

上述代碼,我們可以通過傳入不同 flag 實現 Screen1 和 Screen2 之前的切換,saveableStateHolder.SaveableStateProvider 可以保證 Screen 內部狀態被保存。例如你在 Screen1 中使用 rememberScrollState() 定義瞭一個滾動條狀態,當 Screen1 再次顯示時滾動條仍然處於消失時的位置,因為 rememberScrollState 內部使用 rememberSaveable 保存瞭滾動條的位置。

remember, rememberSaveable 可以跨越 Composable 的生命周期更長久的保存狀態,在橫豎屏切換甚至進程重啟的場景中可以實現狀態恢復。

需要註意的是,如果我們在 SaveableStateProvider 之外使用 rememberSaveable ,雖然可以在橫豎屏切換時保存狀態,但是在導航場景中是無法保存狀態的。因為使用 rememberSaveable 定義的狀態隻有在配置變化時會被自動保存,但是在普通的 UI 結構變化時不會觸發保存,而 SaveableStateProvider 主要作用就是能夠在 onDispose 的時候實現狀態保存,

主要代碼如下:

//androidx/compose/runtime/saveable/SaveableStateHolder.kt

@Composable
fun SaveableStateProvider(key: Any, content: @Composable () -> Unit) {
    ReusableContent(key) {
        // 持有 SaveableStateRegistry
        val registryHolder = ...
        
        CompositionLocalProvider(
            LocalSaveableStateRegistry provides registryHolder.registry,
            content = content
        )
        
        DisposableEffect(Unit) {
            ...
            onDispose {
                //通過 SaveableStateRegistry 保存狀態
                registryHolder.saveTo(savedStates)
                ...
            }
        }
    }

rememberSaveable 中的通過 SaveableStateRegistry 進行保存,上面代碼中可以看到在 onDispose 生命周期中,通過 registryHolder#saveTo 將狀態保存到瞭 savedStates,savedStates 用於下次進入 Composition 時的狀態恢復。

順便提一下,這裡使用 ReusableContent{...} 可以基於 key 復用 LayoutNode,有利於 UI 更快速地重現。

導航回退時的狀態保存

簡單介紹瞭一下 SaveableStateHolder 的作用之後,我們看一下在 NavHost 中它是如何發揮作用的:

@Composable
public fun NavHost(
    ...
) {
    ...
    //SaveableStateHolder 用於記錄 Composition 的局部狀態,後文介紹
    val saveableStateHolder = rememberSaveableStateHolder()
    ...
        Crossfade(backStackEntry.id, modifier) {
            ...
            lastEntry.LocalOwnersProvider(saveableStateHolder) {
                //調用 Destination#content 顯示當前導航對應的頁面
                (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
            }
            
        }

    ...
}

lastEntry.LocalOwnersProvider(saveableStateHolder) 內部調用瞭 Destination#content, LocalOwnersProvider 內部其實就是對 SaveableStateProvider 的調用:

@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
    saveableStateHolder: SaveableStateHolder,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalViewModelStoreOwner provides this,
        LocalLifecycleOwner provides this,
        LocalSavedStateRegistryOwner provides this
    ) {
        // 調用 SaveableStateProvider
        saveableStateHolder.SaveableStateProvider(content)
    }
}

如上,在調用 SaveableStateProvider 之前,通過 CompositonLocal 註入瞭很多 Owner,這些 Owner 的實現都是 this,即指向當前的 NavBackStackEntry

  • LocalViewModelStoreOwner : 可以基於 BackStackEntry 的創建和管理 ViewModel
  • LocalLifecycleOwner:提供 LifecycleOwner,便於進行基於 Lifecycle 訂閱等操作
  • LocalSavedStateRegistryOwner:通過 SavedStateRegistry 註冊狀態保存的回調,例如 rememberSaveable 中的狀態保存其實通過 SavedStateRegistry 進行註冊,並在特定時間點被回調

可見,在基於導航的單頁面架構中,NavBackStackEntry 承載瞭類似 Fragment 一樣的責任,例如提供頁面級的 ViewModel 等等。

前面提到,SaveableStateProvider 需要通過 key 恢復狀態,那麼這個 key 是如何指定的呢。

LocalOwnersProvider 中調用的 SaveableStateProvider 沒有指定參數 key,原來它是對內部調用的包裝:

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

真正的 SaveableStateProvider 調用在這裡,而 key 是通過 ViewModel 管理的。因為 NavBackStackEntry 本身就是 ViewModelStoreOwner,新的 NavBackStackEntry 被壓棧時,下面的 NavBackStackEntry 以及其所轄的 ViewModel 依然存在。當 NavBackStackEntry 重新回到棧頂時,可以從 BackStackEntryIdViewModel 中獲取之前保存的 id,傳入 SaveableStateProvider。

BackStackEntryIdViewModel 的實現如下:

//androidx/navigation/compose/BackStackEntryIdViewModel.kt
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
    private val IdKey = "SaveableStateHolder_BackStackEntryKey"

    // 唯一 ID,可通過 SavedStateHandle 保存和恢復
    val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
    var saveableStateHolder: SaveableStateHolder? = null
    override fun onCleared() {
        super.onCleared()
        saveableStateHolder?.removeState(id)
    }
}

雖然從名字上看,BackStackEntryIdViewModel 主要是用來管理 BackStackEntryId 的,但其實它也是當前 BackStackEntry 的 saveableStateHolder 的持有者,ViewModel 在 SaveableStateProvider 中被傳入 saveableStateHolder,隻要 ViewModel 存在,UI 狀態就不會丟失。當前 NavBackStackEntry 出棧後,對應 ViewModel 發生 onCleared ,此時會通過 saveableStateHolder#removeState removeState 清空狀態,後續再次導航至此 Destination 時,不會遺留之前的狀態。

底部導航欄切換時的狀態保存

navigation-compose 常用來配合 BottomNavBar 實現多Tab頁的切換。如果我們直接使用 NavController#navigate 切換 Tab 頁,會造成 NavBackStack 的無限增長,所以我們需要在頁面切換後,從棧裡及時移除不需要顯示的頁面,例如下面這樣:

val navController = rememberNavController()

Scaffold(
  bottomBar = {
    BottomNavigation {
      ...
      items.forEach { screen ->
        BottomNavigationItem(
          ...
          onClick = {
            navController.navigate(screen.route) {
              // 避免 BackStack 增長,跳轉頁面時,將棧內 startDestination 之外的頁面彈出
              popUpTo(navController.graph.findStartDestination().id) {
                //出棧的 BackStack 保存狀態
                saveState = true
              }
              // 避免點擊同一個 Item 時反復入棧
              launchSingleTop = true
              
              // 如果之前出棧時保存狀態瞭,那麼重新入棧時恢復狀態
              restoreState = true
            }
          }
        )
      }
    }
  }
) { 
  NavHost(...) {
    ...
  }
}

上面代碼的關鍵是通過設置 saveState 和 restoreState,保證瞭 NavBackStack 出棧時,保存對應 Destination 的狀態,當 Destination 再次被壓棧時可以恢復。

狀態想要保存就意味著相關的 ViewModle 不能銷毀,而前面我們知道瞭 NavBackStack 是 ViewModelStoreOwner,如何在 NavBackStack 出棧後繼續保存 ViewModel 呢?其實 NavBackStack 所轄的 ViewModel 是存在 NavController 中管理的

從上面的類圖可以看清他們的關系, NavController 持有一個 NavControllerViewModel,它是 NavViewModelStoreProvider 的實現,通過 Map 管理著各 NavController 對應的 ViewModelStore。NavBackStackEntry 的 ViewModelStore 就取自 NavViewModelStoreProvider 。

當 NavBackStackEntry 出棧時,其對應的 Destination#content 移出畫面,執行 onDispose,

Crossfade(backStackEntry.id, modifier) {
    
    ... 
    DisposableEffect(Unit) {
        ...
        
        onDispose {
            visibleEntries.forEach { entry ->
                //顯示中的 Entry 移出屏幕,調用 onTransitionComplete
                composeNavigator.onTransitionComplete(entry)
            }
        }
    }
    lastEntry.LocalOwnersProvider(saveableStateHolder) {
        (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
    }
}

onTransitionComplete 中調用 NavigatorState#markTransitionComplete:

override fun markTransitionComplete(entry: NavBackStackEntry) {
    val savedState = entrySavedState[entry] == true
    ...
    if (!backQueue.contains(entry)) {
        ...
        if (backQueue.none { it.id == entry.id } && !savedState) {
            viewModel?.clear(entry.id)  //清空 ViewModel
        }
        ...
    } 
    
    ...
}

默認情況下, entrySavedState[entry] 為 false,這裡會執行 viewModel#clear 清空 entry 對應的 ViewModel,但是當我們在 popUpTo { … } 中設置 saveState 為 true 時,entrySavedState[entry] 就為 true,因此此處就不會執行 ViewModel#clear。

如果我們同時設置瞭 restoreState 為 true,當下次同類型 Destination 進入頁面時,k可以通過 ViewModle 恢復狀態。

//androidx/navigation/NavController.kt

private fun navigate(
    ...
) {

    ...
    //restoreState設置為true後,命中此處的 shouldRestoreState()
    if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
        navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
    } 
    ...
}

restoreStateInternal 中根據 DestinationId 找到之前對應的 BackStackId,進而通過 BackStackId 找回 ViewModel,恢復狀態。

5. 導航轉場動畫

navigation-fragment 允許我們可以像下面這樣,通過資源文件指定跳轉頁面時的專場動畫

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

由於 Compose 動畫不依靠資源文件,navigation-compose 不支持上面這樣的 anim { … } ,但相應地, navigation-compose 可以基於 Compose 動畫 API 實現導航動畫。

註意:navigation-compose 依賴的 Comopse 動畫 API 例如 AnimatedContent 等目前尚處於實驗狀態,因此導航動畫暫時隻能通過 accompanist-navigation-animation 引入,待動畫 API 穩定後,未來會移入 navigation-compose。

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

添加依賴後可以提前預覽 navigation-compose 導航動畫的 API 形式:

AnimatedNavHost(
    navController = navController,
    startDestination = AppScreen.main,
    enterTransition = {
        slideInHorizontally(
            initialOffsetX = { it },
            animationSpec = transSpec
        )
    },
    popExitTransition = {
        slideOutHorizontally(
            targetOffsetX = { it },
            animationSpec = transSpec
        )
    },
    exitTransition = {
        ...
    },
    popEnterTransition = {
        ...
    }

) {
    composable(
        AppScreen.splash,
        enterTransition = null,
        exitTransition = null
    ) {
        Splash()
    }
    composable(
        AppScreen.login,
        enterTransition = null,
        exitTransition = null
    ) {
        Login()
    }
    composable(
        AppScreen.register,
        enterTransition = null,
        exitTransition = null
    ) {
        Register()
    }
    ...
}

API 非常直觀,可以在 AnimatedNavHost 中統一指定 Transition 動畫,也可以在各個 composable 參數中分別指定。

回想一下,NavHost 中的 Destination#content 是在 Crossfade 中調用的,熟悉 Compose 動畫的就不難聯想到,可以在此處使用 AnimatedContent 為 content 的切換指定不同的動畫效果,navigatioin-compose 正是這樣做的:

//com/google/accompanist/navigation/animation/AnimatedNavHost.kt
@Composable
public fun AnimatedNavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) =
        { fadeIn(animationSpec = tween(700)) },
    exitTransition: ...,
    popEnterTransition: ...,
    popExitTransition: ...,
) {
    ...
    val backStackEntry = visibleTransitionsInProgress.lastOrNull() ?: visibleBackStack.lastOrNull()

    if (backStackEntry != null) {
        val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
            ...
        }

        val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = {
            ...
        }

        val transition = updateTransition(backStackEntry, label = "entry")
        
        transition.AnimatedContent(
            modifier,
            transitionSpec = { finalEnter(this) with finalExit(this) },
            contentAlignment,
            contentKey = { it.id }
        ) {
            ...
            currentEntry?.LocalOwnersProvider(saveableStateHolder) {
                (currentEntry.destination as AnimatedComposeNavigator.Destination)
                    .content(this, currentEntry)
            }
        }
        ...
    }
    ...
}

如上, AnimatedNavHost 與普通的 NavHost 的主要區別就是將 Crossfade 換成瞭 Transition#AnimatedContentfinalEnter 和 finalExit 是根據參數計算得到的 Compose Transition 動畫,通過 transitionSpec 進行指定。以 finalEnter 為例看一下具體實現

val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = {
    val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination

    if (composeNavigator.isPop.value) {
        //當前頁面即將出棧,執行pop動畫
        targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
            //popEnterTransitions 中存儲著通過 composable 參數指定的動畫
            popEnterTransitions[destination.route]?.invoke(this)
        } ?: popEnterTransition.invoke(this)
    } else {
        //當前頁面即將入棧,執行enter動畫
        targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
            enterTransitions[destination.route]?.invoke(this)
        } ?: enterTransition.invoke(this)
    }
}

如上,popEnterTransitions[destination.route] 是 composable(…) 參數中指定的動畫,所以 composable 參數指定的動畫優先級高於 AnimatedNavHost 。

6. Hilt & Navigation

由於每個 BackStackEntry 都是一個 ViewModelStoreOwner,我們可以獲取導航頁面級別的 ViewModel。使用 hilt-viewmodle-navigation 可以通過 Hilt 為 ViewModel 註入必要的依賴,降低 ViewModel 構造成本。

dependencies {
    implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
}

基於 hilt 獲取 ViewModel 的效果如下:

// import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun MyApp() {
    NavHost(navController, startDestination = startRoute) {
        composable("example") { backStackEntry ->
            // 通過 hiltViewModel() 獲取 MyViewModel,
            val viewModel = hiltViewModel<MyViewModel>()
            MyScreen(viewModel)
        }
        /* ... */
    }
}

我們隻需要為 MyViewModel 添加 @HiltViewModel 和 @Inject 註解,其參數依賴的 repository 可以通過 Hilt 自動註入,省去我們自定義 ViewModelFactory 的麻煩。

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

簡單看一下 hiltViewModel 的源碼

@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, factory = factory)
}

@Composable
@PublishedApi
internal fun createHiltViewModelFactory(
    viewModelStoreOwner: ViewModelStoreOwner
): ViewModelProvider.Factory? = if (viewModelStoreOwner is NavBackStackEntry) {
    HiltViewModelFactory(
        context = LocalContext.current,
        navBackStackEntry = viewModelStoreOwner
    )
} else {
    null
}

前面介紹過 LocalViewModelStoreOwner 就是當前的 BackStackEntry,拿到 viewModelStoreOwner 之後,通過 HiltViewModelFactory() 獲取 ViewModelFactory。 HiltViewModelFactory 是 hilt-navigation 的范圍,這裡就不深入研究瞭。

7. 總結

navigation-compose 的其他一些功能例如 Deeplinks,Arguments 等等,在實現上針對 Compose 沒有什麼特殊處理,這裡就不特別介紹瞭,有興趣可以翻閱 navigation-common 的源碼。通過本文的一系列介紹,我們可以看出 navigation-compose 無論在 API 的設計上還是在具體實現上,都遵循瞭聲明式的基本思想,當我們需要開發自己的 Compose 三方庫時,可以從中參考和借鑒。

到此這篇關於一文詳解 Compose Navigation 的實現原理的文章就介紹到這瞭,更多相關 Compose Navigation 實現內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: