How to navigate from notification to specific screen in an android jetpack compose single activity application?
Asked Answered
D

1

8

I want to navigate from a notification action button to a specific screen in my single activity application in compose. Based on this documentation I decided to use deep-link navigation. The problem is that when I click on the notification action button, it restarts my activity before navigating to the expected screen. I don't want my activity to restart if it's opened in the background.

This is how I did it:

Manifest.xml

Here is what I specified in the application manifest:

<activity
    android:name=".ui.AppActivity"
    android:launchMode="standard"
    android:exported="true">
    <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
    </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:scheme="myApp" android:host="screenRoute" />
    </intent-filter>
</activity>

Root nav graph

Here is the deep link declaration in my root navigation graph:

composable(
   route = "screenRoute",
   deepLinks = listOf(navDeepLink { uriPattern = "myApp://screenRoute" })
) {
   ComposableScreen()
}

Pending intent

Here is the pending intent I use for the notification action button:

val intent = Intent().apply {
    action = Intent.ACTION_VIEW
    data = "myApp://screenRoute".toUri()
}

val deepLinkPendingIntent = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(intent)
    getPendingIntent(1234, FLAG_UPDATE_CURRENT)
}

I thought that I did something wrong here because I didn't find anything about this restart. So I downloaded the official compose navigation codelabs that uses deep links (as it's also a single app activity) and it does the same when using deep links from intents, the activity is restarted.

So my questions are:

  1. Is it possible to achieve deep link navigation from notification in single activity app without restarting it ?
  2. If not, what's the way of achieving this workflow (opening a specific composable from a notification with no restart) ? Should I send a broadcast from the notification action button and use deep link navigation from within my app ?
  3. Is the activity restarting from deep links because it's the main activity (launcher) ?

Thanks

Dabbs answered 13/7, 2023 at 8:42 Comment(0)
A
6

Problems

I see two problems here :

  • In the manifest, you haven't set the launch mode to single task android:launchMode="singleTask"
  • Using TaskStackBuilder will recreate an activity no matter what you do.

Possible solution - Handling deeplink yourself

I think that there is multiple solutions for your problem. However, here is one possible solution

Update the launch mode

To ensure that you will always have one instance of your app, you need to set the launchMode to singleTask

<activity
    android:name=".ui.AppActivity"
    android:launchMode="singleTask"
    android:exported="true">
    <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
    </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:scheme="myApp" android:host="screenRoute" />
    </intent-filter>
</activity>

Cold and host start

Cold start

When your app is not starting yet and you open a deeplink, the app will start and your activity will be created. So, in the onStart method, you will be able to handle the deeplink :

override fun onStart() {
        super.onStart()
        intent?.data?.let { /* handle deeplink */ }
        // consume the deeplink
        intent = null

    }
Hot start

When your app is already running, clicking on a deeplink will bring the app to front and it will trigger an observer with the intent.

setContent {
            DisposableEffect(Unit) {
                val listener = Consumer<Intent> { intent ->
                    // Handle deeplink

                }
                addOnNewIntentListener(listener)
                onDispose { removeOnNewIntentListener(listener) }
            }
        }

Handling deeplink with VM

In cold start, you are not in a composable scope. To fix this issue, you can use your VM as an event emitter for your view.

class MyViewModel : ViewModel() {
    val event = MutableStateFlow<Event>(Event.None)

    fun handleDeeplink(uri: Uri) {
        event.update { Event.NavigateWithDeeplink(uri) }
    }

    fun consumeEvent() {
        event.update { Event.None }
    }
}
sealed interface Event {
    data class NavigateWithDeeplink(val deeplink: Uri) : Event
    object None : Event
}

In the cold start case, call the handleDeeplink(uri) method


override fun onStart() {
        super.onStart()
        // To handle a cold deeplink intent we need to keep it and replace it with null
        intent?.data?.let { myViewModel.handleDeeplink(it) }
        intent = null
    }

In the hot start case, call it too

DisposableEffect(Unit) {
                    val listener = Consumer<Intent> { intent ->
                        intent.data?.let {
                           myViewModel.handleDeeplink(it)
                        }
                    }
                    addOnNewIntentListener(listener)
                    onDispose { removeOnNewIntentListener(listener) }
                }

Now, in your main composable, collect the event as state and navigate to deeplink when you receive the event. Don't forget to consume it because we are using stateFlow here.

 val event by myViewModel.event.collectAsState()

                LaunchedEffect(event) {
                    when (val currentEvent = event) {
                        is Event.NavigateWithDeeplink -> navController.navigate(currentEvent.deeplink)
                        Event.None -> Unit
                    }

                    myViewModel.consumeEvent()
                }

Creating the PendingIntent

Like I said, Using TaskStackBuilder will recreate an activity. Instead of using it to create a pending intent, you can create it yourself

val routeIntent = Intent(
            Intent.ACTION_VIEW,
            MyUri
        ).apply {
            flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
        }

        val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT

        val pending = PendingIntent.getActivity(
            appContext,
            0,
            routeIntent,
            flags
        )
Artamas answered 14/7, 2023 at 11:56 Comment(2)
If I don't need cold start handling can I just navigate directly in disposable effect instead of that clownery with viewmodels and events?Beverie
You can do whatever you want to handle the intent in the disposable effect consumer. My exemple shows how to handle it using a VM because it's a pretty normal way to add a stateFlow and dispatch an event to the viewArtamas

© 2022 - 2024 — McMap. All rights reserved.