Skip to content

Repository files navigation

Compose Navigator

A Jetpack Compose navigation library that gives you what the Fragment Navigation Component editor provides for XML — a live, visual map of every screen and how they connect — but for Compose, viewable directly inside your running app.

With plain NavHost or manual when navigation, screen relationships live only in scattered navigate() calls. There is no single place to see the shape of your app. Compose Navigator fixes that by making the navigation graph the single source of truth: you declare it once, and it drives both runtime navigation and the interactive map.


Features

  • Declarative graph DSL — define screens, groups, icons, and transitions in one place
  • Runtime navigation — back stack, navigate, popBackStack, popTo, system back support
  • Interactive navigation map — pan, pinch-to-zoom, tap a node to jump to that screen
  • Live back-stack highlighting — current screen and active path are highlighted on the map
  • Runtime edge discovery — undeclared transitions are recorded and drawn automatically
  • Orphan detection — unreachable screens are highlighted so you can spot dead ends
  • Mermaid export — generate diagrams for READMEs and PRs
  • Zero code generation — no KSP, no annotations, no IDE plugin required

Project structure

Module Description
:navigator The library (com.majid2851:compose-navigator:0.1.0)
:sample Demo app with ~10 screens showcasing the navigation map

Installation

Option A — Include as a module (recommended for now)

Copy or submodule the :navigator folder into your project, then add it to settings.gradle.kts:

include(":navigator")

In your app module's build.gradle.kts:

dependencies {
    implementation(project(":navigator"))
}

Make sure your app module has Compose enabled:

android {
    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(platform("androidx.compose:compose-bom:2024.09.00"))
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose:1.10.1")
}

Option B — Publish to local Maven

From this repository:

./gradlew :navigator:publishReleasePublicationToMavenLocal

Then in your project's settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        mavenLocal()
    }
}

And in your app module:

dependencies {
    implementation("com.majid2851:compose-navigator:0.1.0")
}

Requirements

Minimum
minSdk 24
compileSdk 35
Kotlin 2.0+
Jetpack Compose BOM 2024.09.00+
Material 3 required (used by the map UI)

Quick start

1. Declare your navigation graph

import com.majid2851.navigator.dsl.navGraph

val appGraph = navGraph(start = "home") {
    screen("home", label = "Home", group = "Main", icon = Icons.Filled.Home) {
        Button(onClick = { navigator.navigate("profile") }) {
            Text("Go to Profile")
        }
    }

    screen("profile", label = "Profile", group = "Account") {
        OutlinedButton(onClick = { navigator.popBackStack() }) {
            Text("Back")
        }
    }

    // Optional: show this transition on the map before the user ever walks it
    edge(from = "home", to = "profile", label = "Open profile")
}

Inside every screen { } block you are in a ScreenScope and have access to:

  • navigator — the NavigatorController
  • route — the current screen's route string

2. Wire up navigation + the map

import com.majid2851.navigator.map.NavigationMapScaffold
import com.majid2851.navigator.runtime.NavigatorHost
import com.majid2851.navigator.runtime.rememberNavigatorController

@Composable
fun App() {
    val graph = remember { appGraph }
    val controller = rememberNavigatorController(graph)

    NavigationMapScaffold(controller = controller) {
        NavigatorHost(
            controller = controller,
            modifier = Modifier.fillMaxSize(),
        )
    }
}

Run the app and tap the floating button in the bottom-right corner to open the full-screen navigation map.


Core concepts

┌─────────────────────────────────────────────────────────┐
│  navGraph { screen(...); edge(...) }  →  NavGraph       │
│         (single source of truth)                        │
└────────────────────┬────────────────────────────────────┘
                     │
         ┌───────────┴───────────┐
         ▼                       ▼
  NavigatorController        NavigationMap
  (back stack, navigate)     (visual graph UI)
         │
         ▼
    NavigatorHost
    (renders current screen)
Concept Role
NavGraph Immutable description of all screens and declared edges
NavigatorController Manages the back stack and records runtime transitions
NavigatorHost Renders the current screen with fade animation; handles system back
NavigationMap Draws the interactive graph canvas
NavigationMapScaffold Wraps your app and adds a FAB to open the map

DSL reference

navGraph(start) { }

Creates a NavGraph. The start route must match one of the declared screens.

val graph = navGraph(start = "home") {
    // screens and edges here
}

screen(route, label, group, icon) { }

Declares a destination.

Parameter Type Default Description
route String Unique identifier used in navigate()
label String route Human-readable name shown on the map node
group String? null Optional grouping label (e.g. "Auth", "Shop")
icon ImageVector? null Optional icon on the map node
content @Composable ScreenScope.() -> Unit The screen UI

edge(from, to, label)

Declares a static transition. Edges declared here appear on the map immediately, even before the user navigates that path. Runtime navigate() calls also create edges automatically.

edge(from = "home", to = "settings", label = "Open settings")

Navigation API

NavigatorController

Obtained via rememberNavigatorController(graph). Survives configuration changes.

// Navigate forward
navigator.navigate("detail")

// Avoid duplicate top entry
navigator.navigate("detail", launchSingleTop = true)

// Reset stack to [start, route] — useful after login / checkout
navigator.navigate("home", popUpToStart = true)

// Go back
navigator.popBackStack()          // returns false if already at root

// Pop until a specific screen is on top
navigator.popTo("home")           // inclusive = false (default)
navigator.popTo("home", inclusive = true)

// Clear everything back to the start destination
navigator.navigateToRoot()

// Read state
controller.currentRoute    // top of back stack
controller.backStack       // full stack as List<String>
controller.canNavigateBack // true when stack size > 1
controller.edges             // declared + runtime-observed edges
controller.observedEdges     // only runtime-discovered edges

