Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Universal Deep Link SDK

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 30-second integration (annotation-based)

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.

Quick look (DSL, no annotations)

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")

What Phase 1 gives you

  • 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 normalizationmyapp://product/1 and https://host/product/1 match the same /product/{id} pattern (the host-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 toolingsimulate() test harness, table.lint() shadowed-route detection, Unmatched.nearest suggestions for debugging.

Modules

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

Installing in a KMP project

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.

1. Shared module dependencies

// 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")
}

2. Define routes once, in commonMain

// 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() }
    }
}

3. Wire Android (once, in the app module)

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.

4. Wire iOS (Swift side)

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

Plain Android app (no KMP)

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").

Building

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

Design docs

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages