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 |
NavHost
The , Navigatot
, and so on in the above roles NavDestination
have 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#createGraph
through , 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.Destination
and NavGraphBuilder#addDestination
add it to the nodes of NavGraph.
Pass in two members when constructing Destination:
provider[ComposeNavigator::class]
: ComposeNavigator obtained through NavigatorProvidercontent
: 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#navigate
specifying a route
navController.navigate("friendslist")
As mentioned above, NavController finally implements specific jump logic through Navigator, such FragmentNavigator
as FragmentTransaction#replace
switching Fragment pages through , then let's take a look at the specific implementation ComposeNavigator#navigate
of :
//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. NavBackStackEntry
Represents a record in the back stack during the navigation process, entries
which is the back stack of the current page navigation. state is an NavigatorState
object , 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 NavigatorState
in
//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 pushWithTransition
pushed 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.
visibleEntries
It is based on NavigatorState#backStack
the obtained Entry that needs to be displayed. It is a State, so when it changes, NavHost will reorganize and Crossfade
display the corresponding page according to visibleEntries. The specific implementation of page display is also very simple, Destination#content
just , 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:
- 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
- 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 SaveableStateHolder
what 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 SaveableStateHolder
that it maintains a saveable state (Saveable State), we can SaveableStateProvider
call the Composable function inside it provided, and the state rememberSaveable
defined 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.SaveableStateProvider
to 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 onDispose
when
//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 SaveableStateRegistry
is saved. In the above code, you can see that in the onDispose life cycle, registryHolder#saveTo
the 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 AnimatedNavHost
in or separately in each composable parameter.
Recall that in NavHost Destination#content
is 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
. finalEnter
and finalExit
are Compose Transition animations calculated according to the parameters, transitionSpec
specified 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 MyViewModel
add @HiltViewModel
and @Inject
annotations , and its parameter-dependent repository
can 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, LocalViewModelStoreOwner
is 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.