Skip to content

Repository files navigation

Tiamat - Compose multiplatform navigation library

StarsForksLicense

TelegramSlack

Slack

gh-tiamat-promo.mp4

Add the dependency below to your module's build.gradle.kts file:

ModuleVersion
tiamatTiamat
tiamat-destinationsTiamat destinations
tiamat-destinations (plugin)Tiamat destinations

Tiamat Destinations README

Multiplatform

sourceSets {
commonMain.dependencies {
implementation("io.github.composegears:tiamat:$version")
}
}

Tiamat destinations

plugins {
// Tiamat-destinations kotlin compiler plugin
id("io.github.composegears.tiamat.destinations.compiler") version "$version"
}
sourceSets {
commonMain.dependencies {
// InstallIn annotations and Graph base class 
implementation("io.github.composegears:tiamat-destinations:$version")
}
}

Android / jvm

Use same dependencies in the dependencies { ... } section

Why Tiamat?

  • Code generation free
  • Pure compose
  • Support nested navigation
  • Support back-stack alteration and deep-links
  • Easy to use
  • Allow to pass ANY types as data, even lambdas (!under small condition)
  • Customizable transitions
  • Customizable screen placement logic
  • Customizable save-state logic
  • Support of Extensions

Setup

  1. Define your screens:
    valScreen by navDestination<Args> {
    // content
    }
  2. Create navController
    val navController = rememberNavController(
    key ="Some nav controller",
    startDestination =Screen,
    )
  3. Setup navigation
    Navigation(
    navController = navController,
    destinations = arrayOf(
    Screen,
    AnotherScreen,
    // ...,
    ),
    modifier =Modifier.fillMaxSize(),
    contentTransformProvider = { navigationPlatformDefault(it) }
    )
  4. Navigate
    valScreen by navDestination<Args> {
    val navController = navController()
    Column {
    Text("Screen")
    Button(onClick = { navController.navigate(AnotherScreen) }){
    Text("Navigate")
    }
    }
    }

see example: App.kt

Overview

Screen

The screens in Tiamat should be an entities (similar to composable functions)

the Args generic define the type of data, acceptable by screen as input parameters in the NavController:navigate fun

valRootScreen by navDestination<Args> {
// ...val nc = navController()
// ...
nc.navigate(DataScreen, DataScreenArgs(1))
// ...
}
data classDataScreenArgs(valt:Int)
valDataScreen by navDestination<DataScreenArgs> {
val args = navArgs()
}

The screen content scoped in NavDestinationScope<Args>

The scope provides a number of composable functions:

Some examples:

  • navController - provides current NavController to navigate back/further
  • navArgs - the arguments provided to this screen by NavControllr:navigate(screen, args) fun
  • navArgsOrNull - same as navArgs but provides null if there is no data passed or if it was lost
  • freeArgs - free type arguments, useful to store metadata or pass deeplink info
  • clearFreeArgs - clear free type arguments (eg: clear handled deeplink info)
  • navResult - provide the data passed to NavControllr:back(screen, navResult) as result
  • clearNavResult - clear passed nav result (eg: you want to show notification base on result and clear it not to re-show)
  • rememberViewModel - create or provide view model scoped(linked) to current screen

NavController

You can create NavController using one of rememberNavController functions:

funrememberNavController(
//...
)

and display as part of any composable function

@Composable
funContent() {
val navController = rememberNavController( /*... */)
Navigation(
navController = navController,
destinations = arrayOf(
// ...
),
modifier =Modifier.fillMaxSize()
)
}

NavController will keep the screens data, view models, and states during navigation

viewModel(navController) shared ViewModels are cleared when that NavController is destroyed (for example, when the corresponding navigation host leaves composition).

Important

The data may be cleared by system (eg: Android may clear memory)

funrememberNavController(
// ...saveable:Boolean? = null,
// ...
)

saveable property of remembered nav controller will indicate if we need to save/restore state or no

Extensions

You can attach an extension to any destination
There is 2 extension types: with and without content
The content-extension allows to process content before destination body and after by specifying type (Overlay, Underlay)
Here is simple tracker extension:

// define extensionclassAnalyticsExt(privatevalname:String) : ContentExtension<Any?>() {
@Composable
overridefunNavDestinationScope<outAny?>.Content() {
val entry = navEntry()
LaunchedEffect(Unit) {
val service =/*...*/// receive tracker
service.trackScreen(screenName = name, destination = entry.destination.name)
}
}
}
// apply ext to screenvalSomeScreen by navDestination<Args>(
AnalyticsExt("SomeScreen")
) {
// screen content
}

Storage mode

Important

Only 'Savable' types of params & args will be available to use within saveable nav controllers

eg: Android - Parcelable + any bundlable primitives

Known limitations

Important

Type checking has run into a recursive problem. Easiest workaround: specify types of your declarations explicitly ide error.

valSomeScreen1 by navDestination<Args> {
val navController = navController()
Button(
onClick = { navController.navigate(SomeScreen2) }, // << error here
content = { Text("goScreen2") }
)
}
valSomeScreen2 by navDestination<Args> {
val navController = navController()
Button(
onClick = { navController.navigate(SomeScreen1) }, // << or here
content = { Text("goScreen2") }
)
}

Appears when it is circular initialization happen (Screen1 knows about Screen2 who knows about Screen1 ...)

Solution: just define types of root(any in chain) screens explicitly

