Skip to content

programadorthi/kotlin-routing

Repository files navigation

Kotlin Routing

A multiplatform, extensible, and independent routing library powered by Ktor. Create routing independently and extend it to what you need.

Core module

The core module is the Ktor routing engine modified to be "server" and "client". It is abstract and ready to extend. Using core module you can:

val router = routing {
    route(path = "/hello") {
        handle {
          // Handle the call to the routing "/login"
        }
    }
}

// And to call login routing...
router.call(uri = "/hello")

Keep reading to see what kotlin routing can provide

Defining routes

Based on Ktor Routing

All route definition provided by Ktor Routing is supported by Kotlin Routing.

val router = routing {
    route("/hello", RouteMethod.Empty) {
        handle {
            // Well, there is no respond* because we are not a server library
        }
    }
}

Shortly version

val router = routing {
    handle("/hello") {
        // Handle any call to the "/hello" route
    }
}

It's also possible to define a name to navigate instead of using the path value.

route(path = "/hello", name = "hello") {
    // ...
}

Type-safe navigation is also supported.

Getting route detail

Use call inside of handle block to get all route details available

handle(path = "/path") {
    val application = call.application
    val routeMethod = call.routeMethod
    val name = call.name
    val uri = call.uri
    val attributes = call.attributes
    val parameters = call.parameters // routing parameters (see Routing routes) plus query parameters when provided
}

Redirecting route

You can redirect from anywhere with an ApplicationCall:

handle(...) {
    call.redirectToPath(path = "/path-destination")
    // or
    call.redirectToName(name = "destination-name")
}

Routing routes

val router = routing {
    // ...
}

// Routing by uri
router.call(uri = "/path")

// Routing by uri with parameters
router.call(uri = "/path", parameters = parametersOf("number", listOf("123")))

// Routing by name
router.call(name = "name")

// Routing by name with parameters
router.call(name = "name", parameters = parametersOf("number", listOf("123")))

// Routing by a route method
router.call(uri = "/path", routeMethod = RouteMethod("custom name"))

Type-safe routing (resources module)

Based on Ktor Type-safe routing

val router = routing {
    install(Resources)

    handle<Articles> {
        // handle any call to Articles
    }
}

// And do:
router.call(Articles())

Exception routing handler (status-pages module)

Based on Ktor Status pages

val router = routing {
    install(StatusPages) {
        // Catch any exception (change to be specific if you need)
        exception<Throwable> { call, cause ->
            // exception handled
        }
    }

    handle(path = "/hello") {
        throw IllegalArgumentException("simulating an exception thrown on routing")
    }
}

// And to simulate
router.call(uri = "/hello")

Events module

An extension module to help working with events, using name instead of paths. You can use it to sent or connect your event based system: Analytics, MVI, etc.

val router = routing {
    event(name = "event_name") {
        // Handle your event here
        call.redirectToEvent(name = "other_event_name") // If you need redirect from one to another
    }
}

// To emit events call:
router.emitEvent(
    name = "event_name",
    parameters = parametersOf(...),
)

Nested Routing

With nested routing you can connect one Routing to another. It is good for projects that have routes on demand as Android Dynamic Feature that each module has your own navigation and are loaded at runtime. Checkout RoutingTest for more usages.

val parent = routing { }

val router = routing(
    rootPath = "/child",
    parent = parent,
) { }

Other modules to interest

Limitations

  • Any type-safe behavior combined with Nested routing does not support navigation from parent to child using the Type. You have to use path routing.
@Resource("/endpoint")
class Endpoint

val parent = routing { }

val router = routing(
    rootPath = "/child",
    parent = parent,
) {
    handle<Endpoint> {
        // ...
    }
}

// IT WORKS
router.call(Endpoint())

// IT DOES NOT WORK
parent.call(Endpoint())

// IT WORKS
parent.call(uri = "/child/endpoint")

Integration modules

These kind of modules are inspirations showing how do you own integration with the target framework.

Compose Routing (compose module)

This module is just for study or simple compose application. I recommend use Voyager module for more robust application.

Are you using Jetpack or Multiplatform Compose Runtime only? This module is for you. Easily route any composable you have just doing:

val routing = routing {
    composable(path = "/login") {
        // Your composable or any compose behavior here 
        call.popped // True if it was popped
        val result = call.popResult<T>() // To get the pop result after pop one composable
        val typedValue = call.resource<T>() // To get the type-safe navigated value
    }
}

@Composable
fun MyComposeApp() {
    Routing(routing = routing, initial = {
        // Initial content
        LocalRouting.current // Available inside compositions to do routing
    })
}

// And in any place that have the routing instance call:
routing.push(path = "/login")

val lastPoppedCall = routing.poppedCall() // The call that was popped after call `routing.pop()`
val result = lastPoppedCall?.popResult<T>() // To get the result after call `routing.pop(result = T)`

Compose Animation (compose-animation module)

This module is just for study or simple compose application. I recommend use Voyager module for more robust application. At the moment Compose Multiplatform Animation (not the routing module) has limited targets and it is not available to all routing targets

Are you using Jetpack or Multiplatform Compose that requires animation? This module is for you. Easily route any composable you have just doing:

val routing = routing { 
    // You can override global behaviors to each composable
    composable(
        path = "/login",
        enterTransition = {...},
        exitTransition = {...},
        popEnterTransition = {...},
        popExitTransition = {...},
    ) {
        // Your composable or any compose behavior here 
        call.animatedVisibilityScope // If you need do something during animation
    }
}

@Composable
fun MyComposeApp() {
    Routing(
        routing = routing,
        enterTransition = {...},    // on enter next composable in forward direction
        exitTransition = {...},     // on exit current composable in forward direction
        popEnterTransition = {...}, // on enter previous composable in backward direction
        popExitTransition = {...},  // on exit current composable in backward direction
        initial = {
        // Initial animated content
    })
}

// And in any place that have the routing instance call:
routing.push(path = "/login")

The kotlin-routing author is not expert in Compose Animation. So, yes, the behavior here is close to Navigation with Compose and will help people that come from it.

Web Routing (javascript module still in development)

Are you building a DOM application? This module is for you.

val routing = routing {
    jsRoute(path = "/page1") {
        // create and return your DOM Element
    }
    jsRoute(path = "/page2") {
        // create and return your DOM Element
    }
}

fun main() {
    render(
        routing = routing,
        root = document.getElementById("root") ?: document.create.div(),
        initial = document.create.h1 {
            +"I am the initial content"
            onClickFunction = {
                routing.push(path = "/page1")
            }
        }
    )
}

// And in any place that have the routing instance call:
routing.push(path = "/page2")

Voyager Routing (voyager module)

Are you building a Voyager application and need routing support? This module is for you.

val routing = routing { 
    screen(path = "/page1") {
        // create and return your Screen instance
    }
}

@Composable
fun App() {
    VoyagerRouting(
        routing = routing,
        initialScreen = SplashScreen() // The first screen rendered
        ... // Any other Voyager related config
    )
}

// And in any place that have the routing instance call:
routing.push(path = "/page1")

Voyager is a screen based library. So the result put in a pop call is passed to the screen and not to the composition. And here it is different from compose module. To get the result after a pop call do the previous screen implement VoyagerRoutingPopResult<T>. Its onResult function will be called on any successfully pop() or popUntil() to the previous screen.