compose-nav-graph renders your @Preview thumbnails and fails the build when the graph drifts
⛵️ Compose Navigation Graph plugin for Android Studio that visualizes your entire app flow as an interactive map of rendered previews, typed arguments, and transitions.
At a glance
- What is it?
- A Kotlin Android Studio plugin that turns an app's navigation into one canvas of rendered preview thumbnails, typed argument nodes and transition arrows, extracted statically at compile time by a KSP processor. The part with teeth is a committed baseline file that turns navigation changes into something a pull request can catch.
- Who is it for?
- compose-nav-graph is for anyone whose navigation lives in lambda and back stack call sites spread over several modules and has stopped being reviewable in their head, since merging every module's graph into one canvas is the thing no amount of code reading does well. It is not a runtime tool: nothing here observes your app, everything is extracted at compile time from annotations and call sites, so a flow that is only constructed dynamically will not appear.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 57 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- skydoves/compose-nav-graph547 ★ · +23
| Repository | 2026-09-18 | 2026-11-14 |
|---|---|---|
| skydoves/compose-nav-graph | 524 | 547 |
23 Stars in the last 57 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Four pieces, and two arrive automatically
The toolkit is four cooperating parts rather than one plugin, and the split explains what it can and cannot see. The annotations artifact, `com.github.skydoves:compose-nav-graph-annotations`, provides four annotations: `@NavDestination`, `@NavEdge`, `@NavPreview` and `@NavGraphRoot`. The KSP processor, `com.github.skydoves:compose-nav-graph-ksp`, statically extracts each module's graph into a `nav-graph.json` at compile time. Both of those are added for you when you apply the Gradle plugin, so you only have to write the annotations. The Gradle plugin itself is applied by id as `com.github.skydoves.navgraph`, and it does the parts a symbol processor cannot: it renders device free Layoutlib thumbnails, infers transitions that KSP cannot read from navigation call sites, merges the graph across modules, and supplies the tasks. The fourth piece is the IDE plugin, which draws the merged flow and the preview gallery in a tool window called NavGraph Graph.
Thumbnails come from previews you already wrote
The rendering trick is worth understanding because it determines what you get on screen. There is no emulator and no screenshot service involved. The plugin renders existing `@Preview` composables through Layoutlib, which is the same rendering stack Compose uses in the IDE, and pairs each rendered thumbnail with the destination it belongs to through a `@NavPreview` annotation that names the route and marks which preview is primary. So the map is only as complete as your preview coverage, and a screen with no preview becomes a node without an image rather than a failure. That is a feature for teams already writing previews as part of their component workflow, and a gap for teams that do not, since the first piece of work is adding previews rather than adding the plugin. The Graph tab also lets you reframe the thumbnails with a device selector, so the same node can be inspected at more than one size.
inferEdges reads the call sites the processor cannot
Static extraction has a blind spot, and the plugin names it rather than hiding it. A KSP processor can see annotations, so `@NavEdge(to = ..., label = ...)` transitions are read directly. It cannot reliably see a `backStack.add(...)` call or an `entry<Route> { }` lambda and work out which screen that actually navigates to, so the Gradle plugin fills that gap by reading navigation call sites. That is what the `inferEdges` switch controls, and it defaults to on. The trade-off is precision versus coverage: inferred edges are what make a graph complete when a team has not annotated every transition, and they are also the edges most prone to being wrong in an unusual codebase. If you annotate thoroughly you can turn inference off, and if you inherited a codebase carrying none of the annotations, inference is what makes the map worth looking at in the first place. The Graph tab then lets you add a transition directly from the canvas and jump to source with a double click.
failOnNavChange turns a committed baseline into a CI gate
The mechanism that goes beyond a visualisation is the `.nav` baseline, and the switch that controls it is `failOnNavChange`, on by default. When enabled, the `navCheck` task fails the build if the extracted graph has drifted from the committed baseline, so a destination added or a transition removed cannot land in a pull request without someone noticing. The framing in the documentation is that no destination or transition should change unreviewed, and the mechanism is ordinary snapshot testing applied to a graph rather than to a screenshot. Two adjacent tasks exist for the same file: `generateNavGraph` regenerates it, and the refresh button in the Graph tool window runs that task for you, so the canvas reflects your latest code rather than whatever was there when you opened the window. `navDump` and the export tasks cover the cases where you want the graph out of the build entirely, which is what you would do before wiring it into an external review flow.
Five switches in the navgraph block, all defaulting on
Configuration is a single extension block, and every switch in it defaults to the behaviour you would probably want:
navgraph {
renderThumbnails.set(true)
variant.set("demoDebug")
inferEdges.set(true)
failOnNavChange.set(true)
galleryEnabled.set(true)
}`renderThumbnails` turns on the device free Layoutlib rendering, `inferEdges` the call site inference, `failOnNavChange` the build failure described above, and `galleryEnabled` the preview gallery pipeline. The only one you normally have to think about is `variant`, which pins a build variant or flavour; leave it blank and it auto detects the debug KSP variant, which is what you want in a multi-flavour project where the KSP output differs per variant. Applying the plugin itself needs the KSP plugin alongside it, with a version matched to your Kotlin version, and `mavenCentral()` present in your plugin repositories in `settings.gradle.kts` alongside `google()` and `gradlePluginPortal()`.
Multi-module merging works even when the app module is unannotated
The feature that decides whether the plugin is useful on a real project is multi-module support. The Graph tab merges every module's graph into a single canvas, and it does so even when the `:app` module itself carries none of the annotations, which is the common case when screens live in feature modules and the application module is only assembly. Before this, the only way to see the flow was to reconstruct it mentally from lambdas and back stack calls spread across module boundaries, and that is exactly the failure mode the tool is aimed at. Alongside the merged graph you get typed argument nodes, so a screen's parameters are visible on the canvas rather than inferred from a signature, and every transition between screens is drawn as an edge. The tool window is reached through View then Tool Windows then NavGraph Graph, and once installed it opens with three tabs: Graph, Previews and Author. The IDE plugin installs from the JetBrains Marketplace, and the annotation badge indicates a minimum API level of 24.
Renovate, Spotless, three sample apps and one markdown setup file
The repository layout says what kind of project this is. There are five Gradle modules, `compose-nav-graph-annotations`, `compose-nav-graph-ksp`, `compose-nav-graph-gradle`, `compose-nav-graph-idea` and `compose-nav-graph-testing`, plus `samples/` holding three worked examples: a bare sample, one wired to a Kotlin conf app structure, and one built on Now in Android. `renovate.json` is present, so dependency updates are automated rather than manual, and `spotless/` means formatting is enforced across the codebase. Documentation is mkdocs, configured through `mkdocs.yml`, with sources under `docs/`, and there is a committed `nav-results/` directory which is consistent with a tool that produces reviewable output. One file is worth calling out for anyone evaluating this quickly: `plugin-agent-guides.md` is written to be handed to an LLM such as Claude Code, Cursor or the Gemini CLI as-is, and it instructs it to apply the Gradle plugin, annotate your screens and generate the first graph. The last release is 0.3.0 on 2026-09-18, following 0.2.1 in July and 0.2.0 in June.
Editorial conclusion
compose-nav-graph is for anyone whose navigation lives in lambda and back stack call sites spread over several modules and has stopped being reviewable in their head, since merging every module's graph into one canvas is the thing no amount of code reading does well. It is not a runtime tool: nothing here observes your app, everything is extracted at compile time from annotations and call sites, so a flow that is only constructed dynamically will not appear. Before you adopt it, check two things. First, whether your screens already have `@Preview` composables, because the thumbnails are those previews rather than something the plugin invents. Second, whether you are willing to let `failOnNavChange` break CI, since it defaults to on and a committed `.nav` baseline plus a failing `navCheck` will reject any destination or transition change that nobody updates deliberately.
Frequently asked questions
How does compose-nav-graph render screens without an emulator?
It renders your existing @Preview composables through Layoutlib at build time, the same rendering stack Compose uses in the IDE, and attaches each thumbnail to a destination through the @NavPreview annotation. Nothing is screenshotted from a running device, and a screen without a preview appears as a node without an image.
What are the four pieces of the compose-nav-graph toolkit?
An annotations artifact providing @NavDestination, @NavEdge, @NavPreview and @NavGraphRoot, which is added automatically. A KSP processor that extracts each module's graph to nav-graph.json at compile time, also added automatically. A Gradle plugin applied as com.github.skydoves.navgraph that renders thumbnails, infers edges, merges modules and supplies the tasks. And an IDE plugin that draws the merged graph and gallery in the NavGraph Graph tool window.
What does failOnNavChange do in compose-nav-graph?
It makes the navCheck task fail the build when the extracted navigation graph drifts from the committed .nav baseline, so a destination or transition change cannot land without being updated deliberately. The switch defaults to on. Related tasks are generateNavGraph, which regenerates the file and is what the refresh button runs, plus navDump and the export tasks.
Does compose-nav-graph work across multiple Gradle modules?
Yes, and that is the feature aimed at real projects. The Graph tab merges every module's graph into one canvas even when the :app module itself carries no annotations, which is the usual case when screens live in feature modules. The merged view adds typed argument nodes and transition edges, and a double click jumps to the source.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/skydoves-compose-nav-graph)Star history chart for your README
[](https://hysenlabs.com/projects/skydoves-compose-nav-graph)Was this useful?