A small and simple, yet fully fledged and customizable navigation library for Jetpack Compose:
- Full type-safety
- State restoration
- Nested navigation with independent backstacks
- Own lifecycle, saved state and view models for every backstack entry
- Animated transitions
- Navigation logic may be easily moved to the ViewModel layer
- No builders, no obligatory superclasses for your composables
- May be used for managing dialogs
Getting started
Add a single dependency to your project:
implementation("dev.olshevski.navigation:reimagined:1.0.0-beta01")
Define a set of screens, for example, as a sealed class:
sealed class Screen : Parcelable {
@Parcelize
object First : Screen()
@Parcelize
data class Second(val id: Int) : Screen()
@Parcelize
data class Third(val text: String) : Screen()
}
Create a composable with NavController
and NavHost
:
@Composable
fun NavHostScreen() {
val navController = rememberNavController<Screen>(
startDestination = Screen.First,
)
NavBackHandler(navController)
NavHost(controller = navController) { screen ->
when (screen) {
Screen.First -> Column {
Text("First screen")
Button(onClick = {
navController.navigate(Screen.Second(id = 42))
}) {
Text("To Second screen")
}
}
is Screen.Second -> Column {
Text("Second screen: ${screen.id}")
Button(onClick = {
navController.navigate(Screen.Third(text = "Hello"))
}) {
Text("To Third screen")
}
}
is Screen.Third -> {
Text("Third screen: ${screen.text}")
}
}
}
}
As you can see, NavController
is used for switching between screens, NavBackHandler
handles the back presses and NavHost
simply provides a composable corresponding to the latest destination in the backstack. As simple as that.
Basics
Here is the general workflow of the library:
Let's go into details about each of them.
NavController
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
within composition or with navController
outside of it. The latter may be used for storing NavController in a ViewModel. As it implements Parcelable interface, it may be (and should be) stored in SavedStateHandle.
Both rememberNavController
and navController
methods accept startDestination
as a parameter. If you want to create NavController with an arbitrary number of backstack items, you may use initialBackstack
parameter instead.
Destinations
NavController accepts all types meeting the requirements as destinations. The requirements are:
-
The type must be either Parcelable, or Serializable, or primitive, or of any other type that can be written to Parcel.
Navigation methods
There is a handful of pre-defined methods suitable for a basic app navigation: navigate
, pop
, popUpTo
, popAll
, replaceLast
, replaceUpTo
, replaceAll
. They all are pretty much self-explanatory.
If your use-case calls for some advanced backstack manipulations, you may use setNewBackstackEntries
method. In fact, this is the only public method defined in NavController, all other methods are provided as extensions and use setNewBackstackEntries
under the hood. You may see how a new extension method navigateToTab
is implemented in the sample.
NavBackstack
This is a read-only class that you may use to access current backstack entries. It is backed up by MutableState, so it will notify Compose about changes.
If you want to listen for backstack changes outside of composition you may set onBackstackChange
listener in NavController.
NavHost
NavHost is a composable that shows the last entry of a backstack and provides all components associated with this particular entry: Lifecycle, SavedStateRegistry and ViewModelStore. All these components are provided through CompositionLocalProvider inside the corresponding owners LocalLifecycleOwner
, LocalSavedStateRegistryOwner
and LocalViewModelStoreOwner
.
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).
NavHost by itself doesn't provide any animated transitions, it simply jump-cuts to the next destination.
AnimatedNavHost
AnimatedNavHost includes all functionality of the regular NavHost, but also supports animated transitions. Default transition is a simple crossfade, but you can granularly customize every transition with your own AnimatedNavHostTransitionSpec
implementation.
Here is one possible implementation of AnimatedNavHostTransitionSpec:
val CustomTransitionSpec = AnimatedNavHostTransitionSpec<Any?> { action, from, to ->
val direction = when (action) {
is NavAction.Backward -> AnimatedContentScope.SlideDirection.End
is NavAction.Forward -> AnimatedContentScope.SlideDirection.Start
}
slideIntoContainer(direction) with slideOutOfContainer(direction)
}
Set it into AnimatedNavHost:
AnimatedNavHost(
controller = navController,
transitionSpec = CustomTransitionSpec
) { destination ->
// ...
}
and it'll end up looking like this:
In AnimatedNavHostTransitionSpec you get the parameters:
action
- the hint about the last NavController method that changed the backstackfrom
- the previous visible destinationto
- the target visible destination
This information is plenty enough to choose a transition for every possible combination of screens and navigation actions.
NavAction
Here is the diagram of NavAction type hierarchy:
Pop
, Replace
and Navigate
are objects that correspond to pop…
, replace…
, navigate
methods of NavController. You may either check for these specific types or handle general Forward
and Backward
actions.
You can also create new action types by extending abstract classes Forward
and Backward
. Pass these new types into setNewBackstackEntries
method of NavController and handle them in AnimatedNavHostTransitionSpec.
DialogNavHost
The version of NavHost that is better suited for showing dialogs. It is based on AnimatedNavHost and provides smoother transition between dialogs without scrim/fade flickering.
If you want to see how you can implement dialogs navigation explore the sample.
Note that DialogNavHost doesn't wrap your composables in a Dialog. You need to use use either Dialog or AlertDialog composable inside a contentSelector
yourself.
Back handling
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 the back presses where you need to.
NavBackHandler is the most basic implementation of BackHandler that calls pop
until one item in the backstack is left. Then it is disabled, so any upper-level BackHandler may react to the back button press.
Important note: always place your NavBackHandler/BackHandler before the corresponding NavHost. Read the explanation here.
Nested navigation
Adding nested navigation is as simple as placing one NavHost into another. Everything is handled correctly and just works.
You may go as many layers deep as you want. It's like fractals, but in navigation.
Return values to previous destinations
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. Again, you may see demo of this in the sample.
Note: In general, returning values to the previous destination makes the navigation logic more complicated, so use it with caution and when you are sure what you are doing. Sometimes it may be easier to use a shared state holder.
Documentation and sample
Explore the KDoc documentation of the library for more details about every component and every supported features.
Also, explore the sample. It provides demos of all the functionality mentioned above and even more. The sample shows:
- nested navigation
- tab navigation
- NavHost/AnimatedNavHost usage
- dialogs
- passing and returning values
- ViewModels
- hoisting NavController to the ViewModel layer
Why beta
I'm very satisfied with the shape and form of the library. I have spent long sleepless nights debugging and polishing all corner cases.
For now I'll be glad to hear a feedback and do a minor fine-tunings of the API (if any at all). If there are any changes you may expect a notice in release notes.
About
I've been thinking about Android app architecture and navigation in particular for the longest time. When introduced to Compose I could finally create the navigation structure that fits perfectly all my needs.
Making it in the form of a public library closes a gestalt for me. I'm finally done with it. Onto new projects!
If you like this library and find it useful, please star the project and share it with your fellow developers. A little bit of promotion never hurts.