Kotlin Multiplatform (KMP) has become a reality for any developer working with Kotlin, particularly those focused on Android development. KMP has grown significantly since its beta phase and is now stable and production-ready, as announced here.
In parallel, JetBrains has introduced several impressive KMP frameworks, among which is Ktor. Ktor facilitates the creation of asynchronous client and server applications.
Ktor boasts an excellent routing structure on its server engine, supporting all aspects related to a URI.
As a mobile developer, I've noticed that in almost every mobile framework, navigation tends to align with a routed version that operates with paths, parameters, and other URI-related elements.
It seems that the Ktor team may not have considered that its routing system could be utilized beyond the confines of client or server networks.
With the increasing use cases of KMP and developers migrating or initiating projects with KMP, the absence of a routing system presents an opportunity to develop 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
val router = routing { // 1
handle(path = "/hello") { // 2
// ...
}
}
router.call(uri = "/hello") // 3
This code represents a basic route system, which facilitates the registration, subscription, and invocation of routes. Here's a breakdown of each aspect:
Creating the route system to register or call routes: This involves setting up the infrastructure to manage routes. It likely include functions for registering routes along with their associated handlers.
Subscribing to a specific route to execute something related: This step allows components to subscribe to specific routes. When a route is invoked, the subscribed component (or handler) will be executed. This mechanism enables modularization and decoupling of code, as different parts of the application can respond to different routes without needing direct dependencies on each other.
Invoking a specific route to be executed: This is the action of triggering the execution of a specific route. It could be initiated by user interaction, incoming requests, or other events in the application. Once invoked, the associated handler or function subscribed to that route will be executed, carrying out the necessary actions or logic.
While this structure may not introduce anything groundbreaking to those familiar with route systems, it provides a foundational framework that can be utilized across any Kotlin Multiplatform (KMP) target. It ensures consistency and compatibility across different platforms, making it easier to develop and maintain applications that leverage Kotlin's routing capabilities.
Named Routing
If working with paths isn't your preference, Kotlin routing also offers the flexibility to route using names. This means that instead of specifying routes based on their paths, you can assign them meaningful names and use these names to navigate between different parts of your application.
Routing by names can offer advantages such as increased readability and abstraction, especially in complex applications where routes may change or be composed dynamically. Additionally, it can simplify route management by decoupling route definitions from specific path details, making it easier to refactor and maintain your codebase.
val router = routing {
handle(path = "/hello", name = "hello") {
// ...
}
}
router.call(name = "hello")
Routing by Method
If you prefer not to create additional paths for the same behavior, you can distinguish between routes using methods. This approach allows you to define different methods for handling the same route based.
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
Grouping routes using sub-paths is a common practice to organize and manage related routes within your application. This approach allows you to define a hierarchical structure for your routes, making it easier to understand and maintain your routing logic, especially in larger applications with many 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
}
}
}
}
Path Pattern
Path pattern allow you to create routes that adapt to changing requirements or conditions dynamically.
val router = routing {
handle(path = "/hello/{id}") {
// ...
}
handle(path = "/hello/*") {
// ...
}
handle(path = "/hello/{...}") {
// ...
}
handle(path = "/hello/{param...}") {
// ...
}
handle(regex = Regex("/.+/hello")) {
// ...
}
}
Query parameters are automatically parsed and made available to route handlers without requiring any additional setup.
Checkout the ktor path pattern.
Route Details
When dealing with dynamic routes and behaviors, it's essential to have a clear understanding of the incoming call to determine how to handle it appropriately. This includes not only the route itself but also any additional parameters.
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
Ktor's Parameters
data structure plays a crucial role in handling dynamic information passed through URIs. When working with dynamic routes or external routes that contain additional information, Kotlin Routing captures and organizes these values within the call.parameters
object.
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
Sometimes redirecting from one route to another is a common requirement. Redirects are used for guiding users to a different path or name.
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're not required to declare all routes during the creation of the Routing
instance. Instead, you can dynamically subscribe and unsubscribe to routes at any time during the application's lifecycle.
val router = routing {}
router.handle(path = "/hello", name = "hello") {
// ...
}
router.unregisterNamed(name = "hello")
router.unregisterPath(path = "/hello")
Nested Routing
Nested routing can be a powerful tool for organizing and managing routing flows in multi-module projects or applications with dynamic feature modules. By nesting routing configurations, you can create a hierarchical structure that allows each module or feature to define its own internal routing flow while still being connected to a parent routing flow.
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")
Conclusion
These are just a few examples of behaviors that you can create using Kotlin Routing within a Kotlin Multiplatform (KMP) project. With its flexibility and extensibility, Kotlin Routing opens up a wide range of possibilities for building sophisticated routing systems tailored to your specific application requirements.:
- 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
Kotlin Routing can serve as a central component for connecting navigation across different platforms in a multi-platform project. Here's how you can achieve this:
// 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
}
By leveraging Kotlin Routing and KMP, you can create a unified navigation system that abstracts away platform-specific details and promotes code sharing and reusability across Android, iOS, and web platforms. This approach simplifies maintenance and reduces the risk of inconsistencies or divergence in navigation logic between platforms.
Bonus 2 - Deeplinks
Deep linking allows users to navigate directly to specific content or features within your application from a URI (Uniform Resource Identifier). While deep links are supported by default as standard URIs, handling them at the platform entry point is crucial for directing users to the appropriate screen or functionality within your app.
// 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)
}
}
}
}
Top comments (0)