“A paper kite flying peacefully at dawn” ~ DALL.E

Guia — Jetpack Compose Navigation

Roudi Korkis Kanaan
4 min readFeb 6, 2023

--

If you prefer getting straight into the docs, you can find the documentation on Github and Gitbook here.

When I first started using Compose, I immediately fell in love with it. I wanted my entire application to be written in Compose, even the navigation part. So I started searching for what is the “official” way of navigating with Compose and stumbled upon Compose Navigation.

I was disappointed since it’s basically a port of the Navigation Component which I was already not a fan of. Using Strings to navigate between destinations, so much generated code to have any sort of safe args, etc.

I just wanted something simple, scalable and doesn’t require another library just to have transitions.

Introducing Guia

I wanted a cool catchy name but I suck at naming things so I asked ChatGPT to do it for me, this is the actual conversation (Not really):

Me: Yo ChatGPT, I’m writing an Android Navigation Library for Jetpack Compose. I want some cool unique name, short and easy to remember.

ChatGPT: I gotchu fam, use “Guia”, it means Guidance. Ya welcome. K bai.

Key features:

  • Multi Module Support
  • Transitions
  • Lifecycle, scoped ViewModel support
  • Screens, Dialogs, BottomSheets supported from the get go.
  • State saving/restoration
  • Type safe arguments
  • Stateful Type safe results / Also results that can survive process death
  • Ability to extend the library to have support for more use cases (Stacks, ViewPager, etc.)
  • Deeplinking
  • BottomNav navigation with different back handling strategies

Alright, let’s get started.

implementation(“com.roudikk.guia:guia:$currentVersion”)

For the latest version, go here.

Navigation Keys (Destinations)

To declare a destination we can navigate to, we just implement NavigationKey

@Parcelize
class HomeKey: NavigationKey

The key itself is Parcelable, that’s how we save and restore the state of our backstack. A key can also have arguments, that’s how arguments are passed in Guia:

@Parcelize
data class ProfileKey(val id: String): NavigationKey

The keys don’t have any UI yet, this is on purpose, this makes multi module navigation easier, since we can expose keys to other modules and keep the implementation of it internal, we will see later how we connect keys to an actual UI.

Navigation Nodes

Navigation nodes is how we define a UI for our keys. The three supported types out of the box in Guia are Screen, Dialog and BottomSheet.

Screens don’t necessarily mean a full screen, their dimensions are controlled by their modifiers and the container they’re hosted in.

Dialogs and BottomSheets both have DialogOptions and BottomSheetOptions that can be updated, that means the behaviours of each can be changed in realtime!

For more information and in-depth explanation on navigation nodes, check the NavigationNode part in the Gitbook.

Navigator

Now that we have keys we can navigate to, let’s create a Navigator. A Navigator is basically a simple Backstack, in fact the only public function the Navigator class has is setBackstack(entries: List<BackstackEntry>, isPop: Boolean).

To create a Navigator we start with:

val navigator = rememberNavigator()

From the above, we see that setBackstack takes in a BackstackEntry not a NavigationKey, this is because we need to differentiate a key in our backstack from other keys of similar type, BackstackEntry is simply an uniquely Id-ed version of a NavigationKey.

So to set a new Backstack, we can call:

val navigator = rememberNavigator()
navigator.setBackstack(HomeKey().entry(), ProfileKey("some_id").entry())

But, Guia makes it easier to set the backstack with traditional operations and some more advanced ones with extension functions, the long list can be found here.

For example:

Connect a NavigationKey to a NavigationNode

To link a key to a composable, we can do so in two ways. If we really don’t care about separating our keys from our UI, we can use self hosted navigation keys:

If we are in a multi module project and we need to separate the Composables (that would be internal to that module) and keys that need to be accessed from different modules, we can link the two in our rememberNavigator call, the block initialises a NavigatorConfig:

Rendering the UI!

Finally, we can render the state of our navigator using NavContainer:

navigator.NavContainer()

It has some sensible defaults on how screens, bottomsheets and dialogs are rendered but we can override some parameters, check the Gitbook guide for containers for customisation.

Transitions!

Guia uses Compose’s EnterTransition and ExitTransition for animation.

The order of deciding which transition to run goes like this:

  • Check if the current transition is being overriden
  • Check if there’s a key transition, a transition for a specific NavigationKey
  • Check if there’s a node transition, a transition for a specific type of NavigationNode
  • Finally fall back to a default transition

The above can be set through the NavigatorConfig

First we need to declare a NavigationTransition, for example:

This will slide the content left/right with some fading in/out as the navigation keys are navigated.

Second, we need to update our NavigatorConfig to add the transition:

Overriding transitions

We can override a specific transition before we set a backstack, for specific cases where we need to simply override a single setBackstack 's transition.

To do so we can use overrideTransition:

Results passing

Passing results between navigation keys is done in Guia using the ResultManager API, it's type safe and stateful.

A Navigator itself is a ResultManager backed by a stateful key/value map.

Survive state restoration

By simply marking our result as Parcelable our result can now be saved and restored, the ResultManager will handle that internally.

@Parcelize
data class Result(val item: String): Parcelable

Passing data between different navigators

The ResultManager API is public can be used to create our own:

val resultManager = rememberResultManager()

Now we can provide this result manager as CompositionLocal to children Composables, that can potentially be hosting different navigators.

And much more…

Guia has more things to offer that we haven’t talked about in this article.

For an indepth documentation, check the Gitbook.

You can also check the code on Github.

--

--

Roudi Korkis Kanaan

Android Engineer currently in Melbourne. I love building beautiful applications for people to enjoy. Accessibility is key.