Tiamat Navigation

repository·main·Indexed 42 days ago

https://github.com/composegears/tiamat

A pure Compose multiplatform navigation library that is code-generation free. Tiamat supports nested navigation, back-stack alteration, deep-links, and passing arbitrary data types between screens. It includes an optional experimental Destinations compiler plugin for automatic destination list generation using @InstallIn annotations and TiamatGraph.

Tokens
5.2K
Snippets
17
Records
23
Agent score
54%

What's inside Tiamat

  1. How Screen and NavDestinationScope work

    main

    A Screen is an entity defined using navDestination<Args>. The Args generic type defines the input parameters accepted by the screen during navigation.

    Inside the screen's content block, you have access to NavDestinationScope<Args>, which provides several useful composable functions:

    • navController(): Provides the current NavController to perform navigation.
    • navArgs(): Retrieves the arguments passed to this screen via navigate(screen, args).
    • navArgsOrNull(): Same as navArgs, but returns null if no data was passed or if data was lost.
    • freeArgs<T>(): Accesses free type arguments (useful for metadata or deep-link info).
    • clearFreeArgs(): Clears the free type arguments.
    • navResult: Provides data passed back via NavController.back(screen, navResult).
    • clearNavResult(): Clears the passed navigation result.
    • rememberViewModel(): Creates or provides a ViewModel scoped to the current screen.
    data class DataScreenArgs(val t: Int)
    
    // Screen receiving arguments
    val DataScreen by navDestination<DataScreenArgs> {
        val args = navArgs()
        // Use args.t
    }
    
    // Screen navigating with arguments
    val RootScreen by navDestination<Unit> {
        val nc = navController()
        nc.navigate(DataScreen, DataScreenArgs(1))
    }
  2. How NavController works

    main

    The NavController manages screen data, ViewModels, and states during navigation. You create it using rememberNavController.

    Key Behaviors:

    • Lifecycle: ViewModels created via viewModel(navController) are cleared when the NavController is destroyed (e.g., when the navigation host leaves the composition).
    • State Restoration: The saveable parameter in rememberNavController determines if the controller should attempt to save and restore state (e.g., on Android during process death). Note that only 'Savable' types (like Parcelable or primitives on Android) can be used with saveable controllers.
    @Composable
    fun Content() {
        val navController = rememberNavController(
            key = "my_nav_controller",
            startDestination = Home,
            saveable = true
        )
        Navigation(
            navController = navController,
            destinations = arrayOf(Home, Profile),
            modifier = Modifier.fillMaxSize()
        )
    }
  3. Quickstart: Define, Setup, and Navigate

    main

    Follow these four steps to implement basic navigation:

    1. Define Screens: Use navDestination<Args> where Args is the type of data the screen accepts.
    2. Create NavController: Use rememberNavController with a startDestination.
    3. Setup Navigation: Use the Navigation composable, passing the navController and an array of all possible destinations.
    4. Navigate: Use navController.navigate(Screen, args) to move between screens.
    // 1. Define 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 navController = navController()
    navController.navigate(AnotherScreen)
  4. Install Tiamat and Tiamat Destinations

    main

    To use Tiamat in a Multiplatform project, add the following dependencies to your commonMain source set. If you want to use the Tiamat Destinations features (like InstallIn annotations and the Graph base class), you must also include the compiler plugin and the destinations library.

    // Standard Tiamat dependency
    // In commonMain.dependencies
    implementation("io.github.composegears:tiamat:$version")
    
    // Tiamat Destinations setup
    // In plugins block
    id("io.github.composegears.tiamat.destinations.compiler") version "$version"
    
    // In commonMain.dependencies
    implementation("io.github.composegears:tiamat-destinations:$version")
  5. Setup Tiamat Destinations

    main

    To use automatic destination list generation, you must apply the Tiamat Destinations compiler plugin and add the corresponding dependency to your commonMain source set.

    Note: This is an experimental API.

    // Apply compiler plugin in the plugins section
    plugins {
        id("io.github.composegears.tiamat.destinations.compiler") version "$version"
    }
    
    // Add dependency
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.composegears:tiamat-destinations:$version")
        }
    }
  6. Register NavDestinations with @InstallIn

    main

    Tiamat Destinations allows you to automatically generate a destination list for a NavController by annotating destinations with @InstallIn.

    1. Create a graph object that extends TiamatGraph.
    2. Annotate your destinations with @InstallIn(Graph::class).

    Supported patterns:

    • Delegate: Using by navDestination<Args> { ... }.
    • Constructor: Using NavDestination<Unit>(...).
    • Object: Implementing the NavDestination<T> interface in an object.
    • Global Instance: Using a top-level property of a class (e.g., val Screen = ScreenClass()).

    Constraints:

    • You cannot apply @InstallIn directly to a non-object class. This will cause a compile error. Use a top-level property instead.
    • Applying duplicate @InstallIn(SameGraph::class) annotations on the same property produces a compile warning, but the destination is only registered once per graph.
    object Graph : TiamatGraph
    
    // Using delegate
    @InstallIn(Graph::class)
    val Screen1 by navDestination<Args> { }
    
    // Using constructor
    @InstallIn(Graph::class)
    val Screen2 = NavDestination<Unit>(name = "Screen2", extensions = emptyList()) {}
    
    // Using object
    @InstallIn(Graph::class)
    object Screen3 : NavDestination<Int> {
        override val name: String = "Screen3"
        override val extensions: List<Extension<Int>> = emptyList()
    
        @Composable
        override fun NavDestinationScope<Int>.Content() {
        }
    }
    
    // Using global instance property (for class-based destinations)
    class Screen4Class : NavDestination<Int> {
        override val name: String = "Screen4"
        override val extensions: List<Extension<Int>> = emptyList()
    
        @Composable
        override fun NavDestinationScope<Int>.Content() {
        }
    }
    
    @InstallIn(Graph::class)
    @InstallIn(SomneOtherGraph::class)
    val Screen4 = Screen4Class()
  7. Generate Graph Debug Dumps

    main

    The compiler plugin can export the resolved navigation graph as a Markdown file for review. To enable this, set the tiamat.destinations.dumpDir property in your gradle.properties file to a directory path (relative to the project root or absolute).

    # gradle.properties
    tiamat.destinations.dumpDir=build/tiamat-graph
  8. Register destinations using @InstallIn

    main

    The Tiamat Destinations compiler plugin uses the @InstallIn annotation to automatically populate the destinations() function of a TiamatGraph subclass.

    To register a screen or destination, annotate either a top-level property or an object that implements NavDestination with @InstallIn(YourGraph::class).

    Rules for usage:

    • Target Types: You can only use @InstallIn on object declarations or properties of type NavDestination.
    • Graph Type: The argument passed to @InstallIn must be a subclass of TiamatGraph (do not use TiamatGraph itself).
    • Uniqueness: Each graph should appear at most once per destination to avoid duplicates.
    // Example: Registering a screen in a specific graph
    @InstallIn(MyGraph::class)
    val myScreen = MyScreen() // where MyScreen is a NavDestination
    
    // Example: Registering an object
    @InstallIn(MyGraph::class)
    object MySettingsScreen : NavDestination { ... }
  9. Configure the M2P Gradle plugin

    main

    The m2p plugin provides automated Maven publishing and signing configuration for Tiamat destinations. You can configure the plugin using the m2p extension function in your build.gradle.kts file.

    Currently, the only configurable property is description, which is used to populate the description field in the generated Maven POM file.

    m2p {
        description.set("A description of my Tiamat destination module")
    }
  10. Troubleshooting: System Back Button behavior

    main

    If your system back button is not working as expected when using a custom back handler, ensure you follow these two rules:

    1. Always place the NavBackHandler before the Navigation composable.
    2. Use the handleSystemBackEvent = false flag in the Navigation composable to disable the default extra back handler.
  11. Manually override destinations (Emergency fallback)

    main

    If you encounter issues with the compiler plugin, you can bypass it by manually overriding the destinations() function in your TiamatGraph implementation. This allows you to provide the array of NavDestination<*> manually.

    object Graph : TiamatGraph {
        override fun destinations(): Array<NavDestination<*>> = arrayOf(
            Screen1, Screen2, Screen3
        )
    }
  12. Troubleshooting: Circular Initialization Errors

    main

    If you encounter IDE errors related to recursive type checking (e.g., SomeScreen1 knows about SomeScreen2, which in turn knows about SomeScreen1), it is caused by circular initialization.

    Solution: Explicitly define the types of the root screens in the chain.

    // Instead of implicit types, define them explicitly:
    val SomeScreen1: NavDestination<Unit> by navDestination { /* ... */ }