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
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.
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.
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
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.
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.
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.
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.
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.
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 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:
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:
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)
}
}
}
}