Deep Links with Navigation Compose

Quick answer: Declare a navDeepLink() on the Navigation Compose destination, then configure an Android manifest intent filter for the URI host and scheme. For verified https links, publish Digital Asset Links too. Navigation matches the incoming intent to the destination; Android decides whether your app receives the web URL.

The Navigation deep-link guide separates those responsibilities. A graph deep link describes where the app goes; an App Link declaration allows the operating system to open that URL in the app.

Add a URI pattern to the destination

import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink

val articleUri = "https://example.com/articles/{articleId}"

NavHost(navController, startDestination = "home") {
    composable(
        route = "article/{articleId}",
        deepLinks = listOf(navDeepLink { uriPattern = articleUri }),
    ) { entry ->
        val articleId = requireNotNull(entry.arguments?.getString("articleId"))
        ArticleScreen(articleId = articleId)
    }
}

Use only the minimum identifier as an argument, then load the actual data from the source of truth. The Navigation guidance similarly recommends passing IDs rather than complex objects.

Tell Android that the Activity handles the URL

<activity android:name=".MainActivity" android:exported="true">
  <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="example.com" />
  </intent-filter>
</activity>

For a verified App Link, host a valid /.well-known/assetlinks.json for the signing certificate and package on that domain. Without verification, Android may show a chooser or open the browser instead. Keep URI patterns narrow and validate any argument before using it to fetch protected data.

Back stack and launch modes

An external deep link builds a back stack that includes start destinations from nested graphs. The official guide strongly recommends the default standard launch mode: Navigation then handles the incoming intent automatically. If an activity uses singleTop or another reuse mode, forward the fresh intent in onNewIntent():

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    navController.handleDeepLink(intent)
}

Do not add a nonstandard launch mode merely to solve navigation behavior; test the default path first.

For a URI that matches a graph destination, build a NavDeepLinkRequest and use navigate(request). Check navController.graph.hasDeepLink(request) before navigating because an invalid request throws.

val request = NavDeepLinkRequest.Builder
    .fromUri("https://example.com/articles/42".toUri())
    .build()

if (navController.graph.hasDeepLink(request)) {
    navController.navigate(request)
}

Unlike an external deep link, navigate(NavDeepLinkRequest) does not reset the existing back stack. Use normal route navigation when the destination is already known; reserve a request for URI-driven navigation.

Test the complete path

  • Test a cold-start URL, a warm-start URL, an unknown route, and malformed arguments.
  • Verify the browser opens your verified domain in the installed release build, not only in debug.
  • Confirm Back follows a sensible destination stack after nested deep links.
  • Test authentication redirects: a protected destination must resume safely after sign-in.

For route ownership and back-stack structure, see Navigation Compose and UI state, events, and one-time effects.

FAQ

No. It matches a deep link once Navigation receives an intent. The manifest and, for verified web links, Digital Asset Links make Android route the URL to the app.

Avoid it. Pass an ID or other small identifier, then retrieve current data from the repository or ViewModel.