DEV Community

Cover image for Kotlin Routing - routing everything
Thiago Santos
Thiago Santos

Posted on • Updated on

Kotlin Routing - routing everything

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.

generated with Summaryze Forem 🌱

The Kotlin Routing

val router = routing {        // 1
    handle(path = "/hello") { // 2
        // ...
    }
}

router.call(uri = "/hello")  // 3
Enter fullscreen mode Exit fullscreen mode

This code represents a basic route system, which facilitates the registration, subscription, and invocation of routes. Here's a breakdown of each aspect:

  1. 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.

  2. 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.

  3. 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")
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")))
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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)
                }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)