In-depth understanding of Compose Navigation implementation principles

foreword

A pure Compose project cannot do without the support of page navigation, and navigation-compose is almost the only choice in this regard, which also makes it a standard second-party library for Compose projects. There are many articles on how to use navigation-compose , such as this one. In fact, Navigation is also very worth learning in terms of code design, so this article will take you to dig into its implementation principle

Start with Jetpack Navigation

Jetpack Navigatioin is a general page navigation framework, and navigation-compose is just a specific implementation of it for Compose. Regardless of the specific implementation, Navigation defines the following important roles in the core public layer:

Role illustrate
NavHost Define the entry of navigation, and it is also the container that hosts the navigation page
NavController The global manager of the navigation maintains the static and dynamic information of the navigation. The static information refers to NavGraph, and the dynamic information refers to the back stack NavBackStacks generated when the navigation is too long.
NavGraph When defining navigation, it is necessary to collect the navigation information of each node and uniformly register it in the navigation graph
NavDestination Each node in the navigation carries information such as route and arguments
Navigator The specific executor of navigation, NavController obtains the target node based on the navigation graph, and executes the jump through Navigator

NavHostThe , Navigatot, and so on in the above roles NavDestinationhave corresponding implementations in different scenarios. For example, in the traditional view, we use Activity or Fragment to host the page, taking navigation-fragment as an example:

  • Frament is a NavDestination in the navigation graph. We define NavGraph through DSL or XMlL, and collect Fragment information in the form of NavDestination in the navigation graph.
  • NavHostFragment serves as NavHost to provide a container for the display of Fragment pages
  • We implement the specific page jump logic through FragmentNavigator. In the implementation of FragmentNavigator#navigate, page replacement is realized based on FragmentTransaction#replace. Through the Fragment class information associated with NavDestination, the Fragment object is instantiated to complete the replacement.

Let's take a look at our protagonist navigation-compose today . Like navigation-fragment , Compose has its own specific implementation for Navigator and NavDestination. A little special is NavHost, which is just a Composable function, so it has no inheritance relationship with the public library:

Unlike object components like Fragment, Compose uses functions to define pages, so how does navigation-compose implement Navigation into a declarative framework like Compose? Next, we introduce the scene by scene.

define navigation

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

NavHost in Compose is essentially a Composable function, which has no derivation relationship with the interface of the same name in navigation-runtime , but the responsibilities are similar, and the main purpose is to build NavGraph. After NavGraph is created, it will be held by NavController and used in navigation, so NavHost accepts a NavController parameter and assigns NavGraph to it

//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
    //...
    
}

As above, assign NavGraph to NavController in NavHost and its function with the same name.

In the code, NavGraph is created navController#createGraphthrough , and the NavGraph object will be created internally based on NavGraphBuilder. During the build process, NavHost{...}the builder in the parameter is called to complete the initialization. This builder is an extension function of NavGraphBuilder. When we use to NavHost{...}define navigation, we will define the navigation page in Compose through a series of {…}. · It is also an extension function of NavGraphBuilder, and the only route in the navigation of the page is passed in through parameters.

//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(...)The specific implementation is as above, create a ComposeNavigator.Destinationand NavGraphBuilder#addDestinationadd it to the nodes of NavGraph.
Pass in two members when constructing Destination:

  • provider[ComposeNavigator::class]: ComposeNavigator obtained through NavigatorProvider
  • content: Composable function corresponding to the current page

Of course, information such as route, arguments, and deeplinks will also be passed in for the Destination.

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

It is very simple, that is, in addition to inheriting from NavDestination, an additional Compsoable content is stored. Destination displays the page corresponding to the current navigation node by calling this content. We will see how this content is called later.

navigation jump

Like Fragment navigation, Compose is also good for page jumps by NavController#navigatespecifying a route

navController.navigate("friendslist")

As mentioned above, NavController finally implements specific jump logic through Navigator, such FragmentNavigatoras FragmentTransaction#replaceswitching Fragment pages through , then let's take a look at the specific implementation ComposeNavigator#navigateof :

//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)
        }
    }
    
    //...

}

The processing here is very simple, there is no specific processing like FragmentNavigator. NavBackStackEntryRepresents a record in the back stack during the navigation process, entrieswhich is the back stack of the current page navigation. state is an NavigatorStateobject , which is a new type introduced after Navigation 2.4.0. It is used to encapsulate the state in the navigation process for use by NavController, etc. For example, backStack is stored NavigatorStatein

//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
        }
    }
    
    //...
}

When the Compose page jumps, the corresponding NavBackStackEntry will be created based on the destination Destination, and then pushWithTransitionpushed into the back stack through . backStack is a StateFlow type, so changes in the back stack can be monitored. Looking back at the implementation of NavHost{...}the function , we will find that the change of backState is monitored here, and according to the change of the top of the stack, the corresponding Composable function is called to realize the page switching.

//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)
            }
        }
    }

    //...
}

As above, in addition to setting NavGraph for NavController in NavHost, the more important work is to monitor the changes of backStack and refresh the page.

