Search code examples
androidkotlinandroid-jetpack-composeandroid-deep-link

Navigating to a composable using a deeplink with Jetpack Compose


When a user enters a geo-fence in our app, we show them an offer notification about the area, which when clicked, should direct them to a specific composable screen called SingleNotification. I've followed google's codelab and their documentation but I haven't managed to make the navigation to the specific screen work yet. Right now, clicking on the notification or running the adb shell am start -d “eway://station_offers/date_str/www.test.com/TITLE/CONTENT” -a android.intent.action.VIEW command, simply opens the app.

The activity is declared as follows in the manifest:

    <activity
        android:name=".MainActivity"
        android:exported="true"
        android:label="@string/app_name"
        android:screenOrientation="portrait">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />

            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
        </intent-filter>

        <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:host="station_offers"
                android:scheme="eway" />
        </intent-filter>
    </activity>

Our MainNavController class contains the NavHost which in turn contains various NavGraphs. I've only included the relevant graph below:

        NavHost(
            navController = navController,
            startDestination = NavigationGraphs.SPLASH_SCREEN.route
        ) {
....
            notificationsNavigation()
....    
    }

The notificationsNavigation graph is defined as follows:

fun NavGraphBuilder.notificationsNavigation() {
    navigation(
        startDestination = Screens.NOTIFICATION_DETAILS.navRoute,
        route = NavigationGraphs.NOTIFICATIONS.route
    ) {
        composable(
            route = "${Screens.NOTIFICATION_DETAILS.navRoute}/{date}/{imageUrl}/{title}/{content}",
            arguments = listOf(
                navArgument("date") { type = NavType.StringType },
                navArgument("imageUrl") { type = NavType.StringType },
                navArgument("title") { type = NavType.StringType },
                navArgument("content") { type = NavType.StringType }
            ),
            deepLinks = listOf(navDeepLink {
                uriPattern = "eway://${Screens.NOTIFICATION_DETAILS.navRoute}/{date}/{imageUrl}/{title}/{content}"
            })
        ) { backstackEntry ->
            val args = backstackEntry.arguments
            SingleNotification(
                date = args?.getString("date")!!,
                imageUrl = args.getString("imageUrl")!!,
                title = args.getString("title")!!,
                description = args.getString("content")!!
            )
        }
    }
}

The Screes.NOTIFICATION_DETAILS.navRoute corresponds to the value of notification_details.

Inside the geo-fence broadcast receiver, I construct the pending Intent as follows:

                        val deepLinkIntent = Intent(
                            Intent.ACTION_VIEW,
                            "eway://station_offers/${
                                offer.date
                            }/${
                                offer.image
                            }/${offer.title}/${offer.content}".toUri(),
                            context,
                            MainActivity::class.java
                        )
                        val deepLinkPendingIntent: PendingIntent =
                            TaskStackBuilder.create(context!!).run {
                                addNextIntentWithParentStack(deepLinkIntent)
                                getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)!!
                            }
                        showNotification(offer.title, offer.content, deepLinkPendingIntent)

I can't figure out what I'm missing here.


Solution

  • It turns out that the limitations described in this answer are not entirely true. Specifically,

    1. It is possible to deep link from a notification directly into a destination that is inside a nested graph
    2. There is no relation between a destination's route and the deepLink URI.

    Point 2 above was the key to unlock my understanding of how deeplinks work. They are just arbitrary URIs and have no relationship to the destination's route at all. The rule is that the following 3 items must match up

    1. The URI pattern defined in a composable's navDeepLink DSL
    2. The URI used to construct a PendingIntent for the notification
    3. The scheme and host declared in the intent-filter in the manifest.

    Here are some code snippets. In my case the URIs were static, so you will need to make adjustments in order to address the OP's situation. This example has the following structure

    • LandingScreen ("landing_screen_route")
    • SecondScreen ("second_screen_route")
    • A nested graph ("nested_graph_route") with a NestedScreen ("nested_destination_route")

    We are going to see how to reach both SecondScreen and NestedScreen from a notification.

    First, defining the NavGraph using the DSL. Pay special attention to the navDeepLink entries here.

    @Composable
    fun AppGraph(onNotifyClick: () -> Unit) {
        val navController = rememberNavController()
        NavHost(
            navController = navController,
            startDestination = "landing_screen_route"
        ) {
            composable("landing_screen_route") {
                LandingScreen {
                    navController.navigate("second_screen_route")
                }
            }
            composable(
                route = "second_screen_route",
                deepLinks = listOf(
                    navDeepLink { uriPattern = "myapp://arbitrary_top_level" } // Note that this pattern has no relation to the route itself
                )
            ) {
                SecondScreen {
                    navController.navigate("nested_graph_route")
                }
            }
            navigation(
                startDestination = "nested_destination_route",
                route = "nested_graph_route"
            ) {
                composable(
                    route = "nested_destination_route",
                    deepLinks = listOf(
                        navDeepLink { uriPattern = "myapp://arbitrary_nested" } // Note that this pattern has no relation to the route itself
                    )
                ) {
                    NestedScreen(onNotifyClick)
                }
            }
        }
    }
    
    

    Next, here's how you would construct the PendingIntent for both these cases:

    val notNestedIntent = TaskStackBuilder.create(this).run {
        addNextIntentWithParentStack(
            Intent(
                Intent.ACTION_VIEW,
                "myapp://arbitrary_top_level".toUri() // <-- Notice this
            )
        )
        getPendingIntent(1234, PendingIntent.FLAG_UPDATE_CURRENT)
    }
    
    val nestedIntent = TaskStackBuilder.create(this).run {
        addNextIntentWithParentStack(
            Intent(
                Intent.ACTION_VIEW,
                "myapp://arbitrary_nested".toUri() // <-- Notice this
            )
        )
        getPendingIntent(2345, PendingIntent.FLAG_UPDATE_CURRENT)
    }
    

    Finally, here are the intent-filter entries in the manifest

    <activity
        android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
    
            <!--
                The scheme and host must match both of the below:
                1. The navDeepLink declaration
                2. The URI defined in the PendingIntent
             -->
            <data
                android:scheme="myapp"
                android:host="arbitrary_top_level"
            />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
    
            <!--
                The scheme and host must match both of the below:
                1. The navDeepLink declaration
                2. The URI defined in the PendingIntent
             -->
            <data
                android:scheme="myapp"
                android:host="arbitrary_nested"
            />
        </intent-filter>
    </activity>