Kotlin offers multiple options for URL handling: Java's java.net.URI for JVM/server code, Android's Uri for Android apps, and Ktor's Url for multiplatform projects.
Key Takeaways
- 1Use java.net.URI for JVM/server Kotlin applications
- 2Use android.net.Uri for Android-specific code
- 3Ktor provides Url class for multiplatform HTTP clients
- 4Kotlin extension functions simplify URL manipulation
- 5All options provide query parameter parsing capabilities
“Kotlin runs on the JVM and has full interoperability with Java, including the ability to use Java libraries like java.net.URI. For Android development, android.net.Uri provides additional convenience methods for URL parsing.”
Using java.net.URI (JVM)
For JVM-based Kotlin (server-side, desktop), you can use Java's URI class directly. Kotlin's Java interop makes this seamless, though you'll want to add extension functions for more idiomatic code.
import java.net.URI
import java.net.URLEncoder
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
fun main() {
val uri = URI("https://user:pass@example.com:8080/path/page?query=value#fragment")
println("Scheme: ${uri.scheme}") // https
println("Host: ${uri.host}") // example.com
println("Port: ${uri.port}") // 8080
println("Path: ${uri.path}") // /path/page
println("Query: ${uri.query}") // query=value
println("Fragment: ${uri.fragment}") // fragment
println("User Info: ${uri.userInfo}") // user:pass
// Raw vs decoded
val encoded = URI("https://example.com/path?q=hello%20world")
println("Raw Query: ${encoded.rawQuery}") // q=hello%20world
println("Query: ${encoded.query}") // q=hello world
}Kotlin's string templates (\$) make URL component access concise. Note the same raw vs decoded distinction from Java: rawQuery keeps percent-encoding, query decodes it.
Parsing Query Parameters
Kotlin's extension functions let you add query parsing directly to URI. This is more idiomatic than utility classes.
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
// Extension function to parse query parameters
fun URI.queryParameters(): Map<String, List<String>> {
if (query.isNullOrEmpty()) return emptyMap()
return query.split("&")
.map { param ->
val parts = param.split("=", limit = 2)
val key = URLDecoder.decode(parts[0], StandardCharsets.UTF_8)
val value = if (parts.size > 1) {
URLDecoder.decode(parts[1], StandardCharsets.UTF_8)
} else ""
key to value
}
.groupBy({ it.first }, { it.second })
}
// Single value accessor
fun URI.queryParam(name: String): String? = queryParameters()[name]?.firstOrNull()
fun main() {
val uri = URI("https://shop.com/search?category=books&tag=kotlin&tag=android")
val params = uri.queryParameters()
println(params)
// {category=[books], tag=[kotlin, android]}
println(uri.queryParam("category")) // books
println(params["tag"]) // [kotlin, android]
}The extension function parses the query string using split(), decodes each part, and groups by key. The groupBy function collects all values for duplicate keys into lists.
Building URLs
Kotlin's DSL capabilities let you create fluent builders that feel natural to use.
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
// URL builder with DSL
class UrlBuilder(private var base: String) {
private val queryParams = mutableListOf<Pair<String, String>>()
private var fragment: String? = null
fun param(key: String, value: Any): UrlBuilder {
queryParams.add(key to value.toString())
return this
}
fun fragment(value: String): UrlBuilder {
fragment = value
return this
}
fun build(): String {
val query = if (queryParams.isNotEmpty()) {
queryParams.joinToString("&") { (key, value) ->
"${encode(key)}=${encode(value)}"
}
} else null
return buildString {
append(base)
query?.let { append("?").append(it) }
fragment?.let { append("#").append(it) }
}
}
private fun encode(value: String) =
URLEncoder.encode(value, StandardCharsets.UTF_8)
}
fun url(base: String, block: UrlBuilder.() -> Unit = {}): String {
return UrlBuilder(base).apply(block).build()
}
fun main() {
val apiUrl = url("https://api.example.com/search") {
param("q", "kotlin programming")
param("page", 1)
param("sort", "relevance")
fragment("results")
}
println(apiUrl)
// https://api.example.com/search?q=kotlin+programming&page=1&sort=relevance#results
}The url() function with a trailing lambda creates a clean DSL. The apply() scope function lets you configure the builder inline. This pattern is common in Kotlin APIs.
Android Uri
For Android apps, the android.net.Uri class provides better integration with Android APIs and convenient methods for query parameter access.
import android.net.Uri
// Parse URL
val uri = Uri.parse("https://example.com/path?q=test&page=1#section")
println(uri.scheme) // https
println(uri.host) // example.com
println(uri.path) // /path
println(uri.query) // q=test&page=1
println(uri.fragment) // section
// Get query parameters
println(uri.getQueryParameter("q")) // test
println(uri.getQueryParameter("page")) // 1
// Get all values for a key
val tags = uri.getQueryParameters("tag")
// Build URL with Uri.Builder
val newUri = Uri.Builder()
.scheme("https")
.authority("api.example.com")
.path("/v2/users")
.appendQueryParameter("page", "1")
.appendQueryParameter("limit", "20")
.build()
println(newUri)
// https://api.example.com/v2/users?page=1&limit=20
// Modify existing URL
val modified = uri.buildUpon()
.appendQueryParameter("new", "param")
.fragment("updated")
.build()Android's Uri provides getQueryParameter() directly: no manual parsing needed. The Uri.Builder class follows the builder pattern, and buildUpon() creates a builder from an existing URI for modifications.
Modifying URLs
These extension functions add convenient parameter manipulation to Android's Uri class.
import android.net.Uri
// Extension functions for Android Uri
fun Uri.withQueryParam(key: String, value: String): Uri {
return buildUpon()
.clearQuery()
.apply {
queryParameterNames.forEach { name ->
if (name != key) {
getQueryParameters(name).forEach { v ->
appendQueryParameter(name, v)
}
}
}
appendQueryParameter(key, value)
}
.build()
}
fun Uri.withoutQueryParam(key: String): Uri {
return buildUpon()
.clearQuery()
.apply {
queryParameterNames.forEach { name ->
if (name != key) {
getQueryParameters(name).forEach { v ->
appendQueryParameter(name, v)
}
}
}
}
.build()
}
fun Uri.withQueryParams(params: Map<String, String>): Uri {
return buildUpon()
.clearQuery()
.apply {
// Keep existing params not being replaced
queryParameterNames.forEach { name ->
if (name !in params) {
getQueryParameters(name).forEach { v ->
appendQueryParameter(name, v)
}
}
}
// Add new/updated params
params.forEach { (key, value) ->
appendQueryParameter(key, value)
}
}
.build()
}
// Usage
val uri = Uri.parse("https://api.com/search?q=kotlin&page=1")
val updated = uri.withQueryParam("page", "2")
// https://api.com/search?q=kotlin&page=2
val removed = uri.withoutQueryParam("page")
// https://api.com/search?q=kotlin
val multi = uri.withQueryParams(mapOf("sort" to "date", "limit" to "20"))
// https://api.com/search?q=kotlin&page=1&sort=date&limit=20The clearQuery() method removes all existing parameters so you can rebuild from scratch. The extension functions return new Uri instances since Uri is immutable.
Ktor Url (Multiplatform)
For Kotlin Multiplatform projects, Ktor's Url and URLBuilder classes work across all platforms: JVM, Android, iOS, and JavaScript.
import io.ktor.http.*
// Parse URL
val url = Url("https://api.example.com/search?q=kotlin&page=1")
println(url.protocol) // https
println(url.host) // api.example.com
println(url.port) // 443
println(url.encodedPath) // /search
println(url.parameters["q"]) // kotlin
// Build URL with URLBuilder
val newUrl = URLBuilder().apply {
protocol = URLProtocol.HTTPS
host = "api.example.com"
path("v2", "users")
parameters.append("page", "1")
parameters.append("limit", "20")
}.build()
println(newUrl)
// https://api.example.com/v2/users?page=1&limit=20
// Clone and modify
val modified = URLBuilder(url).apply {
parameters["page"] = "2"
parameters.append("sort", "name")
}.build()
// In Ktor HTTP client
import io.ktor.client.*
import io.ktor.client.request.*
suspend fun fetchUsers(client: HttpClient, page: Int) {
val response = client.get("https://api.example.com/users") {
parameter("page", page)
parameter("limit", 20)
}
}Ktor's URL API is designed for HTTP clients, so it integrates naturally with request building. The parameters object supports both direct assignment and append() for multiple values. Use URLBuilder(existingUrl) to modify an existing URL.
Deep Link Handling (Android)
Android deep links are URLs that open your app. Here's how to parse them into typed navigation targets using Kotlin's sealed classes.
import android.net.Uri
sealed class DeepLink {
data class Product(val id: String) : DeepLink()
data class Category(val name: String, val page: Int?) : DeepLink()
data class Search(val query: String) : DeepLink()
object Unknown : DeepLink()
}
object DeepLinkParser {
fun parse(uri: Uri): DeepLink {
val pathSegments = uri.pathSegments
return when (pathSegments.firstOrNull()) {
"product" -> {
val id = pathSegments.getOrNull(1) ?: return DeepLink.Unknown
DeepLink.Product(id)
}
"category" -> {
val name = pathSegments.getOrNull(1) ?: return DeepLink.Unknown
val page = uri.getQueryParameter("page")?.toIntOrNull()
DeepLink.Category(name, page)
}
"search" -> {
val query = uri.getQueryParameter("q") ?: return DeepLink.Unknown
DeepLink.Search(query)
}
else -> DeepLink.Unknown
}
}
}
// In Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.data?.let { uri ->
when (val deepLink = DeepLinkParser.parse(uri)) {
is DeepLink.Product -> navigateToProduct(deepLink.id)
is DeepLink.Category -> navigateToCategory(deepLink.name, deepLink.page)
is DeepLink.Search -> performSearch(deepLink.query)
DeepLink.Unknown -> showHome()
}
}
}Sealed classes are perfect for deep links: they enumerate all possible navigation targets and the compiler ensures you handle every case. The when expression with is checks provides smart casting to access associated data.
URL Validation
This validator object demonstrates Kotlin idioms: companion-object-like syntax, null-safe calls, and the in operator for set membership.
import java.net.URI
import java.net.URISyntaxException
object UrlValidator {
private val ALLOWED_SCHEMES = setOf("http", "https")
fun isValidHttpUrl(url: String): Boolean {
return try {
val uri = URI(url)
uri.scheme?.lowercase() in ALLOWED_SCHEMES &&
!uri.host.isNullOrEmpty()
} catch (e: URISyntaxException) {
false
}
}
fun isSafeRedirect(url: String, allowedHosts: Set<String>): Boolean {
return try {
val uri = URI(url)
val scheme = uri.scheme?.lowercase()
val host = uri.host?.lowercase()
scheme in ALLOWED_SCHEMES &&
host != null &&
allowedHosts.any { allowed ->
host == allowed || host.endsWith(".$allowed")
}
} catch (e: URISyntaxException) {
false
}
}
}
// Usage
println(UrlValidator.isValidHttpUrl("https://example.com")) // true
println(UrlValidator.isValidHttpUrl("javascript:alert(1)")) // false
val allowed = setOf("myapp.com", "api.myapp.com")
println(UrlValidator.isSafeRedirect("https://myapp.com/cb", allowed)) // true
println(UrlValidator.isSafeRedirect("https://evil.com/cb", allowed)) // falseThe lowercase() calls ensure case-insensitive comparison. The any() function with a lambda provides a clean way to check if a host matches any allowed host or subdomain.
The table below summarizes when to use each URL library in Kotlin projects.
| Library | Platform | Best For |
|---|---|---|
| java.net.URI | JVM, Android | Server-side Kotlin, backend |
| android.net.Uri | Android only | Android apps, deep links |
| Ktor Url | Multiplatform | Ktor HTTP client, KMM projects |
| OkHttp HttpUrl | JVM, Android | OkHttp-based networking |
For shared code in KMM projects, Ktor is the best choice. For platform-specific code, use the native option (Android Uri or java.net.URI) for better integration with platform APIs.