diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..3b0c254f --- /dev/null +++ b/404.html @@ -0,0 +1 @@ +
If you want to show animated transitions between destinations use AnimatedNavHost. The default transition is a simple crossfade, but you can granularly customize every transition with your own NavTransitionSpec
implementation.
Here is one possible implementation of NavTransitionSpec:
val CustomTransitionSpec = NavTransitionSpec<Any?> { action, _, _ ->
+ val direction = if (action == NavAction.Pop) {
+ AnimatedContentTransitionScope.SlideDirection.End
+ } else {
+ AnimatedContentTransitionScope.SlideDirection.Start
+ }
+ slideIntoContainer(direction) togetherWith slideOutOfContainer(direction)
+}
+
Set it into AnimatedNavHost:
AnimatedNavHost(
+ controller = navController,
+ transitionSpec = CustomTransitionSpec
+) { destination ->
+ // ...
+}
+
and it'll end up looking like this:
In NavTransitionSpec you get the parameters:
action
- a hint about the last NavController method that changed the backstackfrom
- a previous visible destinationto
- a target visible destinationThis information is plenty enough to choose a transition for every possible combination of destinations and navigation actions.
Tip
You can add more enter/exit animations to the composables through the AnimatedNavHostScope
receiver of the contentSelector
parameter. AnimatedNavHostScope gives you access to the current transition
and to animateEnterExit
modifier.
There are four default NavAction types:
Pop
, Replace
and Navigate
- objects that correspond to pop…
, replace…
, navigate
methods of NavControllerIdle
- the default action of a newly created NavController. You don't need to handle it in NavTransitionSpec.You can also create new action types by implementing NavAction
interface. Pass any object of the new type into setNewBackstack
method of NavController and handle it in NavTransitionSpec.
The last action can also be accessed through action
property of NavBackstack.
Back handling in the library is opt-in, rather than opt-out. By itself, neither NavController nor NavHost handles the back button press. You can add NavBackHandler
or usual BackHandler
in order to react to back presses where you need to.
NavBackHandler is the most basic implementation of BackHandler - it calls pop
only when there are more than one item in the backstack. When there is only one backstack item left, NavBackHandler is disabled, and any upper-level BackHandler may take its turn to react to back button presses.
If you want to specify your own backstack logic, use BackHandler directly. For example, this is how back navigation is handled for BottomNavigation in the sample:
@Composable
+private fun BottomNavigationBackHandler(
+ navController: NavController<BottomNavigationDestination>
+) {
+ BackHandler(enabled = navController.backstack.entries.size > 1) {
+ val lastEntry = navController.backstack.entries.last()
+ if (lastEntry.destination == BottomNavigationDestination.values()[0]) {
+ // The start destination should always be the last to pop. We move
+ // it to the start to preserve its saved state and view models.
+ navController.moveLastEntryToStart()
+ } else {
+ navController.pop()
+ }
+ }
+}
+
Bug
Always place your NavBackHandler/BackHandler before the corresponding NavHost.
As both BackHandler and NavHost use Lifecycle under the hood, there is a case when the order of back handling may be restored incorrectly after process/activity recreation. This is how the framework works and there is nothing to do about it. Simple placement of BackHandler before NavHost guarantees no issues in this part.
Similar to dialogs, you may use BottomSheetNavHost to handle a backstack of bottom sheets alongside the backstack of screens.
To use it, you need to add the dependency:
// if you are using Material
+implementation("dev.olshevski.navigation:reimagined-material:<latest-version>")
+
+// if you are using Material 3
+implementation("dev.olshevski.navigation:reimagined-material3:<latest-version>")
+
The usage would look like this:
@Composable
+fun NavHostScreen() {
+ val navController = rememberNavController<ScreenDestination>(
+ startDestination = ScreenDestination.First,
+ )
+
+ val sheetController = rememberNavController<SheetDestination>(
+ initialBackstack = emptyList()
+ )
+
+ NavBackHandler(navController)
+
+ NavHost(navController) { destination ->
+ when (destination) {
+ ScreenDestination.First -> { /* ... */ }
+ ScreenDestination.Second -> { /* ... */ }
+ }
+ }
+
+ BackHandler(enabled = sheetController.backstack.entries.isNotEmpty()) {
+ sheetController.pop()
+ }
+
+ BottomSheetNavHost(
+ controller = sheetController,
+ onDismissRequest = { sheetController.pop() }
+ ) { destination ->
+ Surface(
+ elevation = ModalBottomSheetDefaults.Elevation
+ ) {
+ when (destination) {
+ SheetDestination.First -> { /* ... */ }
+ SheetDestination.Second -> { /* ... */ }
+ }
+ }
+ }
+}
+
BottomSheetNavHost is based on the source code of ModalBottomSheetLayout, but with some improvements for switching between multiple bottom sheets. The API also has some similarities.
Tip
You can access current sheetState
through the BottomSheetNavHostScope
receiver of the contentSelector
parameter.
If you need to handle a backstack of dialogs in your application, simply add DialogNavHost to the same composition layer where your regular NavHost lives. This way you may show and control the backstack of regular screen destinations, as well as a second backstack of dialogs:
@Composable
+fun NavHostScreen() {
+ val navController = rememberNavController<ScreenDestination>(
+ startDestination = ScreenDestination.First,
+ )
+
+ val dialogController = rememberNavController<DialogDestination>(
+ initialBackstack = emptyList()
+ )
+
+ NavBackHandler(navController)
+
+ NavHost(navController) { destination ->
+ when (destination) {
+ ScreenDestination.First -> { /* ... */ }
+ ScreenDestination.Second -> { /* ... */ }
+ }
+ }
+
+ DialogNavHost(dialogController) { destination ->
+ Dialog(onDismissRequest = { dialogController.pop() }) {
+ when (destination) {
+ DialogDestination.First -> { /* ... */ }
+ DialogDestination.Second -> { /* ... */ }
+ }
+ }
+ }
+}
+
DialogNavHost is an alternative version of NavHost that is better suited for showing dialogs. It is based on AnimatedNavHost and provides smoother transition between dialogs without scrim flickering:
And this is how it looks in the regular NavHost:
Note
DialogNavHost doesn't wrap your composables into a Dialog. You need to use either Dialog or AlertDialog composable inside contentSelector
yourself.
A small and simple, yet fully fledged and customizable navigation library for Jetpack Compose:
Add a single dependency to your project:
implementation("dev.olshevski.navigation:reimagined:1.4.0")
+
Define a set of destinations. It is convenient to use a sealed class for this:
sealed class Destination : Parcelable {
+
+ @Parcelize
+ data object First : Destination()
+
+ @Parcelize
+ data class Second(val id: Int) : Destination()
+
+ @Parcelize
+ data class Third(val text: String) : Destination()
+
+}
+
Create a composable with NavController
, NavBackHandler
and NavHost
:
@Composable
+fun NavHostScreen() {
+ val navController = rememberNavController<Destination>(
+ startDestination = Destination.First
+ )
+
+ NavBackHandler(navController)
+
+ NavHost(navController) { destination ->
+ when (destination) {
+ is Destination.First -> Column {
+ Text("First destination")
+ Button(onClick = {
+ navController.navigate(Destination.Second(id = 42))
+ }) {
+ Text("Open Second destination")
+ }
+ }
+
+ is Destination.Second -> Column {
+ Text("Second destination: ${destination.id}")
+ Button(onClick = {
+ navController.navigate(Destination.Third(text = "Hello"))
+ }) {
+ Text("Open Third destination")
+ }
+ }
+
+ is Destination.Third -> {
+ Text("Third destination: ${destination.text}")
+ }
+ }
+ }
+}
+
As you can see, NavController
is used for switching between destinations, NavBackHandler
handles back presses and NavHost
provides a composable corresponding to the last destination in the backstack. As simple as that.
This is the main control point of navigation. It keeps record of all current backstack entries and preserves them on activity/process recreation.
NavController may be created with rememberNavController
method in a composable function or with navController
outside of composition. The latter may be used for storing NavController in a ViewModel. As it implements Parcelable interface, it could be stored in a SavedStateHandle.
Both rememberNavController
and navController
methods accept startDestination
as a parameter:
val navController = rememberNavController<Destination>(
+ startDestination = Destination.First
+)
+
If you want to create NavController with an arbitrary number of backstack items, you may use initialBackstack
parameter instead:
val navController = rememberNavController<Destination>(
+ initialBackstack = listOf(Destination.First, Destination.Second, Destination.Third)
+)
+
Destination.Third
will become the currently displayed item. Destination.First
and Destination.Second
will be stored in the backstack.
If you want to store NavController in a ViewModel use saveable
delegate for SavedStateHandle:
class NavigationViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
+
+ val navController by savedStateHandle.saveable<NavController<Destination>> {
+ navController(startDestination = Destination.First)
+ }
+
+}
+
NavController accepts all types that meet the requirements as destinations:
The type must be writable to Parcel - it could be Parcelable, Serializable, string/primitive, or other supported type.
The type must be Stable, Immutable, or string/primitive type.
Other than that, you are not limited to any particular type.
Tip
It is very convenient to define your set of destinations as a sealed class or enum. This way you will always be notified by the compiler that you have a non-exhaustive when
statement if you add a new destination.
Tip
You may also define your own base interface for destinations, for example:
interface Destination : Parcelable {
+
+ @Composable
+ fun Content()
+
+}
+
This way you may handle each destinations without checking its instance:
NavHost(navController) { it.Content() }
+
In order to be passed into NavController, each destination should be wrapped into NavEntry. It contains a unique identifier which is used to properly preserve saved state and manage Android architecture components (Lifecycle, ViewModelStore and SavedStateRegistry) for each such entry inside NavHost.
Saved state and view models of each entry are guaranteed to be preserved for as long as the associated entry is present in the backstack.
Note
If you add two equal destinations to the backstack, wrapped into two different entries, they will get their own separate identities, saved states and components. However, it is possible to put same exact entry instance into the backstack and it will be correctly treated as the same entry.
There is a handful of pre-defined methods suitable for basic app navigation: navigate
, moveToTop
, pop
, popUpTo
, popAll
, replaceLast
, replaceUpTo
, replaceAll
. They all are pretty much self-explanatory, except maybe moveToTop
.
moveToTop
method searches for some particular destination in the backstack and moves it to the top, effectively making it the currently displayed destination. This is particularly useful for integration with BottomNavigation/TabRow, when you want to always keep a single instance of every destination in the backstack.
The method is expected to be used in pair with navigate
:
if (!navController.moveToTop { it is SomeDestination }) {
+ // navigate to a new destination if there is no existing one
+ navController.navigate(SomeDestination())
+}
+
You may see how it is used for BottomNavigation in the sample.
moveToTop
, popUpTo
, replaceUpTo
methods require the predicate
parameter to be specified. It provides a selection condition for a destination to search for.
In case multiple destinations match the predicate, you may specify the match
parameter. Match.Last
is the default value and in this case the last matching item from the start of the backstack will be selected. Alternatively, you may use Match.First
.
If your use-case calls for some advanced backstack manipulations, you may use setNewBackstack
method. It is in fact the only public method defined in NavController, all other methods are provided as extensions and use setNewBackstack
under the hood. Here is how a new extension method moveLastEntryToStart
is implemented in the sample:
fun NavController<BottomNavigationDestination>.moveLastEntryToStart() {
+ setNewBackstack(
+ entries = backstack.entries.toMutableList().also {
+ val entry = it.removeLast()
+ it.add(0, entry)
+ },
+ action = NavAction.Pop
+ )
+}
+
You may access current backstack entries and the last NavAction through backstack
property of NavController. This property is backed up by MutableState and any changes to it will notify composition.
NavHost is a composable that shows the last entry of a backstack, manages saved state and provides all Android architecture components associated with the entry: Lifecycle, ViewModelStore and SavedStateRegistry. All these components are provided through CompositionLocalProvider within their corresponding owners: LocalLifecycleOwner
, LocalViewModelStoreOwner
and LocalSavedStateRegistryOwner
.
The components are kept around until its associated entry is removed from the backstack (or until the parent entry containing the current child NavHost is removed).
The default NavHost implementation by itself doesn't provide any animated transitions, it simply jump-cuts to the next destination:
Each NavEntry from the backstack is mapped to NavHostEntry within NavHost. NavHostEntry is what actually implements LifecycleOwner
, SavedStateRegistryOwner
and ViewModelStoreOwner
interfaces.
Usually, you don't need to interact with NavHostEntries directly, everything just works out of the box. But if you have a situation when you need to access all NavHostEntries from the current backstack, e.g. trying to access a ViewModel of neighbour entry, you could do it through the NavHostScope
receiver of the contentSelector
parameter.
NavHostState is a state holder of NavHost that stores and manages saved state and all Android architecture components for each entry. By default, it is automatically created by NavHost, but it is possible to create and set it into NavHost manually.
Note that you most probably don't need to use the state holder directly unless you are conditionally adding/removing NavHost to/from composition:
val state = rememberNavHostState(backstack)
+if (visible) {
+ NavHost(state) {
+ // ...
+ }
+}
+
In this example, the state of NavHost will be properly preserved, as it is placed outside of condition.
If you do want to clear the state when NavHost is removed by condition, use NavHostVisibility
/NavHostAnimatedVisibility
. These composables properly clear the internal state of NavHost when the visible
parameter is set to false
:
NavHostVisibility(visible) {
+ NavHost(backstack) {
+ // ...
+ }
+}
+
You can explore the sample of NavHostVisibility usage here.
Nested navigation is actually quite simple. You just need to place NavHost (let's call it a child) into any entry of the other NavHost (a parent). You may want to add decoration around your child NavHost or leave it within the same viewport of the parent NavHost.
There may be different reasons for nesting your NavHosts:
It may be useful when you need to have several backstacks at once, as in case of BottomNavigation, TabRow, or similar, where each item has it's own inner independent layer of navigation.
You want to contain some particular flow of destinations within a single composable function. This flow may also contain some shared static layout elements.
You want to share a ViewModel between several destinations that logically and visually may be grouped into a single flow.
Note
There is no depth limit for nesting NavHosts. In fact, each NavHost is completely oblivious to its placement in the hierarchy.
As destination types are not strictly required to be Immutable, you may change them while they are in the backstack. This may be used for returning values from other destinations. Just make a mutable property backed up by mutableStateOf
and change it when required.
For example, we want to return a string from the Second screen. Here is how destinations may be defined:
interface AcceptsResultFromSecond {
+ val resultFromSecond: MutableState<String?>
+}
+
+@Stable
+sealed class Destination : Parcelable {
+
+ @Parcelize
+ data class First(
+ override val resultFromSecond: @RawValue MutableState<String?> = mutableStateOf(null)
+ ) : Destination(), AcceptsResultFromSecond
+
+ @Parcelize
+ data object Second : Destination()
+
+}
+
And to actually set the result from the Second screen you do:
val previousDestination = navController.backstack.entries.let {
+ it[it.lastIndex - 1].destination
+}
+check(previousDestination is AcceptsResultFromSecond)
+previousDestination.resultFromSecond.value = text
+navController.pop()
+
You may see how it is implemented in the sample here.
Warning
In general, returning values to previous destinations makes the navigation logic more complicated. Also, this approach doesn't guarantee full compile time type-safety. Use it with caution and when you are sure what you are doing. Sometimes it may be easier to use a shared state holder.
A small and simple, yet fully fledged and customizable navigation library for Jetpack Compose:
Add a single dependency to your project:
implementation(\"dev.olshevski.navigation:reimagined:1.4.0\")\n
Define a set of destinations. It is convenient to use a sealed class for this:
sealed class Destination : Parcelable {\n\n@Parcelize\ndata object First : Destination()\n\n@Parcelize\ndata class Second(val id: Int) : Destination()\n\n@Parcelize\ndata class Third(val text: String) : Destination()\n\n}\n
Create a composable with NavController
, NavBackHandler
and NavHost
:
@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<Destination>(\nstartDestination = Destination.First\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nis Destination.First -> Column {\nText(\"First destination\")\nButton(onClick = {\nnavController.navigate(Destination.Second(id = 42))\n}) {\nText(\"Open Second destination\")\n}\n}\n\nis Destination.Second -> Column {\nText(\"Second destination: ${destination.id}\")\nButton(onClick = {\nnavController.navigate(Destination.Third(text = \"Hello\"))\n}) {\nText(\"Open Third destination\")\n}\n}\n\nis Destination.Third -> {\nText(\"Third destination: ${destination.text}\")\n}\n}\n}\n}\n
As you can see, NavController
is used for switching between destinations, NavBackHandler
handles back presses and NavHost
provides a composable corresponding to the last destination in the backstack. As simple as that.
If you want to show animated transitions between destinations use AnimatedNavHost. The default transition is a simple crossfade, but you can granularly customize every transition with your own NavTransitionSpec
implementation.
Here is one possible implementation of NavTransitionSpec:
val CustomTransitionSpec = NavTransitionSpec<Any?> { action, _, _ ->\nval direction = if (action == NavAction.Pop) {\nAnimatedContentTransitionScope.SlideDirection.End\n} else {\nAnimatedContentTransitionScope.SlideDirection.Start\n}\nslideIntoContainer(direction) togetherWith slideOutOfContainer(direction)\n}\n
Set it into AnimatedNavHost:
AnimatedNavHost(\ncontroller = navController,\ntransitionSpec = CustomTransitionSpec\n) { destination ->\n// ...\n}\n
and it'll end up looking like this:
In NavTransitionSpec you get the parameters:
action
- a hint about the last NavController method that changed the backstackfrom
- a previous visible destinationto
- a target visible destinationThis information is plenty enough to choose a transition for every possible combination of destinations and navigation actions.
Tip
You can add more enter/exit animations to the composables through the AnimatedNavHostScope
receiver of the contentSelector
parameter. AnimatedNavHostScope gives you access to the current transition
and to animateEnterExit
modifier.
There are four default NavAction types:
Pop
, Replace
and Navigate
- objects that correspond to pop\u2026
, replace\u2026
, navigate
methods of NavControllerIdle
- the default action of a newly created NavController. You don't need to handle it in NavTransitionSpec.You can also create new action types by implementing NavAction
interface. Pass any object of the new type into setNewBackstack
method of NavController and handle it in NavTransitionSpec.
The last action can also be accessed through action
property of NavBackstack.
Back handling in the library is opt-in, rather than opt-out. By itself, neither NavController nor NavHost handles the back button press. You can add NavBackHandler
or usual BackHandler
in order to react to back presses where you need to.
NavBackHandler is the most basic implementation of BackHandler - it calls pop
only when there are more than one item in the backstack. When there is only one backstack item left, NavBackHandler is disabled, and any upper-level BackHandler may take its turn to react to back button presses.
If you want to specify your own backstack logic, use BackHandler directly. For example, this is how back navigation is handled for BottomNavigation in the sample:
@Composable\nprivate fun BottomNavigationBackHandler(\nnavController: NavController<BottomNavigationDestination>\n) {\nBackHandler(enabled = navController.backstack.entries.size > 1) {\nval lastEntry = navController.backstack.entries.last()\nif (lastEntry.destination == BottomNavigationDestination.values()[0]) {\n// The start destination should always be the last to pop. We move\n// it to the start to preserve its saved state and view models.\nnavController.moveLastEntryToStart()\n} else {\nnavController.pop()\n}\n}\n}\n
Bug
Always place your NavBackHandler/BackHandler before the corresponding NavHost.
As both BackHandler and NavHost use Lifecycle under the hood, there is a case when the order of back handling may be restored incorrectly after process/activity recreation. This is how the framework works and there is nothing to do about it. Simple placement of BackHandler before NavHost guarantees no issues in this part.
"},{"location":"bottom-sheets/","title":"Bottom sheets","text":"Similar to dialogs, you may use BottomSheetNavHost to handle a backstack of bottom sheets alongside the backstack of screens.
To use it, you need to add the dependency:
// if you are using Material\nimplementation(\"dev.olshevski.navigation:reimagined-material:<latest-version>\")\n\n// if you are using Material 3\nimplementation(\"dev.olshevski.navigation:reimagined-material3:<latest-version>\")\n
The usage would look like this:
@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<ScreenDestination>(\nstartDestination = ScreenDestination.First,\n)\n\nval sheetController = rememberNavController<SheetDestination>(\ninitialBackstack = emptyList()\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nScreenDestination.First -> { /* ... */ }\nScreenDestination.Second -> { /* ... */ }\n}\n}\n\nBackHandler(enabled = sheetController.backstack.entries.isNotEmpty()) {\nsheetController.pop()\n}\n\nBottomSheetNavHost(\ncontroller = sheetController,\nonDismissRequest = { sheetController.pop() }\n) { destination ->\nSurface(\nelevation = ModalBottomSheetDefaults.Elevation\n) {\nwhen (destination) {\nSheetDestination.First -> { /* ... */ }\nSheetDestination.Second -> { /* ... */ }\n}\n}\n}\n}\n
BottomSheetNavHost is based on the source code of ModalBottomSheetLayout, but with some improvements for switching between multiple bottom sheets. The API also has some similarities.
Tip
You can access current sheetState
through the BottomSheetNavHostScope
receiver of the contentSelector
parameter.
If you need to handle a backstack of dialogs in your application, simply add DialogNavHost to the same composition layer where your regular NavHost lives. This way you may show and control the backstack of regular screen destinations, as well as a second backstack of dialogs:
@Composable\nfun NavHostScreen() {\nval navController = rememberNavController<ScreenDestination>(\nstartDestination = ScreenDestination.First,\n)\n\nval dialogController = rememberNavController<DialogDestination>(\ninitialBackstack = emptyList()\n)\n\nNavBackHandler(navController)\n\nNavHost(navController) { destination ->\nwhen (destination) {\nScreenDestination.First -> { /* ... */ }\nScreenDestination.Second -> { /* ... */ }\n}\n}\n\nDialogNavHost(dialogController) { destination ->\nDialog(onDismissRequest = { dialogController.pop() }) {\nwhen (destination) {\nDialogDestination.First -> { /* ... */ }\nDialogDestination.Second -> { /* ... */ }\n}\n}\n}\n}\n
DialogNavHost is an alternative version of NavHost that is better suited for showing dialogs. It is based on AnimatedNavHost and provides smoother transition between dialogs without scrim flickering:
And this is how it looks in the regular NavHost:
Note
DialogNavHost doesn't wrap your composables into a Dialog. You need to use either Dialog or AlertDialog composable inside contentSelector
yourself.
This is the main control point of navigation. It keeps record of all current backstack entries and preserves them on activity/process recreation.
NavController may be created with rememberNavController
method in a composable function or with navController
outside of composition. The latter may be used for storing NavController in a ViewModel. As it implements Parcelable interface, it could be stored in a SavedStateHandle.
Both rememberNavController
and navController
methods accept startDestination
as a parameter:
val navController = rememberNavController<Destination>(\nstartDestination = Destination.First\n)\n
If you want to create NavController with an arbitrary number of backstack items, you may use initialBackstack
parameter instead:
val navController = rememberNavController<Destination>(\ninitialBackstack = listOf(Destination.First, Destination.Second, Destination.Third)\n)\n
Destination.Third
will become the currently displayed item. Destination.First
and Destination.Second
will be stored in the backstack.
If you want to store NavController in a ViewModel use saveable
delegate for SavedStateHandle:
class NavigationViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {\n\nval navController by savedStateHandle.saveable<NavController<Destination>> {\nnavController(startDestination = Destination.First)\n}\n\n}\n
"},{"location":"nav-controller/#destinations","title":"Destinations","text":"NavController accepts all types that meet the requirements as destinations:
The type must be writable to Parcel - it could be Parcelable, Serializable, string/primitive, or other supported type.
The type must be Stable, Immutable, or string/primitive type.
Other than that, you are not limited to any particular type.
Tip
It is very convenient to define your set of destinations as a sealed class or enum. This way you will always be notified by the compiler that you have a non-exhaustive when
statement if you add a new destination.
Tip
You may also define your own base interface for destinations, for example:
interface Destination : Parcelable {\n\n@Composable\nfun Content()\n\n}\n
This way you may handle each destinations without checking its instance:
NavHost(navController) { it.Content() }\n
"},{"location":"nav-controller/#naventry","title":"NavEntry","text":"In order to be passed into NavController, each destination should be wrapped into NavEntry. It contains a unique identifier which is used to properly preserve saved state and manage Android architecture components (Lifecycle, ViewModelStore and SavedStateRegistry) for each such entry inside NavHost.
Saved state and view models of each entry are guaranteed to be preserved for as long as the associated entry is present in the backstack.
Note
If you add two equal destinations to the backstack, wrapped into two different entries, they will get their own separate identities, saved states and components. However, it is possible to put same exact entry instance into the backstack and it will be correctly treated as the same entry.
"},{"location":"nav-controller/#navigation-methods","title":"Navigation methods","text":"There is a handful of pre-defined methods suitable for basic app navigation: navigate
, moveToTop
, pop
, popUpTo
, popAll
, replaceLast
, replaceUpTo
, replaceAll
. They all are pretty much self-explanatory, except maybe moveToTop
.
moveToTop
method searches for some particular destination in the backstack and moves it to the top, effectively making it the currently displayed destination. This is particularly useful for integration with BottomNavigation/TabRow, when you want to always keep a single instance of every destination in the backstack.
The method is expected to be used in pair with navigate
:
if (!navController.moveToTop { it is SomeDestination }) {\n// navigate to a new destination if there is no existing one\nnavController.navigate(SomeDestination())\n}\n
You may see how it is used for BottomNavigation in the sample.
"},{"location":"nav-controller/#methods-with-a-search-predicate","title":"Methods with a search predicate","text":"moveToTop
, popUpTo
, replaceUpTo
methods require the predicate
parameter to be specified. It provides a selection condition for a destination to search for.
In case multiple destinations match the predicate, you may specify the match
parameter. Match.Last
is the default value and in this case the last matching item from the start of the backstack will be selected. Alternatively, you may use Match.First
.
If your use-case calls for some advanced backstack manipulations, you may use setNewBackstack
method. It is in fact the only public method defined in NavController, all other methods are provided as extensions and use setNewBackstack
under the hood. Here is how a new extension method moveLastEntryToStart
is implemented in the sample:
fun NavController<BottomNavigationDestination>.moveLastEntryToStart() {\nsetNewBackstack(\nentries = backstack.entries.toMutableList().also {\nval entry = it.removeLast()\nit.add(0, entry)\n},\naction = NavAction.Pop\n)\n}\n
"},{"location":"nav-controller/#navbackstack","title":"NavBackstack","text":"You may access current backstack entries and the last NavAction through backstack
property of NavController. This property is backed up by MutableState and any changes to it will notify composition.
NavHost is a composable that shows the last entry of a backstack, manages saved state and provides all Android architecture components associated with the entry: Lifecycle, ViewModelStore and SavedStateRegistry. All these components are provided through CompositionLocalProvider within their corresponding owners: LocalLifecycleOwner
, LocalViewModelStoreOwner
and LocalSavedStateRegistryOwner
.
The components are kept around until its associated entry is removed from the backstack (or until the parent entry containing the current child NavHost is removed).
The default NavHost implementation by itself doesn't provide any animated transitions, it simply jump-cuts to the next destination:
"},{"location":"nav-host/#navhostentry-and-navhostscope","title":"NavHostEntry and NavHostScope","text":"
Each NavEntry from the backstack is mapped to NavHostEntry within NavHost. NavHostEntry is what actually implements LifecycleOwner
, SavedStateRegistryOwner
and ViewModelStoreOwner
interfaces.
Usually, you don't need to interact with NavHostEntries directly, everything just works out of the box. But if you have a situation when you need to access all NavHostEntries from the current backstack, e.g. trying to access a ViewModel of neighbour entry, you could do it through the NavHostScope
receiver of the contentSelector
parameter.
NavHostState is a state holder of NavHost that stores and manages saved state and all Android architecture components for each entry. By default, it is automatically created by NavHost, but it is possible to create and set it into NavHost manually.
Note that you most probably don't need to use the state holder directly unless you are conditionally adding/removing NavHost to/from composition:
val state = rememberNavHostState(backstack)\nif (visible) {\nNavHost(state) {\n// ...\n}\n}\n
In this example, the state of NavHost will be properly preserved, as it is placed outside of condition.
If you do want to clear the state when NavHost is removed by condition, use NavHostVisibility
/NavHostAnimatedVisibility
. These composables properly clear the internal state of NavHost when the visible
parameter is set to false
:
NavHostVisibility(visible) {\nNavHost(backstack) {\n// ...\n}\n}\n
You can explore the sample of NavHostVisibility usage here.
"},{"location":"nested-navigation/","title":"Nested navigation","text":"Nested navigation is actually quite simple. You just need to place NavHost (let's call it a child) into any entry of the other NavHost (a parent). You may want to add decoration around your child NavHost or leave it within the same viewport of the parent NavHost.
There may be different reasons for nesting your NavHosts:
It may be useful when you need to have several backstacks at once, as in case of BottomNavigation, TabRow, or similar, where each item has it's own inner independent layer of navigation.
You want to contain some particular flow of destinations within a single composable function. This flow may also contain some shared static layout elements.
You want to share a ViewModel between several destinations that logically and visually may be grouped into a single flow.
Note
There is no depth limit for nesting NavHosts. In fact, each NavHost is completely oblivious to its placement in the hierarchy.
"},{"location":"return-results/","title":"Return results","text":"As destination types are not strictly required to be Immutable, you may change them while they are in the backstack. This may be used for returning values from other destinations. Just make a mutable property backed up by mutableStateOf
and change it when required.
For example, we want to return a string from the Second screen. Here is how destinations may be defined:
interface AcceptsResultFromSecond {\nval resultFromSecond: MutableState<String?>\n}\n\n@Stable\nsealed class Destination : Parcelable {\n\n@Parcelize\ndata class First(\noverride val resultFromSecond: @RawValue MutableState<String?> = mutableStateOf(null)\n) : Destination(), AcceptsResultFromSecond\n\n@Parcelize\ndata object Second : Destination()\n\n}\n
And to actually set the result from the Second screen you do:
val previousDestination = navController.backstack.entries.let {\nit[it.lastIndex - 1].destination\n}\ncheck(previousDestination is AcceptsResultFromSecond)\npreviousDestination.resultFromSecond.value = text\nnavController.pop()\n
You may see how it is implemented in the sample here.
Warning
In general, returning values to previous destinations makes the navigation logic more complicated. Also, this approach doesn't guarantee full compile time type-safety. Use it with caution and when you are sure what you are doing. Sometimes it may be easier to use a shared state holder.
"},{"location":"shared-view-models/","title":"Shared ViewModels","text":"Sometimes you need to access the same ViewModel instance from several destinations. The library provides multiple ways to achieve this.
"},{"location":"shared-view-models/#nested-navigation","title":"Nested navigation","text":"The easiest way to share a ViewModel between several destinations is to use a nested NavHost. Simply collect all required destinations into a separate nested NavHost, and pass a ViewModel of the parent entry to each destination.
However, it may not work for all scenarios. Sometimes it is not desirable or possible to group destinations into a single nested NavHost. For such cases, it would be more convenient to use scoping NavHosts.
"},{"location":"shared-view-models/#scoping-navhosts","title":"Scoping NavHosts","text":"Each NavHost in the library has its own Scoping
counterpart. For NavHost
it is ScopingNavHost
, for AnimatedNavHost
it is ScopingAnimatedNavHost
, and so on.
Every scoping NavHost gives you the ability to assign scopes to destinations and access scoped ViewModelStoreOwners bound to each of the defined scope. Such scoped ViewModelStoreOwner is created when there is at least one backstack entry marked with the corresponding scope, and removed when there are none of the entries marked with it.
For example, the ViewModelStoreOwner for Scope X will exist only when at least one of the destinations B or C is in the backstack (their positions don't matter). Both B and C can access the same ViewModelStoreOwner instance. A cannot access it as it is not marked with Scope X.
When both B and C are popped off the backstack and there is only A left, the ViewModelStoreOwner for Scope X will be cleared and removed.
Note that if you replace B and C with a new destination D that is also marked with Scope X, the ViewModelStoreOwner will not be recreated, but left as is.
In order to use scoping NavHost, you need to implement NavScopeSpec
and pass it as the scopeSpec
parameter. NavScopeSpec
requests a set of scopes for each destination in the backstack:
@Parcelize\ndata object ScopeX : Parcelable\n\nval DestinationScopeSpec = NavScopeSpec<Destination, ScopeX> { destination -> when (destination) {\nDestination.B, Destination.C, Destination.D -> setOf(ScopeX)\nelse -> emptySet()\n}\n}\n
Note that a destination may belong to several scopes at once, that's why NavScopeSpec requires you to return a Set
.
Scoped ViewModelStoreOwner is implemented by ScopedNavHostEntry
class. You can acquire all scoped entries associated with the current destination through the ScopingNavHostScope
receiver of the contentSelector
parameter:
ScopingNavHost(\ncontroller = navController,\nscopeSpec = DestinationScopeSpec\n) { destination ->\nwhen (destination) {\nDestination.A -> { /* ... */ }\nDestination.B -> {\nval sharedViewModel = viewModel<SharedViewModel>(\nviewModelStoreOwner = scopedHostEntries[ScopeX]!!\n)\n}\nDestination.C -> { /* same code as for B */ }\nDestination.D -> { /* same code as for B */ }\n}\n}\n
You just have to pass this ScopedNavHostEntry as the viewModelStoreOwner
parameter of the viewModel
method.
Alternatively, you can access scoped ViewModelStoreOwners through the LocalScopedViewModelStoreOwners
composition local.
If the two previous solutions are not suitable for your case, you may always access ViewModels of neighbour entries directly. Read more about it here.
"},{"location":"view-models/","title":"Entry-scoped ViewModels","text":"Every unique NavEntry in NavHost provides its own ViewModelStore. Every such ViewModelStore is guaranteed to exist for as long as the associated NavEntry is present in the backstack.
As soon as NavEntry is removed from the backstack, its ViewModelStore with all ViewModels is cleared.
You can get ViewModels as you do it usually, by using composable viewModel
from androidx.lifecycle:lifecycle-viewmodel-compose
artifact, for example:
@Composable\nfun SomeScreen() {\nval someViewModel = viewModel<SomeViewModel>()\n// ...\n}\n
"},{"location":"view-models/#accessing-viewmodels-of-backstack-entries","title":"Accessing ViewModels of backstack entries","text":"It is possible to access ViewModelStoreOwner of any entry that is currently present on the backstack. It is done through the the NavHostScope
receiver of the contentSelector
parameter of NavHost:
@Composable\nfun NavHostScope<Destination>.SomeScreen() {\nval previousViewModel = viewModel<PreviousViewModel>(\nviewModelStoreOwner = hostEntries.find {\nit.destination is Destination.Previous\n}!!\n)\n// ...\n}\n
"},{"location":"view-models/#passing-parameters-into-a-viewmodel","title":"Passing parameters into a ViewModel","text":"There is no such thing as a Bundle of arguments for navigation entries in this library. This means that there is literally nothing to pass into SavedStateHandle
of your ViewModel as the default arguments.
I personally recommend passing all parameters into a ViewModel constructor directly. This keeps everything clean and type-safe.
If you use dependency injections in your project, explore the samples that show how to pass parameters into a ViewModel and inject all other dependencies:
Dagger/Anvil/Hilt use @AssistedInject
Koin supports ViewModel parameters out of the box and does it charmingly simple
If Hilt is the DI library of your choice and you want to use hiltViewModel()
method that you may already be familiar with from the official Navigation Component, you can add the dependency:
implementation(\"dev.olshevski.navigation:reimagined-hilt:<latest-version>\")\n
It provides a similar hiltViewModel()
method that works with the Reimagined library. The only catch is that by default it doesn't know how to pass arguments to the SavedStateHandle of your ViewModel. For this you can use an additional defaultArguments
parameter:
val viewModel = hiltViewModel<SomeViewModel>(\ndefaultArguments = bundleOf(\"id\" to id)\n)\n
And in ViewModel you can read this argument as such:
@HiltViewModel\nclass SomeViewModel @Inject constructor(\nsavedStateHandle: SavedStateHandle\n) : LoggingViewModel() {\n\nprivate val id: Int = savedStateHandle[\"id\"]!!\n\n}\n
Tip
Don't forget to annotate your view models with @HiltViewModel
annotation.
Warning
Do not pass mutable data structures as defaultArguments
and expect the external changes to be reflected through the SavedStateHandle inside a ViewModel, e.g. when trying to return results as described here.
As soon as SavedStateHandle parcelize/unparcelize data once, it becomes the only source of truth for the data it holds.
If you still need to pass mutable data structure into your ViewModel, it would be more reliable to pass it directly as a constructor parameter).
"}]} \ No newline at end of file diff --git a/shared-view-models/index.html b/shared-view-models/index.html new file mode 100644 index 00000000..facac44d --- /dev/null +++ b/shared-view-models/index.html @@ -0,0 +1,25 @@ +Sometimes you need to access the same ViewModel instance from several destinations. The library provides multiple ways to achieve this.
The easiest way to share a ViewModel between several destinations is to use a nested NavHost. Simply collect all required destinations into a separate nested NavHost, and pass a ViewModel of the parent entry to each destination.
However, it may not work for all scenarios. Sometimes it is not desirable or possible to group destinations into a single nested NavHost. For such cases, it would be more convenient to use scoping NavHosts.
Each NavHost in the library has its own Scoping
counterpart. For NavHost
it is ScopingNavHost
, for AnimatedNavHost
it is ScopingAnimatedNavHost
, and so on.
Every scoping NavHost gives you the ability to assign scopes to destinations and access scoped ViewModelStoreOwners bound to each of the defined scope. Such scoped ViewModelStoreOwner is created when there is at least one backstack entry marked with the corresponding scope, and removed when there are none of the entries marked with it.
For example, the ViewModelStoreOwner for Scope X will exist only when at least one of the destinations B or C is in the backstack (their positions don't matter). Both B and C can access the same ViewModelStoreOwner instance. A cannot access it as it is not marked with Scope X.
When both B and C are popped off the backstack and there is only A left, the ViewModelStoreOwner for Scope X will be cleared and removed.
Note that if you replace B and C with a new destination D that is also marked with Scope X, the ViewModelStoreOwner will not be recreated, but left as is.
In order to use scoping NavHost, you need to implement NavScopeSpec
and pass it as the scopeSpec
parameter. NavScopeSpec
requests a set of scopes for each destination in the backstack:
@Parcelize
+data object ScopeX : Parcelable
+
+val DestinationScopeSpec = NavScopeSpec<Destination, ScopeX> { destination ->
+ when (destination) {
+ Destination.B, Destination.C, Destination.D -> setOf(ScopeX)
+ else -> emptySet()
+ }
+}
+
Note that a destination may belong to several scopes at once, that's why NavScopeSpec requires you to return a Set
.
Scoped ViewModelStoreOwner is implemented by ScopedNavHostEntry
class. You can acquire all scoped entries associated with the current destination through the ScopingNavHostScope
receiver of the contentSelector
parameter:
ScopingNavHost(
+ controller = navController,
+ scopeSpec = DestinationScopeSpec
+) { destination ->
+ when (destination) {
+ Destination.A -> { /* ... */ }
+ Destination.B -> {
+ val sharedViewModel = viewModel<SharedViewModel>(
+ viewModelStoreOwner = scopedHostEntries[ScopeX]!!
+ )
+ }
+ Destination.C -> { /* same code as for B */ }
+ Destination.D -> { /* same code as for B */ }
+ }
+}
+
You just have to pass this ScopedNavHostEntry as the viewModelStoreOwner
parameter of the viewModel
method.
Alternatively, you can access scoped ViewModelStoreOwners through the LocalScopedViewModelStoreOwners
composition local.
If the two previous solutions are not suitable for your case, you may always access ViewModels of neighbour entries directly. Read more about it here.
Every unique NavEntry in NavHost provides its own ViewModelStore. Every such ViewModelStore is guaranteed to exist for as long as the associated NavEntry is present in the backstack.
As soon as NavEntry is removed from the backstack, its ViewModelStore with all ViewModels is cleared.
You can get ViewModels as you do it usually, by using composable viewModel
from androidx.lifecycle:lifecycle-viewmodel-compose
artifact, for example:
@Composable
+fun SomeScreen() {
+ val someViewModel = viewModel<SomeViewModel>()
+ // ...
+}
+
It is possible to access ViewModelStoreOwner of any entry that is currently present on the backstack. It is done through the the NavHostScope
receiver of the contentSelector
parameter of NavHost:
@Composable
+fun NavHostScope<Destination>.SomeScreen() {
+ val previousViewModel = viewModel<PreviousViewModel>(
+ viewModelStoreOwner = hostEntries.find {
+ it.destination is Destination.Previous
+ }!!
+ )
+ // ...
+}
+
There is no such thing as a Bundle of arguments for navigation entries in this library. This means that there is literally nothing to pass into SavedStateHandle
of your ViewModel as the default arguments.
I personally recommend passing all parameters into a ViewModel constructor directly. This keeps everything clean and type-safe.
If you use dependency injections in your project, explore the samples that show how to pass parameters into a ViewModel and inject all other dependencies:
Dagger/Anvil/Hilt use @AssistedInject
Koin supports ViewModel parameters out of the box and does it charmingly simple
If Hilt is the DI library of your choice and you want to use hiltViewModel()
method that you may already be familiar with from the official Navigation Component, you can add the dependency:
implementation("dev.olshevski.navigation:reimagined-hilt:<latest-version>")
+
It provides a similar hiltViewModel()
method that works with the Reimagined library. The only catch is that by default it doesn't know how to pass arguments to the SavedStateHandle of your ViewModel. For this you can use an additional defaultArguments
parameter:
val viewModel = hiltViewModel<SomeViewModel>(
+ defaultArguments = bundleOf("id" to id)
+)
+
And in ViewModel you can read this argument as such:
@HiltViewModel
+class SomeViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle
+) : LoggingViewModel() {
+
+ private val id: Int = savedStateHandle["id"]!!
+
+}
+
Tip
Don't forget to annotate your view models with @HiltViewModel
annotation.
Warning
Do not pass mutable data structures as defaultArguments
and expect the external changes to be reflected through the SavedStateHandle inside a ViewModel, e.g. when trying to return results as described here.
As soon as SavedStateHandle parcelize/unparcelize data once, it becomes the only source of truth for the data it holds.
If you still need to pass mutable data structure into your ViewModel, it would be more reliable to pass it directly as a constructor parameter).