A Kotlin Multiplatform-first deep-linking SDK. One route table for every entry point — App Links, Universal Links, custom schemes, push notifications, QR — so you think in routes, not platform-specific plumbing.
Status: 0.1.0 is live on Maven Central. Core + runtime + annotations/KSP
processor + testing module + both platform adapters + Compose Navigation adapter +
deferred-deep-link SPI (Android Install Referrer provider), all green: full Gradle
build incl. iOS-simulator and Robolectric suites, plus the KSP end-to-end sample
(12 checks). Full build log in ISSUES.md.
The flagship DX: annotate a function, add one line at startup — done.
@DeepLink("/product/{id}")
fun openProduct(id: String, color: String? = null) { ... } // {id} bound by name; color from ?color=
@DeepLink("/user/{id:int}") // typed pattern ⇒ Int parameter, checked at BUILD time
fun openUser(id: Int, ctx: DeepLinkContext) { ... } // ask for the context if you want source/uri
val deepLink = DeepLink {
schemes("myapp"); hosts("link.example.com")
includeGenerated() // ← everything annotated, registered
}Invalid patterns, missing parameters, and type mismatches ({id:int} vs String) fail
at compile time with exact file:line errors — not in production. On Android, startup is
one line: DeepLinkAndroid.install(application, deepLink). See
docs/annotation-quickstart.md.
val deepLink = DeepLink {
schemes("myapp")
hosts("link.example.com")
route("/product/{id}") { ctx ->
navigator.openProduct(ctx.params.require("id"), color = ctx.params["color"])
}
route("/user/{id:int}/orders/{orderId:uuid}") { ctx -> /* typed params */ }
route("/docs/**") { ctx -> openDocs(ctx.params["tail"]) }
fallback { navigator.openHome() }
listener { event -> analytics.track(event) }
}
// Same table matches both:
deepLink.handle("myapp://product/123?color=red")
deepLink.handle("https://link.example.com/product/123?color=red")- URI parser (
DeepLinkUri) — pure Kotlin, no platform URI classes; percent-decoding, multi-value query params, fragments, ports; malformed input returns a result, never throws. - Custom-scheme normalization —
myapp://product/1andhttps://host/product/1match the same/product/{id}pattern (thehost-vs-first-segment ambiguity is handled for you). - Route patterns —
{param}, typed{id:int|long|bool|uuid},*single-segment and**tail wildcards; invalid patterns fail at registration, not at dispatch. - Specificity-ordered matching — static beats typed param beats param beats wildcards; registration order never silently changes behavior.
- Contained failures — handler exceptions become
DispatchResult.HandlerError; a bad analytics listener can never break dispatch. - DX tooling —
simulate()test harness,table.lint()shadowed-route detection,Unmatched.nearestsuggestions for debugging.
| Module | What it is | Status |
|---|---|---|
deeplink-core |
URI parser, route patterns, matcher, lint — zero deps | ✅ tested (jvm + iOS sim) |
deeplink-runtime |
DeepLink { } facade, events, cold-start buffer, deferred-link SPI |
✅ tested (jvm + iOS sim) |
deeplink-annotations |
@DeepLink, @DeepLinkQuery |
✅ tested via processor |
deeplink-processor |
KSP processor: compile-time validated route generation | ✅ tested + KSP e2e sample |
deeplink-testing |
simulation() assertions, assertNoLintErrors() |
✅ tested (jvm + iOS sim) |
deeplink-android |
DeepLinkAndroid.install/attach, Intent adapters |
✅ AAR + lint + Robolectric |
deeplink-coroutines |
events: Flow<DeepLinkEvent> adapter |
✅ builds (jvm + iOS) |
deeplink-ios |
DeepLinkIos.shared.handle(url:/userActivity:) adapters |
✅ compiles, 3 iOS targets |
deeplink-compose-navigation |
NavBinding + DeepLinkNavigation for Compose Navigation |
✅ AAR (docs/compose-navigation.md) |
deeplink-install-referrer |
Deterministic deferred deep links via Play Install Referrer | ✅ AAR + parser tests |
All artifacts are on Maven Central (version 0.1.0) under the group in
gradle.properties (GROUP, currently io.github.ankitkumar-os.deeplink — swaps to a
dev.<domain>.deeplink group in one line once the domain is chosen; artifact ids and
dev.deeplink.* packages never change). No extra repository needed.
The split follows the architecture: the routing engine goes in common code — that's the point of the SDK — and each platform module only wires its OS entry points.
// shared/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.ksp) // only if using @DeepLink annotations
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.ankitkumar-os.deeplink:deeplink-runtime:0.1.0") // pulls deeplink-core
implementation("io.github.ankitkumar-os.deeplink:deeplink-annotations:0.1.0") // @DeepLink / @DeepLinkQuery
// optional: events as Flow<DeepLinkEvent>
implementation("io.github.ankitkumar-os.deeplink:deeplink-coroutines:0.1.0")
}
androidMain.dependencies {
implementation("io.github.ankitkumar-os.deeplink:deeplink-android:0.1.0")
// optional: deterministic deferred deep links via Play Install Referrer
implementation("io.github.ankitkumar-os.deeplink:deeplink-install-referrer:0.1.0")
}
iosMain.dependencies {
implementation("io.github.ankitkumar-os.deeplink:deeplink-ios:0.1.0")
}
commonTest.dependencies {
implementation("io.github.ankitkumar-os.deeplink:deeplink-testing:0.1.0")
}
}
}
// The KSP processor attaches per compilation target, not per source set:
dependencies {
add("kspAndroid", "io.github.ankitkumar-os.deeplink:deeplink-processor:0.1.0")
add("kspIosArm64", "io.github.ankitkumar-os.deeplink:deeplink-processor:0.1.0")
add("kspIosSimulatorArm64", "io.github.ankitkumar-os.deeplink:deeplink-processor:0.1.0")
}// shared/src/commonMain/kotlin/.../Links.kt
object Links {
val deepLink = DeepLink {
schemes("myapp"); hosts("link.example.com")
includeGenerated() // annotation-generated routes, or route("/...") { } DSL
fallback { navigator.openHome() }
}
}class App : Application() {
override fun onCreate() {
super.onCreate()
DeepLinkAndroid.install(this, Links.deepLink) // the whole integration
// deferred deep links (optional):
Links.deepLink.resolveDeferred(InstallReferrerDeferredSource(this))
}
}Compose Navigation apps add deeplink-compose-navigation to the app module, pass
markReady = false to install, and bind inside setContent — see
docs/compose-navigation.md. Manifest intent filters: docs/android-integration.md.
// AppDelegate / SceneDelegate — or SwiftUI's .onOpenURL:
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
DeepLinkIos.shared.handle(url: url) // custom schemes
}
func application(_ app: UIApplication, continue activity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
DeepLinkIos.shared.handle(userActivity: activity) // Universal Links
}with DeepLinkIos.install(deepLink: Links.deepLink) called at startup — AASA checklist
and SwiftUI wiring in docs/ios-integration.md.
Everything goes in the one app module: implementation(...) for runtime + annotations +
android (+ compose-navigation / install-referrer as needed) and
ksp("io.github.ankitkumar-os.deeplink:deeplink-processor:0.1.0").
./gradlew build # everything: all targets + all test suites
./gradlew :samples:annotated-sample:run # KSP end-to-end ("SMOKE OK — 12 checks")
./gradlew publishToMavenLocal # all 10 modules into ~/.m2
Toolchain: Kotlin 2.3.21 / KSP 2.3.11 / AGP 8.13.2 on Gradle 9.3 (JDK 17+). iOS
targets need a Mac with Xcode; build runs the common suites on an iOS simulator.
Publishing to Maven Central goes through the vanniktech plugin — credentials and the
signing key live in ~/.gradle/gradle.properties (see the root build.gradle.kts
comment), and signing is skipped automatically when no key is configured.
The tools/*.sh scripts are the legacy no-network sandbox harness (kotlinc-direct,
stub-verified Android/iOS); on a dev machine the Gradle build is the source of truth.
See docs/technical-design-spec.md for the API design and semantics, and
docs/roadmap-critique.md for sequencing rationale. Integration guides:
docs/annotation-quickstart.md (the @DeepLink annotation layer),
docs/android-integration.md (one-line DeepLinkAndroid.install, manifest filters,
assetlinks.json checklist, push extras, cold-start buffering),
docs/ios-integration.md (DeepLinkIos.install + SwiftUI/SceneDelegate/AppDelegate
wiring, AASA checklist), docs/compose-navigation.md (the NavBinding seam between
app-scope handlers and composition-scope NavControllers), and
docs/deferred-deep-links.md (the provider SPI + Play Install Referrer source).