日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

一文詳解?Compose?Navigation?的實現原理_Android

作者:fundroid??????? ? 更新時間: 2022-10-21 編程語言

前言

一個純 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 三方庫時,可以從中參考和借鑒。

原文鏈接:https://juejin.cn/post/7135253864411824165

欄目分類
最近更新