Navigating to a composable using a deeplink with Jetpack Compose
Asked Answered
W

2

11

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.

Woodprint answered 20/5, 2022 at 9:51 Comment(0)
B
18

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>
Bachelorism answered 27/6, 2022 at 9:31 Comment(2)
I'm updating the selected answer! You were right, I tested it with nested graphs & navigation arguments, and it works. Nice debugging!Woodprint
@SteliosPapamichail It would be helpful if you could edit the answer to also show how to use this with non-static URIs like in your original questionBachelorism
W
6

UPDATE: Please see @curioustechizen's answer below for the actual solution instead of this workaround!

Alright, after a lot of testing and running the solution of Google's relative code lab a bunch of times line by line, I figured out how to make it work. First and foremost, it looks like the host that we define in the AndroidManifest.xml for the <data> tag of the intent filter needs to much the composable destination's route. So in my case, it is defined as:

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

Second of all, the deep link's uri pattern should match the composable's route format. In this case, since the composable's route is defined as route = "${Screens.NOTIFICATION_DETAILS.navRoute}/{date}/{imageUrl}/{title}/{content}", the correct deep Link uriPattern, would be :

deepLinks = listOf(navDeepLink {
                    uriPattern =
                        "eway://${Screens.NOTIFICATION_DETAILS.navRoute}/{date}/{imageUrl}/{title}/{content}"
                })

Furthermore, the composable destination seems to MUST be declared inside the NavHost itself and not inside a NavGraph. Initially as you can see, I thought that the system would be able to find the destination via the nested NavGraph, but it couldn't (threw a relative exception), so I came to the conclusion that it must be done this way (as is done in the code labs). Please correct me if I'm wrong!

Lastly, I changed the val uri definition inside my GeofenceBroadcastReceiver accordingly. Now it looks like so:

val uri = "eway://${Screens.NOTIFICATION_DETAILS.navRoute}/${
                                    offer.date.replace(
                                        "/",
                                        "@"
                                    )
                                }/${
                                    offer.image.replace(
                                        "/",
                                        "@"
                                    )
                                }/${offer.title}/${offer.content.replace("/", "@")}".toUri()

So to recap, these are the steps that seem to solve this issue as far as my understanding goes:

  1. The deep link's destination composable must be a direct child of the main NavHost
  2. The AndroidManifest's android:host should match the destination composable's route, and lastly,
  3. The deep link's Uri pattern should match the destination composable's route (if you use the format scheme://host/.... you should be fine if you followed number 2)
Woodprint answered 24/5, 2022 at 16:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.