-
Notifications
You must be signed in to change notification settings - Fork 5
Compose Navigator Tutorials
These are small tutorials that covers some specific implementation detail.
- Navigating with arguments
- Navigating with animation
- Implementing Nested Navigation
- Navigate with single top instance or
popUpTo
- Navigate with
goBackUntil
orgoBackToRoot
- Implementing Dialogs
- Navigation in Dialogs
- Managing
onBackPressed
manually
When you declare a Route say a sealed
class (shown in Quick setup), the constructor parameters of the individual destination becomes the argument for that destination.
// Go to Quick setup guide to see the full example.
navigator.Setup(...) { controller, dest ->
when(dest) {
is MainRoute.First -> FirstScreen(dest.data, onChanged)
}
}
Kotlin's is
keyword will smartly cast the dest
to the type we have specified so that we can easily access the arguments.
Navigator allows you to set transitions for target
and current
destination.
Consider the following example, where we are navigating from current destination A
to target B
with animation.
// current --> target
controller.navigateTo(dest) {
withAnimation {
target = SlideRight
current = Fade
}
}
SlideRight
& Fade
are few of the built-in transition (see custom animations to built your own) you can apply to a destination.
Each transition defines a forward & backward transition methods which will be choose by the navigator based on navigation. In the above example SlideRight
transition is associated with target
& Fade
transition is associated with current
.
Since we are moving from current -> target
, SlideRight
's forward transition will run on target
& Fade
's backward transition will run on current
. This will result in slide-in-right for target
& fade out for current
.
During a back navigation (i.e on back press) these transition will be reversed i.e from moving target -> current
, Fade
's forward transition will run on current
& SlideRight
's backward transition will run on target
. This will result in slide-out-right for target
& fade in for current
.
Each transition extends from com.kpstv.navigation.compose.NavigatorTransition
interface which contains two important properties forwardTransition
& backwardTransition
(which you have to implement) that returns Modifier
object.
Optionally you can override animationSpec
property to provide a different FiniteAnimationSpec<T>
.
// Create & expose custom transition
public val Custom get() = CustomTransition.key
private val CustomTransition = object : NavigatorTransition() {
override val key: TransitionKey = TransitionKey("a_unique_name")
override val forwardTransition: ComposeTransition = ComposeTransition { modifier, width, height, progress ->
modifier.then(Modifier /*...*/)
}
override val backwardTransition: ComposeTransition = ComposeTransition { modifier, width, height, progress ->
modifier.then(Modifier /*...*/)
}
}
// register transition when initializing ComposeNavigator
class MainActivity : ComponentActivity() {
private lateinit var navigator: ComposeNavigator
override fun onCreate(savedInstanceState: Bundle?) {
...
navigator = ComposeNavigator.with(this, savedInstanceState)
.registerTransitions(CustomTransition) // <--
.initialize()
}
}
// Use the Custom transition
controller.navigateTo(dest) {
withAnimation {
target = Custom // our custom transition
current = Fade // built-in fade transition
}
}
When you call navigator.Setup
, it binds the current ComposeNavigator
to the CompositionLocal
which can be retrived using findComposeNavigator()
. It also binds the Controller<T>
associated with the destination T
for all the child composables which can be retrieved using findController<T>()
.
All you have to do is implement navigator.Setup
for another screen where you want nested-navigation.
Check out the Basic Sample for a more clear example.
@Composable
fun SecondScreen() { // nested to MainScreen
val mainController = findController<MainRoute>() // controller associated with MainRoute.
val navigator = findComposeNavigator()
navigator.Setup(key = SecondRoute.key, initial = SecondRoute.First()) { controller, dest ->
when(dest) {
...
}
}
}
controller.navigateTo(dest) {
singleTop = true // makes sure that there is only one instance of this destination in backstack.
}
controller.navigateTo(dest) {
popUpTo(Route::class) { // recursivly pops the backstack till the destination.
inclusive = true // inclusive
all = false // last added one will be chosen.
}
}
Both Controller<T>
& ComposeNavigator
support this functionality.
-
goBackUntil(dest)
- is similar topopUntil
feature where a destination is recursively popped until the specified destination is met. -
goBackToRoot()
- provides a jump to root functionality, for eg:A -> B -> C -> D
becomesA
more likegoBackUntil(A)
.
Note: These features may not be stable for ComposeNavigator
as determining parent routes from nested navigation is tricky & sometimes produce incorrect/unexpected results. For now you need to optin @UnstableNavigatorApi
to use them.
A -> B -> C -> D
controller.goBackUntil(B, inclusive = false) // produces: A -> B
controller.goBackUntil(B, inclusive = true) // produces: A
controller.goBackToRoot() // produces: A
Examples:
1. [s = {1,2,3} , n = {1,2} , t = {1,2}] -> target is "n:2" (inclusive)
=> navigator.goBackUntil(n:2, inclusive = true) // produces [s = {1,2,3} , n = {1}]
2. [s = {1,2,3} , n = {1,2} , t = {1,2}] -> target is "n:2" (not inclusive)
=> navigator.goBackUntil(n:2, inclusive = false) // produces [s = {1,2,3} , n = {1,2} , t = {1}]
Reason: n:2 serves as nested navigation for t which means it cannot exist unless t is present
which is why not inclusive keeps t:1 which is the intial route for that nested navigation.
3. [s = {1,2,3} , n = {1,2} , t = {1,2}] -> target is "n:1" (inclusive)
=> navigator.goBackUntil(n:1, inclusive = true) // produces [s = {1,2}]
Reason: s:3 serves as nested navigation for n which means it cannot exist unless n is present
which is why inclusive also removes s:3 which serves nothing more than just a route for
nested navigation for n.
4. [s = {1} , n = {1} , t = {1,2}] -> target is t:1 (inclusive)
=> navigator.goBackUntil(t:1, inclusive = true) // produces [s = {1} , n = {1} , t = {1}]
Reason: Only t:2 was removed, the reason is same both s:1 & n:1 serves as nested navigation for
their consecutive route hence they cannot exist one way the other.
- Refer to the tests for more examples.
Dialogs in navigator are similar to DialogFragment
from the View system. Here in Compose androidx.compose.ui.window.Dialog
are composables shown in android.view.Window
i.e above the actual setContent
& are considered to be unique dialog destination in navigator-compose
that are added or removed from the backStack.
When you setup navigation with navigator.Setup
, the controller that is used to manage the navigation for that Route
is used create, show & close dialogs.
Each dialog must extend from com.kpstv.navigation.compose.DialogRoute
, they can be a data
class where the constructor parameters becomes the argument for the dialog destination or object
for no argument dialog destination.
Check out the Basic Sample for a more clear example.
@Parcelize
data class MainDialog(val arg: String) : DialogRoute // arg dialog
@Parcelize
object CloseDialog : DialogRoute // no arg dialog
fun MainScreen() {
navigator.Setup(key = ...) { controller, dest
when(dest) {
is ... -> {
/* destination content */
// Button to show main dialog
Button(onClick = {
controller.showDialog(MainDialog(arg = "Test")) // <- Navigate to Main dialog.
}) {
Text("Show dialog")
}
}
...
}
// setup main dialog
controller.CreateDialog(key = MainDialog::class) {
/* You are in DialogScope */
/* your content */
Text(dialogRoute.arg) // <- /*this.dialogRoute*/ Use of argument
// `dismiss` is a function in DialogScope which can be invoked to
// close this dialog.
Button(onClick = { dismiss() }) {
Text("Close")
}
}
// setup close dialog
controller.CreateDialog(key = CloseDialog::class) {
...
}
}
}
Note:
- If dialog is not created using
controller.CreateDialog
&controller.showDialog
is called to show the dialog, then anIllegalStateException
is thrown. - If a dialog does not exist in the backStack & still tried to close it using
controller.closeDialog
, then anIllegalStateException
is thrown. - You should avoid re-using
DialogRoute
to create multiple dialogs in the sameController
instance. - Similar to the navigation backStack you can query the dialog backStack using
controller.getAllDialogHistory()
which returns an immutable list of all theDialogRoute
present in the backStack.
When you create a dialog using controller.CreateDialog
you are immediately in a DialogScope
which contains some helpful functions. One of them is dialogNavigator
.
dialogNavigator
is itself a ComposeNavigator
which you can use to create navigation inside this DialogScope
whether be nested, etc. This ComposeNavigator
is separate from activity's ComposeNavigator
i.e through findComposeNavigator()
.
Check out the Basic Sample for a more clear example on navigation dialogs.
@Composable
fun MainScreen() {
val controller = findController<MainRoute>()
controller.CreateDialog(key = SomeDialog::class) {
// inside DialogScope
// Setup navigation using `dialogNavigator`
dialogNavigator.Setup(key = ...) { controller, dest
when(dest) {
...
}
}
}
}
The current implementation of ComposeNavigator
registers a BackPressDispatcher
to automatically handle back press logic i.e back navigation for you. However there might be a case where you need full control over backpress logic.
In such case ComposeNavigator
exposes 2 methods canGoBack() : Boolean
& goBack()
which does what is says.
But first you need to disable the built-in backpress logic.
class MainActivity : ComponentActivity() {
private lateinit var navigator: ComposeNavigator
override fun onCreate(savedInstanceState: Bundle?) {
...
navigator = ComposeNavigator.with(...)
.disableDefaultBackPressLogic() // <--
.initialize()
}
override fun onBackPressed() {
if (navigator.canGoBack()) {
navigator.goBack()
} else {
super.onBackPressed()
}
}
}