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:
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>
Here is the deep link declaration in my root navigation graph:
composable(
route = "screenRoute",
deepLinks = listOf(navDeepLink { uriPattern = "myApp://screenRoute" })
) {
ComposableScreen()
}
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:
Thanks
I see two problems here :
android:launchMode="singleTask"
I think that there is multiple solutions for your problem. However, here is one possible solution
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>
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
}
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) }
}
}
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()
}
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
)