The page switching in navigation-framgent is completed imperatively in FragmentNavigator, while the page switching in navigation-compose is refreshed in a responsive manner in NavHost, which also reflects the implementation ideas of declarative UI and imperative UI s difference.

visibleEntriesIt is based on NavigatorState#backStackthe obtained Entry that needs to be displayed. It is a State, so when it changes, NavHost will reorganize and Crossfadedisplay the corresponding page according to visibleEntries. The specific implementation of page display is also very simple, Destination#contentjust , and this content is NavHost{...}the Composable function we defined for each page in .

save state

Earlier we learned about the specific implementation principles of navigation definition and navigation jump. Next, let's look at the state preservation during the navigation process.
The state preservation of navigation-compose mainly occurs in the following two scenarios:

  1. When the system back button is clicked or NavController#popup is called, the backStackEntry at the top of the navigation stack pops up, and the navigation returns to the previous page. At this time, we hope that the state of the previous page will be maintained
  2. When used with the bottom navigation bar, click the Item of the nav bar to switch between different pages. At this time, we hope that the switched back page will maintain the previous state

In the above scenario, we hope that the page state such as the position of the scroll bar will not be lost during the page switching process. However, through the previous code analysis, we also know that the page switching of Compose navigation is essentially reorganizing and calling different Composables. By default, a Composable's state is lost when it leaves a Composition (ie, the recomposition is no longer executed). So how does navigation-compose avoid state loss? The key here is SaveableStateHolderwhat appeared .

SaveableStateHolder & rememberSaveable

SaveableStateHolder comes from compose-runtime and is defined as follows:

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

    fun removeState(key: Any)
}

It is not difficult to understand from the name SaveableStateHolderthat it maintains a saveable state (Saveable State), we can SaveableStateProvidercall the Composable function inside it provided, and the state rememberSaveabledefined will be saved by the key, and will not follow the life cycle of Composable When the SaveableStateProvider is executed next time, the saved state can be restored through the key. Let's use an experiment to understand the role of SaveableStateHolder:

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

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

In the above code, we can switch between Screen1 and Screen2 by passing in different flags saveableStateHolder.SaveableStateProviderto ensure that the internal state of the Screen is saved. For example, if you use in Screen1 to rememberScrollState()define a scroll bar state, when Screen1 is displayed again, the scroll bar is still at the position when it disappears, because rememberScrollState internally uses rememberSaveable to save the position of the scroll bar.

If you don’t know about rememberSaveable, you can refer to https://developer.android.com/jetpack/compose/state#restore-ui-state. Compared with ordinary remember, rememberSaveable can save the state for a longer period of time across the life cycle of Composable. State restoration can be achieved in the case of handover or even process restart.

It should be noted that if we use rememberSaveable outside SaveableStateProvider, although the state can be saved when the horizontal and vertical screens are switched, the state cannot be saved in the navigation scene. Because the state defined using rememberSaveable will be automatically saved only when the configuration changes, but it will not trigger saving when the common UI structure changes, and the main function of SaveableStateProvider is to be able to realize state saving onDisposewhen

//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)
                ...
            }
        }
    }

The pass in rememberSaveable SaveableStateRegistryis saved. In the above code, you can see that in the onDispose life cycle, registryHolder#saveTothe state is saved to savedStates by , and savedStates is used for state recovery when entering Composition next time.

By the way, the LayoutNode ReusableContent{...}can reused based on the key, which is conducive to faster UI reproduction.

State preservation when navigating back

After briefly introducing the role of SaveableStateHolder, let's take a look at how it works in 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)Internally called Destination#content, LocalOwnersProvider is actually a call to 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)
    }
}

As above, before calling SaveableStateProvider, many Owners are injected through CompositonLocal, and the implementation of these Owners is this, which points to the current NavBackStackEntry

  • LocalViewModelStoreOwner : Can create and manage ViewModel based on BackStackEntry
  • LocalLifecycleOwner: Provide LifecycleOwner to facilitate operations such as Lifecycle-based subscriptions
  • LocalSavedStateRegistryOwner: Register the callback for state saving through SavedStateRegistry. For example, the state saving in rememberSaveable is actually registered through SavedStateRegistry and is called back at a specific time point

It can be seen that in the navigation-based single-page architecture, NavBackStackEntry carries the same responsibility as Fragment, such as providing page-level ViewModel and so on.

As mentioned earlier, SaveableStateProvider needs to restore the state through the key, so how is the key specified?

The SaveableStateProvider called in LocalOwnersProvider does not specify the parameter key, it turns out that it is a wrapper for the internal call:

@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
        }
    }
}

The real SaveableStateProvider is called here, and the key is managed through ViewModel. Because NavBackStackEntry itself is ViewModelStoreOwner, when a new NavBackStackEntry is pushed onto the stack, the following NavBackStackEntry and its ViewModel still exist. When NavBackStackEntry returns to the top of the stack, you can get the previously saved id from BackStackEntryIdViewModel and pass it into SaveableStateProvider.

The implementation of BackStackEntryIdViewModel is as follows:

//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)
    }
}

