Tiamat
Simple Compose multiplatform navigation library
Install / Use
/learn @ComposeGears/TiamatREADME
[![Stars][badge:stars]][url:gh-stars] [![Forks][badge:forks]][url:gh-forks] [![License][badge:license]][url:gh-license]
[![Telegram][badge:telegram-invite]][url:telegram-invite] [![Slack][badge:slack-invite]][url:slack-invite]
[![Slack][badge:wasm-sample]][url:wasm-sample]
</div>https://github.com/user-attachments/assets/daa73bec-47f6-42bf-b38f-6378793540ee
Add the dependency below to your module's build.gradle.kts file:
| Module | Version | |------------------------------|:--------------------------------------------------------------------------------------------------------------------:| | tiamat | [![Tiamat][badge:maven-tiamat]][url:maven-tiamat] | | tiamat-destinations | [![Tiamat destinations][badge:maven-tiamat-destinations]][url:maven-tiamat-destinations] | | tiamat-destinations (plugin) | [![Tiamat destinations][badge:maven-tiamat-destinations-gradle-plugin]][url:maven-tiamat-destinations-gradle-plugin] |
Migration Tiamat 1.* -> Tiamat 2.*
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
- Define your screens:
val Screen by navDestination<Args> { // content } - Create navController
val navController = rememberNavController( key = "Some nav controller", startDestination = Screen, ) - Setup navigation
Navigation( navController = navController, destinations = arrayOf( Screen, AnotherScreen, // ..., ), modifier = Modifier.fillMaxSize(), contentTransformProvider = { navigationPlatformDefault(it) } ) - Navigate
val Screen 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
val RootScreen by navDestination<Args> {
// ...
val nc = navController()
// ...
nc.navigate(DataScreen, DataScreenArgs(1))
// ...
}
data class DataScreenArgs(val t: Int)
val DataScreen 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/furthernavArgs- the arguments provided to this screen byNavControllr:navigate(screen, args)funnavArgsOrNull- same asnavArgsbut providesnullif there is no data passed or if it was lostfreeArgs- free type arguments, useful to store metadata or pass deeplink infoclearFreeArgs- clear free type arguments (eg: clear handled deeplink info)navResult- provide the data passed toNavControllr:back(screen, navResult)as resultclearNavResult- 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:
fun rememberNavController(
//...
)
and display as part of any composable function
@Composable
fun Content() {
val navController = rememberNavController( /*... */)
Navigation(
navController = navController,
destinations = arrayOf(
// ...
),
modifier = Modifier.fillMaxSize()
)
}
NavController will keep the screens data, view models, and states during navigation
[!IMPORTANT] The data may be cleared by system (eg: Android may clear memory)
fun rememberNavController( // ... saveable: Boolean? = null, // ... )
saveableproperty of remembered nav controller will indicate if we need to save/restore state or no
Extensions
You can attach an extension to any destination<br>
There is 2 extension types: with and without content<br>
The content-extension allows to process content before destination body and after by specifying type (Overlay, Underlay)<br>
Here is simple tracker extension:
// define extension
class AnalyticsExt(private val name: String) : ContentExtension<Any?>() {
@Composable
override fun NavDestinationScope<out Any?>.Content() {
val entry = navEntry()
LaunchedEffect(Unit) {
val service = /*...*/ // receive tracker
service.trackScreen(screenName = name, destination = entry.destination.name)
}
}
}
// apply ext to screen
val SomeScreen by navDestination<Args>(
AnalyticsExt("SomeScreen")
) {
// screen content
}
Storage mode
[!IMPORTANT] Only 'Savable' types of params & args will be available to use within
saveablenav controllerseg: Android - Parcelable + any bundlable primitives
Known limitations
[!IMPORTANT]
Type checking has run into a recursive problem. Easiest workaround: specify types of your declarations explicitlyide error.val SomeScreen1 by navDestination<Args> { val navController = navController() Button( onClick = { navController.navigate(SomeScreen2) }, // << error here content = { Text("goScreen2") } ) } val SomeScreen2 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
val SomeScreen1: 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
- Always place
NavBackHandlerbeforeNavigation- 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` screen
val DeeplinkScreen by navDestination<Args> {
val deeplink = freeArgs<DeeplinkData>() // take free args
val deeplinkNavController = rememberNavController(
key = "deeplinkNavController",
startDestination = ShopScreen
) {
// handle deeplink and open next screen
if (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-api
if (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` fun
val deeplinkNavController = rememberNavController(
key = "deeplinkNavController",
startDestination = ShopScreen,
) { // executed right after being created or restored
//
Related Skills
node-connect
350.1kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.9kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
350.1kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
350.1kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
