Kotlin Routing – routing everything

RMAG news

Kotlin multiplatform (KMP) is a reality for any developer working with Kotlin. Mainly people working with Android. KMP has incresed since it was beta and now is stable and production-ready.

In paralel Jetbrains has created a lot of KMP frameworks that are awesome. One of them is Ktor that helps “create asynchronous client and server applications.”

Ktor has an awesome routing structure on your server engine and supporting all the whole things related to an URI.

As a mobile developer I saw that in any mobile framework the navigation almost scale to a routed version that works with path, parameters, all thing related to URI.

I think that the Ktor team has not thought that your routing system could be exported to scenarios out of the Client or Server networks.

KMP use cases increasing, developers migrating or creating your projects to KMP, no routing system or navigation available to KMP (Except Voyager), that was the opportunity to create one.

The Kotlin Routing

Named Routing

Routing by Method

Group Routes

Path Pattern

Route Details

Working with Parameters

Redirect Routes

On demand handling

Nested Routing

Conclusion

Bonus 1 – Routing Natives

Bonus 2 – Deeplinks

generated with Summaryze Forem 🌱

The Kotlin Routing

I’m not good with names. So I put kotlin in anything to have meaning on it

val router = routing { // 1
handle(path = “/hello”) { // 2
// …
}
}

router.call(uri = “/hello”) // 3

This is the simplest code of a route system. What it means?

Creating the route system to register or call routes
Subscribe to a specific route to execute something related
Invoke a specific route to be executed

That shows nothing new to whom already works with a route system. But this structure works in any KMP target.
Let’s checkout what more kotlin routing has.

Named Routing

Don’t like working with paths? You can route using names.

val router = routing {
handle(path = “/hello”, name = “hello”) {
// …
}
}

router.call(name = “hello”)

Routing by Method

Don’t want to create another path to the same behavior? You can distinguish using methods.

val router = routing {
handle(path = “/hello”, method = RouteMethod.Push) {
// …
}
handle(path = “/hello”, method = RouteMethod(“your method”)) {
// …
}
}

router.call(uri = “/hello”, method = RouteMethod.Push)
// or
router.call(uri = “/hello”, method = RouteMethod(“your method”))

Group Routes

Do you have sub-paths? You can group your routes

val router = routing {
route(path = “/parent”) {
handle {
// invoked on call to /parent
}
handle(path = “/child”) {
// invoked on call to /parent/child
}
route(path = “/brother”) {
handle(path = “/nephew”) {
// invoked on call to /parent/brother/nephew
}
}
}
}

Until now, there is no limitation to inner routes.

Path Pattern

It means you can create dynamic routing structure instead of having static ways.