Although from the name, BackStackEntryIdViewModel is mainly used to manage BackStackEntryId, but in fact it is also the holder of the saveableStateHolder of the current BackStackEntry. ViewModel is passed into saveableStateHolder in SaveableStateProvider. As long as the ViewModel exists, the UI state will not be lost. After the current NavBackStackEntry is out of the stack, onCleared occurs on the corresponding ViewModel. At this time, the state will be cleared through saveableStateHolder#removeState removeState. When you navigate to this Destination again, the previous state will not be left.

State saving when the bottom navigation bar is switched

navigation-compose is often used to cooperate with BottomNavBar to realize multi-Tab page switching. If we directly use NavController#navigate to switch Tab pages, it will cause NavBackStack to grow infinitely, so we need to remove pages that do not need to be displayed from the stack in time after page switching, for example as follows:

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(...) {
    
    
    ...
  }
}

The key to the above code is to ensure that the state of the corresponding Destination is saved when the NavBackStack is popped from the stack by setting saveState and restoreState, and can be restored when the Destination is pushed to the stack again.

If the state wants to be saved, it means that the related ViewModel cannot be destroyed, and we know that the NavBackStack is the ViewModelStoreOwner, how to continue to save the ViewModel after the NavBackStack is popped out of the stack? In fact, the ViewModel under the jurisdiction of NavBackStack is managed in NavController

From the above class diagram, you can see their relationship clearly. NavController holds a NavControllerViewModel, which is the implementation of NavViewModelStoreProvider, and manages the ViewModelStore corresponding to each NavController through Map. The ViewModelStore of NavBackStackEntry is taken from NavViewModelStoreProvider.

When NavBackStackEntry pops out of the stack, its corresponding Destination#content moves out of the screen, executes 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)
    }
}

Call NavigatorState#markTransitionComplete in onTransitionComplete:

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
        }
        ...
    } 
    
    ...
}

By default, entrySavedState[entry] is false, and viewModel#clear will be executed here to clear the ViewModel corresponding to entry, but when we set saveState to true in popUpTo { ... }, entrySavedState[entry] will be true, so here ViewModel#clear will not be executed.

If we set restoreState to true at the same time, when the same type of Destination enters the page next time, k can restore the state through 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)
    } 
    ...
}

In restoreStateInternal, find the previous corresponding BackStackId according to the DestinationId, and then use the BackStackId to retrieve the ViewModel and restore the state.

Navigation transition animation

navigation-fragment allows us to specify the special animation when jumping to the page through the resource file as follows

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

Since Compose animation does not rely on resource files, navigation-compose does not support the above anim { … }, but accordingly, navigation-compose can realize navigation animation based on Compose animation API.

Note: The Comopse animation APIs that navigation-compose relies on, such as AnimatedContent, are still in an experimental state. Therefore, navigation animations can only be introduced through accompanying-navigation-animation for the time being. After the animation API is stabilized, it will be moved into navigation-compose in the future.

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

After adding dependencies, you can preview the API form of navigation-compose navigation animation in advance:

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()
    }
    ...
}

The API is very intuitive. Transition animations can be specified uniformly AnimatedNavHostin or separately in each composable parameter.

Recall that in NavHost Destination#contentis called in Crossfade. Those who are familiar with Compose animations can easily imagine that you can use AnimatedContent to specify different animation effects for content switching. This is exactly what navigatioin-compose does:

//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)
            }
        }
        ...
    }

    ...
}

As above, the main difference between AnimatedNavHost and ordinary NavHost is that Crossfade is replaced Transition#AnimatedContent. finalEnterand finalExitare Compose Transition animations calculated according to the parameters, transitionSpecspecified by . Take finalEnter as an example to see the specific implementation

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)
    }
}

As above, popEnterTransitions[destination.route]it is the animation specified in the composable(…) parameter, so the priority of the animation specified by the composable parameter is higher than that of AnimatedNavHost.

Hilt & Navigation

Since each BackStackEntry is a ViewModelStoreOwner, we can get the ViewModel at the navigation page level. Using hilt-viewmodle-navigation can inject necessary dependencies into ViewModel through Hilt, reducing the construction cost of ViewModel.

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

The effect of obtaining ViewModel based on hilt is as follows:

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

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

We only need to MyViewModeladd @HiltViewModeland @Injectannotations , and its parameter-dependent repositorycan be automatically injected through Hilt, saving us the trouble of customizing ViewModelFactory.

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

Take a brief look at the source code of 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
}

As mentioned earlier, LocalViewModelStoreOwneris the current BackStackEntry. After getting the viewModelStoreOwner, HiltViewModelFactory()get the ViewModelFactory through . HiltViewModelFactory is the scope of hilt-navigation , so I won't delve into it here.

at last

Some other functions of navigation-compose , such as Deeplinks, Arguments, etc., have no special treatment for Compose in implementation, so I won’t introduce them here. If you are interested, you can read the source code of navigation-common . Through a series of introductions in this article, we can see that navigation-compose follows the basic idea of ​​declarative in both API design and implementation. When we need to develop our own Compose tripartite library, we can refer to and learn from.

Guess you like

Origin blog.csdn.net/vitaviva/article/details/126635257