SkillAgentSearch skills...

Tiamat

Simple Compose multiplatform navigation library

Install / Use

/learn @ComposeGears/Tiamat
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<h2 align="center">Tiamat - Compose multiplatform navigation library</h2> <div align="center">

[![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] |

Tiamat Destinations README

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

  1. Define your screens:
       val Screen 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
    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/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:

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,
  // ...
)

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<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 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.

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

  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` 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

View on GitHub
GitHub Stars258
CategoryDevelopment
Updated19d ago
Forks10

Languages

Kotlin

Security Score

100/100

Audited on Mar 18, 2026

No findings