val router = routing {
handle(path = “/hello/{id}”) {
// …
}

handle(path = “/hello/*”) {
// …
}

handle(path = “/hello/{…}”) {
// …
}

handle(path = “/hello/{param…}”) {
// …
}

handle(regex = Regex(“/.+/hello”)) {
// …
}
}

Query parameters are handled by default. You don’t need any setup for them.

Checkout the ktor path pattern documentation that explains how it works.

Route Details

Having dynamic routes and behaviors we need to know what is comming when the route is invoked.

val router = routing {
handle(path = “/hello”) {
val application = call.application
val routeMethod = call.routeMethod
val name = call.name
val uri = call.uri
val attributes = call.attributes
val parameters = call.parameters
}
}

router.call(uri = “/hello”)

Working with Parameters

Sometimes we need to provide dynamic info to the route or handle an external route that contains other infos. There are some ways to provide dynamic info using URI and all the values are captured and put into the call.parameters.
Parameters is a Ktor data structure for associating a String with a List of Strings. All values in Parameters are String.

val router = routing {
handle(path = “/with/{id}”, name = “with”) {
val parameters = call.parameters
// {“id”: [“1234”]}
}

handle(path = “/query”, name = “query”) {
val parameters = call.parameters
// {“color”: [“red”], “tag”: [“kotlin”, “routing”]}
}

handle(path = “/all/{id}”, name = “all”) {
val parameters = call.parameters
// {“id”: [“1234”], “color”: [“red”], “tag”: [“kotlin”, “routing”]}
}
}

router.call(uri = “/with/1234”)
router.call(uri = “/query?color=red&tag=kotlin&tag=routing”)
router.call(uri = “/all/1234?color=red&tag=kotlin&tag=routing”)

// same call using names
// on named routing you have to provide each parameter

router.call(name = “with”, parameters = parametersOf(“id”, “1234”))
router.call(name = “query”, parameters = parametersOf(“color” to listOf(“red”), “tag” to listOf(“kotlin”, “routing”)))
router.call(name = “all”, parameters = parametersOf(“id” to listOf(“1234”), “color” to listOf(“red”), “tag” to listOf(“kotlin”, “routing”)))

Redirect Routes

Maybe you need to redirect from one route to another.

val router = routing {
handle(path = “/hello”) {
call.redirectToPath(path = “/other-path”)
// or
call.redirectToName(name = “other-name”)
}
}

router.call(uri = “/hello”)

On demand handling

You don’t need to declare all routes on the Routing creation. Subscribe and Unsubscribe to a route is available anytime with a Routing instance.

val router = routing {}

router.handle(path = “/hello”, name = “hello”) {
// …
}

router.unregisterNamed(name = “hello”)
router.unregisterPath(path = “/hello”)

Nested Routing

Some projects are multi-module or have features installed on demand (Dynamic Features in the Android world).
Each project/module can have your internal routing flow. Maybe connected to a parent routing flow. Nested routing provide this connection on demand.

val parent = routing { }

val featureARouting = routing(
rootPath = “/feature-a”,
parent = parent,
) { }

val featureBRouting = routing(
rootPath = “/feature-b”,
parent = parent,
) { }

// Try to route internaly on feature A module.
// If not found, look up the route on parent
// It has no access to feature B routes
featureARouting.call(...)

// Same behavior as A above.
// And it has no access to feature A routes
featureBRouting.call(...)

// Routing from parent directly to a route inside feature A
parent.call(path = “/feature-a/something”)

// Routing from parent directly to a route inside feature B
parent.call(path = “/feature-b/something”)

Nested routing behaviors as Group Routes section tranforming each Routing child in a Route that can be invoked.

Conclusion

Kotlin Routing bring all the power provided by the Ktor server structure to KMP world in a way that can be used in any context that needs a routing system.
Samples in the article are generics and simple to show that you can extend it to any situation you have.
There are a lot of behaviors that you can create from that. Some of them are already provided in the repository as:

Type-Safe Routing
Handling Exception
Event routing
Session, Authentication and Authorization
Integration with external frameworks (Android Activity, Compose Multiplatform, Javascript DOM, UIKit UIViewController, Voyager)

All ktor plugins structure still working in the Kotlin Routing and your can creates your own.

More articles about other modules and integrations come soon.

Bonus 1 – Routing Natives

Think in a scenario that your Android Project (no KMP) are using its own navigation (Jetpack Navigation, Your custom navigation, etc.), iOS Project has its own navigation (XCoordinator, etc.) and web with React, Vue, etc. How can you connect them with you KMP new project? Kotlin Routing helps with that:

// commonMain

val router = routing {
// …
}

router.call(uri = “/something”)

// androidMain or a non KMP android project

router.handle(path = “/something”) {
// Start an Activity?
// Show a Fragment?
// Call an Android navigation
// You are free
}

// iosMain or a non KMP ios project

router.handle(path = “/something”) {
// Show a UIViewController?
// Call an iOS navigation
// Update a SwiftUI view
// You are free
}

// jsMain or a non KMP web project

router.handle(path = “/something”) {
// Call react navigation
// Call vue navigation
// Update the DOM
// You are free
}

Bonus 2 – Deeplinks

What about deeplinks? Deeplinks are URI and supported by default. They aren’t handled by default on each platform entrypoint. You have to provide the start point. E.g:

// commonMain
val router = routing {
handle(path = “scheme://host/path/{field}?query=q”) {
}
}

// android project
class LaunchActivity : ... {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)

// Ensure that you Routing is initialized

val action: String? = intent?.action
val data: Uri? = intent?.data

router.call(uri = data?.toString() ?: “/home”)
}
}

// ios project
import SwiftUI
import YourFrameworkHavingKotlinRouting

@main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
router.call(uri = url.absoluteString)
}
}
}
}

Leave a Reply

Your email address will not be published. Required fields are marked *