NavigatorHost

NavigatorHost(
    controller = controller,
    modifier = Modifier.fillMaxSize(),
    handleSystemBack = true,   // wire Android back button (default: true)
)

LocalNavigator

For deeply nested composables that should navigate without callback threading:

@Composable
fun DeepChildButton() {
    val navigator = LocalNavigator.current
    Button(onClick = { navigator.navigate("settings") }) {
        Text("Settings")
    }
}

LocalNavigator is provided automatically inside NavigatorHost.


Navigation map

Using the scaffold (easiest)

NavigationMapScaffold(
    controller = controller,
    enabled = BuildConfig.DEBUG,   // only show FAB in debug builds
) {
    NavigatorHost(controller)
}

Embedding the map directly

NavigationMap(
    controller = controller,
    modifier = Modifier.fillMaxSize(),
    style = NavigationMapStyle(
        nodeWidth = 180.dp,
        nodeHeight = 90.dp,
        horizontalGap = 100.dp,
    ),
    onNodeClick = { route ->
        controller.navigate(route, launchSingleTop = true)
    },
)

Full-screen dialog

var showMap by remember { mutableStateOf(false) }

if (showMap) {
    NavigationMapDialog(
        controller = controller,
        onDismiss = { showMap = false },
    )
}

Map legend

Color Meaning
Primary Current screen
Secondary Screen in the back stack
Surface variant Reachable from the start destination
Error Unreachable / orphan screen (no path from start)

Gestures

Gesture Action
Drag Pan the canvas
Pinch Zoom in / out
Tap a node Navigate to that screen

Edge styles

Style Meaning
Solid arrow Declared in the DSL via edge(...)
Dashed arrow Discovered at runtime from a navigate() call
Bold arrow Part of the current back-stack path

Exporting the graph as Mermaid

Generate a versionable diagram for documentation or pull requests:

import com.majid2851.navigator.export.toMermaid

val diagram = graph.toMermaid(controller.observedEdges)
println(diagram)

Output example:

graph LR
    n0([Home])
    n1[Profile]
    n0 -->|Open profile| n1
    n1 -.-> n2[Settings]
Loading
  • Solid arrows (-->) = declared edges
  • Dashed arrows (-.->) = runtime-observed edges
  • Rounded nodes (([...])) = start destination

Paste the output into any Mermaid renderer or a GitHub markdown file.


Complete example

See the :sample module for a full working app with 10 screens across three groups (Main, Account, Shop), declared edges, orphan detection, and the map scaffold.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                val graph = remember { buildAppGraph() }
                val controller = rememberNavigatorController(graph)

                NavigationMapScaffold(controller = controller) {
                    NavigatorHost(controller, Modifier.fillMaxSize())
                }
            }
        }
    }
}

private fun buildAppGraph() = navGraph(start = "home") {
    screen("home", label = "Home", group = "Main", icon = Icons.Filled.Home) {
        Button(onClick = { navigator.navigate("catalog") }) { Text("Shop") }
        Button(onClick = { navigator.navigate("profile") }) { Text("Profile") }
    }
    screen("catalog", label = "Catalog", group = "Shop") {
        Button(onClick = { navigator.navigate("product") }) { Text("Product") }
        OutlinedButton(onClick = { navigator.popBackStack() }) { Text("Back") }
    }
    screen("product", label = "Product", group = "Shop") {
        OutlinedButton(onClick = { navigator.popBackStack() }) { Text("Back") }
    }
    screen("profile", label = "Profile", group = "Account") {
        OutlinedButton(onClick = { navigator.popBackStack() }) { Text("Back") }
    }
    edge(from = "home", to = "catalog", label = "Browse")
    edge(from = "catalog", to = "product", label = "View item")
}

Best practices

  1. Keep the graph at the top level — define it once in a remember { } block or a dedicated file, not inside individual screens.

  2. Use group to organize complex apps — groups appear as labels on map nodes and help you visually cluster related screens.

  3. Declare edges for important flows — runtime discovery works, but declaring edge(...) makes the map complete from the first launch.

  4. Gate the map behind BuildConfig.DEBUG — pass enabled = BuildConfig.DEBUG to NavigationMapScaffold so the FAB does not appear in release builds.

  5. Extract screen composables — keep screen { } blocks thin; put UI in separate @Composable functions for readability.

  6. Use LocalNavigator in deep trees — avoid passing onNavigate callbacks through many layers.


Building this project

# Build the demo app
./gradlew :sample:assembleDebug

# Build the library AAR
./gradlew :navigator:assembleRelease

# Publish to local Maven
./gradlew :navigator:publishReleasePublicationToMavenLocal

Open the project in Android Studio, sync Gradle, and run the sample configuration on a device or emulator.


How it compares

Approach Static map in IDE Live map in app Code generation Works with existing NavHost
Compose Navigator — (replaces navigation)
Navigation Component XML N/A (Fragments)
compose-nav-graph ✅ (KSP) ✅ (annotate only)

Compose Navigator is the right choice when you want developers to see and interact with the navigation map inside the running app without adding build-time tooling.


Public API summary

Package Key symbols
com.majid2851.navigator.dsl navGraph, NavGraphBuilder
com.majid2851.navigator.model NavGraph, Destination, Edge
com.majid2851.navigator.runtime NavigatorController, NavigatorHost, ScreenScope, LocalNavigator, rememberNavigatorController
com.majid2851.navigator.map NavigationMap, NavigationMapDialog, NavigationMapScaffold, NavigationMapStyle
com.majid2851.navigator.export NavGraph.toMermaid()

License

MIT

About

This is a library to have a vision of relations between pages in jetpack compose

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages