Skip to content

Compose Navigator Tutorials

Kaustubh Patange edited this page Aug 3, 2021 · 20 revisions

These are small tutorials that covers some specific implementation detail.

Table of contents

Navigating with arguments

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.

Navigating with animation

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.

Custom animations

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

Navigate with single top instance or popUpTo

controller.navigateTo(dest) {
    singleTop = true // makes sure that there is only one instance of this destination in backstack.
}
controller.navigateTo(dest) {
    popUpTo(dest) { // recursivly pops the backstack till the destination.
        inclusive = true // inclusive
        all = false // last added one will be chosen.
    }
}

Implementing Nested Navigation

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

Implementing Dialogs

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) { dialogRoute, dismiss ->
            /* your content */
            Text(dialogRoute.arg) // <- Use of argument

            // `dismiss` is a lamda function which can be invoked to
            // close this dialog.
            Button(onClick = dismiss) {
                Text("Close")
            }
        }

        // setup close dialog
        controller.CreateDialog(key = CloseDialog::class) { _, dismiss ->
            ...
        }
    }
}

Note:

  • If dialog is not created using controller.CreateDialog & controller.showDialog is called to show the dialog, then an IllegalStateException is thrown.
  • If a dialog does not exist in the backStack & still tried to close it using controller.closeDialog, then an IllegalStateException is thrown.
  • You should avoid re-using DialogRoute to create multiple dialogs in the same Controller instance.
  • Similar to the navigation backStack you can query the dialog backStack using controller.getAllDialogHistory() which returns an immutable list of all the DialogRoute present in the backStack.
Clone this wiki locally