valSomeScreen1:NavDestination<Unit> by navDestination { /* ... */ }

Important

Why is my system back button works wired with custom back handler?

While using custom back handler do not forget 2 rules

  1. Always place NavBackHandler before Navigation
  2. use Navigation(handleSystemBackEvent = false) flag to disable extra back handler

Samples

See the examples here

Or try them in browser (require WASM support) here

Hint

Multiplatform

I want to navigate through multiple nav steps in 1 call (e.g. handle deeplink)

// there is 2 common ideas behind handle complex navigation//---- idea 1 -----// create some data/param that will be passed via free args // each screen handle this arg and opens `next` screenvalDeeplinkScreen by navDestination<Args> {
val deeplink = freeArgs<DeeplinkData>() // take free args val deeplinkNavController = rememberNavController(
key ="deeplinkNavController",
startDestination =ShopScreen
) {
// handle deeplink and open next screenif (deeplink !=null) {
editNavStack { _->listOf(
ShopScreen.toNavEntry(),
CategoryScreen.toNavEntry(navArgs = deeplink.categoryId),
DetailScreen.toNavEntry(navArgs =DetailParams(deeplink.productName, deeplink.productId))
)
}
clearFreeArgs()
}
}
Navigation(/*...*/)
}
//---- idea 2 -----// use route-apiif (deeplink !=null) {
navController?.route {
element(ShopScreen)
element(CategoryScreen.toNavEntry(navArgs = deeplink.categoryId))
element(DetailScreen.toNavEntry(navArgs =DetailParams(deeplink.productName, deeplink.productId)))
}
deepLinkController.clearDeepLink()
}

I use startDestination = null + LaunchEffect \ DisposableEffect to make start destination dynamic and see 1 frame of animation

// LaunchEffect & DisposableEffect are executed on `next` frame, so you may see 1 frame of animation// to avoid this effect use `configuration` lambda within `rememberNavController` funval deeplinkNavController = rememberNavController(
key ="deeplinkNavController",
startDestination =ShopScreen,
) { // executed right after being created or restored// so you can handle initial navigation here without any animations
}

How about 2-pane & custom layout?

// Yep, there is 2-pane layout example. You can also create fully custom layout by using `scene` apival nc = rememberNavController(
key ="nav controller",
startDestination =SomeDest1,
)
// using scene apiNavigationScene(
navController = nc,
destinations = arrayOf(
SomeDest1,
SomeDest2,
SomeDest3,
)
) {
// place you destinations as you want ( !!!CAUTION!!! do not render same entry twice in a frame)AnimatedContent(
targetState = nc.currentNavEntryAsState(),
contentKey = { it?.contentKey() },
transitionSpec = { navigationFadeInOut() }
) {
// you can also draw an entries from the whole nav stack if you need (but be careful)EntryContent(it)
}
}

Compose Preview for NavDestination

Library provides a utility function TiamatPreview for previewing individual navigation destinations in Compose Preview.

Note

Preview works best for pure Compose UI code. If your destination contains ViewModels, dependency injection, or complex app logic, consider creating separate preview functions for specific UI components instead of the entire destination.

Usage:

// Define your destinationvalDemoScreen by navDestination<Unit> {
Text("Demo Screen")
}
// Create preview
@Preview
@Composable
privatefunDemoScreenPreview() {
TiamatPreview(destination =DemoScreen)
}

For screens with arguments:

data classUserProfileArgs(valuserId:String, valuserName:String)
valUserProfileScreen by navDestination<UserProfileArgs> {
val args = navArgs()
Column {
Text("User: ${args.userName}")
Text("ID: ${args.userId}")
}
}
@Preview
@Composable
privatefunUserProfileScreenPreview() {
TiamatPreview(
destination =UserProfileScreen,
navArgs =UserProfileArgs(userId ="123", userName ="John")
)
}

For complex destinations with ViewModels or app logic:

// Instead of previewing the entire destinationvalComplexScreen by navDestination<Unit> {
val viewModel = viewModel<MyViewModel>()
val data by viewModel.data.collectAsState()
ComplexScreenContent(data = data)
}
// Create preview for the UI component
@Composable
privatefunComplexScreenContent(data:MyData) {
Column {
Text("Title: ${data.title}")
// ... rest of UI
}
}
@Preview
@Composable
privatefunComplexScreenContentPreview() {
ComplexScreenContent(
data =MyData(title ="Preview Title")
)
}

Desktop

Nothing specific (yet)

Android

Tiamat overrides LocalLifecycleOwner for each destination. This makes it compatible with lifecycle-aware components

See an example of CameraX usage: CameraXLifecycleScreen.kt

iOS

Nothing specific (yet)

Run/Build sample

Android: ./gradlew sample:app-android:assembleDebug

Jvm: ./gradlew sample:app-jvm:run

Jvm + hot-reload: ./gradlew sample:app-jvm:hotRun

Web: ./gradlew :sample:app-wasm:wasmJsBrowserDevelopmentRun

iOS: run XCode project or else use KMP plugin iOS target

other commands:

  • check ABI: ./gradlew checkAbi

  • update ABI: ./gradlew updateAbi

  • kover html report: ./gradlew :tiamat:koverHtmlReport

  • print test coverage: ./gradlew :tiamat:koverLog

  • run detekt checks: ./gradlew detekt

Contributors

Thank you for your help! ❤️

License

Developed by ComposeGears 2025
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Releases

Used by

Contributors

Languages