Skip to content
Back to skills

Kmp Deep Linking

ASecurity

Deep linking for Kotlin Multiplatform — Android App Links (Digital Asset Links), iOS Universal Links (Apple App Site Association), NavHost route integration, deep-link route parsing in commonMain, intent handling in AndroidActivity, and NSUserActivity/openURL handling in the iOS app delegate.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
ai-agentsgoswiftkotlintestinggitapi

Works with

  • api

Security analysis

A100/100

Scanned September 2, 2026

npx -y skills add ronjunevaldoz/kmp-agent-skills --skill kmp-deep-linking --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Kmp Deep Linking?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Kmp Deep Linking
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ronjunevaldoz-kmp-deep-linking/badge)](https://www.skillsdirectory.com/skills/ronjunevaldoz-kmp-deep-linking)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: kmp-deep-linking
description: >-
  Deep linking for Kotlin Multiplatform — Android App Links (Digital Asset Links),
  iOS Universal Links (Apple App Site Association), NavHost route integration,
  deep-link route parsing in commonMain, intent handling in AndroidActivity,
  and NSUserActivity/openURL handling in the iOS app delegate.
license: Apache-2.0
metadata:
  author: kmp-agent-skills
  last-updated: '2026-06-21'
  keywords:
    - deep linking
    - App Links
    - Universal Links
    - deep link
    - NavHost deep link
    - AASA
    - Digital Asset Links
    - intent filter
    - route parsing
    - deep link handling
    - openURL
    - NSUserActivity
    - KMP deep link
    - URL scheme
    - custom scheme
---

## When to Use This Skill

Use when:
- Tapping a URL in email/browser should open a specific screen in the app
- Implementing App Links (Android) or Universal Links (iOS) verified against your domain
- Parsing URL parameters and mapping them to NavHost routes
- Supporting custom URL schemes (`myapp://`) as a fallback for unverified deep links
- Testing deep link entry points with unit tests

**Trigger keywords:** deep linking, App Links, Universal Links, deep link, AASA,
Digital Asset Links, intent filter, route parsing, NavHost deep link, openURL,
NSUserActivity, deep link handling, custom URL scheme, in-app linking, KMP deep link.

**Freshness rule:** App Links verification requires a valid `assetlinks.json` hosted at
`https://<yourdomain>/.well-known/assetlinks.json`. The SHA-256 fingerprint in the file
must match the app's signing certificate. On iOS, the `apple-app-site-association` file
must be hosted at `https://<yourdomain>/.well-known/apple-app-site-association` (or at
the domain root). Verify hosting via `aasa-validator` or Android App Links Assistant
before shipping.

---

## Recommendation First

Define deep-link routes as a `DeepLink` sealed class in `commonMain`. Android and iOS
entry points (Activity intent, iOS `application:openURL:` / `application:continueUserActivity:`)
parse the URL into a `DeepLink` and pass it to the NavController. The parser lives in
`commonMain` so it can be unit tested without a device.

---

## Route definition — commonMain

```kotlin
// :core:navigation — DeepLink.kt
sealed class DeepLink {
    data class ProductDetail(val productId: String) : DeepLink()
    data class OrderStatus(val orderId: String)     : DeepLink()
    object Home                                     : DeepLink()
    data class Unknown(val url: String)             : DeepLink()
}
```

---

## URL parser — commonMain (pure, testable)

```kotlin
// :core:navigation — DeepLinkParser.kt
object DeepLinkParser {
    private const val HOST = "www.example.com"

    fun parse(url: String): DeepLink {
        // Supports both https://www.example.com/... and myapp://...
        val normalized = url
            .replace("myapp://", "https://$HOST/")
            .trimEnd('/')

        return when {
            normalized.matches(Regex(".*/$HOST/products/([^/]+)")) -> {
                val id = Regex(".*/products/([^/]+)").find(normalized)?.groupValues?.get(1)
                if (id != null) DeepLink.ProductDetail(id) else DeepLink.Unknown(url)
            }
            normalized.matches(Regex(".*/$HOST/orders/([^/]+)")) -> {
                val id = Regex(".*/orders/([^/]+)").find(normalized)?.groupValues?.get(1)
                if (id != null) DeepLink.OrderStatus(id) else DeepLink.Unknown(url)
            }
            normalized.contains("$HOST/") -> DeepLink.Home
            else -> DeepLink.Unknown(url)
        }
    }
}
```

---

## NavHost route registration

```kotlin
// :ui — AppNavHost.kt
@Composable
fun AppNavHost(
    navController: NavHostController,
    startDestination: String = Routes.Home,
) {
    NavHost(navController, startDestination = startDestination) {

        composable(
            route = Routes.ProductDetail,
            arguments = listOf(navArgument("productId") { type = NavType.StringType }),
            deepLinks = listOf(
                navDeepLink { uriPattern = "https://www.example.com/products/{productId}" },
                navDeepLink { uriPattern = "myapp://products/{productId}" },
            ),
        ) { backStackEntry ->
            val productId = backStackEntry.arguments?.getString("productId") ?: return@composable
            ProductDetailScreen(productId = productId)
        }

        composable(
            route = Routes.OrderStatus,
            arguments = listOf(navArgument("orderId") { type = NavType.StringType }),
            deepLinks = listOf(
                navDeepLink { uriPattern = "https://www.example.com/orders/{orderId}" },
                navDeepLink { uriPattern = "myapp://orders/{orderId}" },
            ),
        ) { backStackEntry ->
            val orderId = backStackEntry.arguments?.getString("orderId") ?: return@composable
            OrderStatusScreen(orderId = orderId)
        }

        composable(Routes.Home) { HomeScreen() }
    }
}

object Routes {
    const val Home          = "home"
    const val ProductDetail = "product/{productId}"
    const val OrderStatus   = "order/{orderId}"
}
```

---

## Android — AndroidManifest.xml intent filters

```xml
<activity android:name=".MainActivity" ...>

    <!-- App Links — verified HTTPS deep links -->
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="www.example.com" />
    </intent-filter>

    <!-- Custom scheme fallback -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" />
    </intent-filter>

</activity>
```

---

## Android — handle intent in Compose Activity

```kotlin
// androidApp/MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            // Handle deep link from the launching intent
            LaunchedEffect(intent) {
                intent?.data?.toString()?.let { url ->
                    val route = DeepLinkParser.parse(url).toRoute()
                    if (route != null) navController.navigate(route) { launchSingleTop = true }
                }
            }
            AppNavHost(navController = navController)
        }
    }

    // Handle deep links while app is already running
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        // Delegate to NavController via a shared state or event channel
    }
}

fun DeepLink.toRoute(): String? = when (this) {
    is DeepLink.ProductDetail -> "product/$productId"
    is DeepLink.OrderStatus   -> "order/$orderId"
    DeepLink.Home             -> Routes.Home
    is DeepLink.Unknown       -> null
}
```

---

## iOS — Universal Links and custom scheme

Add to `iosApp/iosApp.swift` (or `AppDelegate.swift`):

```swift
// Universal Links via NSUserActivity
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
          let url = userActivity.webpageURL else { return false }
    return handleDeepLink(url.absoluteString)
}

// Custom scheme
func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
    return handleDeepLink(url.absoluteString)
}

private func handleDeepLink(_ url: String) -> Bool {
    // Call shared Kotlin parser via KMP bridging
    let deepLink = DeepLinkParser.shared.parse(url: url)
    // Post to a Kotlin Flow/channel consumed by the NavController
    DeepLinkHandler.shared.emit(deepLink)
    return true
}
```

---

## assetlinks.json (Android) — host at /.well-known/

```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["AA:BB:CC:..."]
  }
}]
```

---

## apple-app-site-association (iOS) — host at /.well-known/

```json
{
  "applinks": {
    "apps": [],
    "details": [{
      "appID": "TEAMID.com.example.app",
      "paths": ["/products/*", "/orders/*", "/"]
    }]
  }
}
```

---

## Testing the parser

```kotlin
class DeepLinkParserTest {
    @Test fun `product URL parses to ProductDetail`() {
        val result = DeepLinkParser.parse("https://www.example.com/products/abc-123")
        assertEquals(DeepLink.ProductDetail("abc-123"), result)
    }

    @Test fun `custom scheme falls through to ProductDetail`() {
        val result = DeepLinkParser.parse("myapp://products/xyz")
        assertEquals(DeepLink.ProductDetail("xyz"), result)
    }

    @Test fun `unknown URL maps to Unknown`() {
        val result = DeepLinkParser.parse("https://other.com/page")
        assertTrue(result is DeepLink.Unknown)
    }
}
```

---

## Common Anti-Patterns

- **Parsing URLs in the Activity/AppDelegate** — URL parsing belongs in `commonMain`
  (`DeepLinkParser`) so it can be unit tested; Activities and delegates should only relay
  the raw URL string to the parser
- **Deep link routes not registered in NavHost** — if a route isn't in `navDeepLink { uriPattern }`,
  the Compose Navigation library ignores the intent's data URI; register every path pattern
- **Missing `android:autoVerify="true"`** — without this attribute Android never initiates
  App Links verification and the link opens in a browser chooser, not the app directly
- **Trailing-slash mismatch** — `assetlinks.json` paths and NavHost `uriPattern` must agree
  on whether paths end with `/`; normalize before matching
- **Not handling `onNewIntent`** — if the app is already running and receives a deep link,
  `onCreate` is not called again; `onNewIntent` must relay the new intent to the NavController

---

## Related Skills

- `kmp-navigation` — `NavHost` setup and `NavController` wiring;
  deep links are registered as `navDeepLink` blocks inside `composable { }` builders
- `kmp-mvi` — deep-link handling triggers a `NavigateTo` side effect,
  not a state mutation; the ViewModel emits a `Channel`-based `Effect`
- `kmp-unit-testing` — `DeepLinkParser` is a pure object with no
  platform dependencies; test it with plain JUnit `@Test` in `commonTest`

---

## Output Style

When implementing deep linking, respond in this order:
1. **DeepLink sealed class** — one subclass per linkable screen
2. **DeepLinkParser** — pure regex-based parser with tests
3. **NavHost** — `navDeepLink { uriPattern }` blocks for each route
4. **AndroidManifest** — intent filter (App Links + custom scheme)
5. **Android Activity** — `LaunchedEffect(intent)` + `onNewIntent`
6. **iOS** — `continue userActivity` + `open url` handlers
7. **Server files** — `assetlinks.json` and `apple-app-site-association` templates

---

## Changelog

| Date | Change |
|---|---|
| 2026-06-21 | Initial release. |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.