Navigator Overview

In compose_bloc the UI is split in Screens and you navigate between the UI Screens with a Navigator. compose_bloc make extensive use of CompositionLocal data so that we can scope data to a specific part of the tree of composable calls that defines the UI. This also allows us to handle the lifecycle of Screens and Navigators that defines the UI of the app.

RootNavigator

We start by defining a RootNavigator for some Android Activity (or for some Compose Desktop Window):

class MainScreen: Screen {
    @Composable
        override fun Content() {
            //... main screen implementation here
        }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            //*IMPORTANT* need to use RootNavigator to initialize root navigator for activity, not as in original voyager
            RootNavigator(MainScreen())
        }
    }
}

LocalNavigator

Then in MainScreen Content() composable function and all its Composable subtree, we can access the navigator with LocalNavigator.current or LocalNavigator.currentOrThrow

class MainScreen: Screen {
    @Composable
    override fun Content() {
        val navigator= LocalNavigator.currentOrThrow
        //...
    }
}

renderComposableWithNavigator for Compose Web

For compose Web instead of RootNavigator you need to mount the composition at some HTML element using method renderComposableWithNavigator instead of the usual method renderComposable:

class MainScreen: Screen {
    @Composable
    override fun Content() {
		//,,, screen content implementation here
    }
}

fun main() {
    renderComposableWithNavigator
        (screens = listOf(MainScreen()),
         rootElementId = "root")
}

For compose Web in addition to LocalNavigator also the DOMScope for the HTML Element to which the navigator was bound, is made available with the CompositionLocal variable LocalDomScope .

Basic Navigator Operations

We can use the navigator instance to push new screens into the stack, or popping the current one if we want to go back to a previous screen.

class Screen1:Screen {
    @Composable
    override fun Content() {
        //... content for Screen 1 here
    }
}
class MainScreen2: Screen {
    @Composable
    override fun Content() {
        val navigator= LocalNavigator.currentOrThrow
        Column {
            Button(onClick={ navigator.push(Screen1())}) { Text("click to open screen1") }
            Button(onClick={ navigator.pop() }) { Text("click to close this screen") }
        }
    }
}

Navigator has integrated handling of OnBackPressed() events. In other words when the users navigate away from a screen (pressing the back button on Android), this is handled by calling navigator.pop() for the local navigator or its parent navigator if the local navigator has no screen that can be popped.

It is also possible to override handling of OnBackPressed() events by specifying a custom handler in the definition of the navigator. For example, for the RootNavigator we can write

setContent {
    RootNavigator(
        screen = MainScreen(),
        onBackPressed = { currentScreen ->
            //return true to pop the current screen, or false otherwise.
            false // won't pop the current screen
            // true will pop, default behavior
        }
        // To disable integration of handling of onBackPressed, set onBackPressed to null
        // onBackPressed = null
    )
}

Nested Navigators

For more complex navigation scenarios, for example for a screen with sub screens with each sub screen with an independent navigation stack, like a screen with tabs, with each tab with an independent navigation stack, we can define nested navigators

setContent {
    RootNavigator(ScreenA) { navigator0 ->
        println(navigator.level)
        // 0
        println(navigator.parent == null)
        // true
        Navigator(ScreenB) { navigator1 ->
            println(navigator.level)
            // 1
            println(navigator.parent == navigator0)
            // true
            Navigator(ScreenC) { navigator2 ->
                println(navigator.level)
                // 2
                println(navigator.parent == navigator1)
                // true
            }
        }
    }
}

Note that usage of RootNavigator is required only for the top Navigator of the current Activity

Router and path-based navigation.

path-based navigation was not supported in the original voyager library. Its implementation is derived from routing-compose .

Path-based navigation (i.e. navigation to a Screen with an URL ), is very natural for web apps, but is actually very useful for all platforms, for two main reasons:

  • support for deep links: for app deep links we need a way to associate a Screen to an URL

  • decoupling of an app into independent components: for big applications, where multiple features are developed independently in multiple modules, there is usually central module that is the only one linked against all other modules, that implement a router that allows to navigate to Screens of any of the modules without the need of knowing anything of the other modules except the URL of the target screen.

We take a slightly different approach from other navigation libraries like the official Android Compose navigation library, and also routing-compose : In our implementation routing is not part of the composition. It is instead a separate operation that it is executed outside the composition that take as input an URL and return a Screen that is then pushed into the navigation stack. This way we gain a better separation between UI rendering and routing logic. Let's see an example.

Last updated