英文:
How to navigate from notification to specific screen in an android jetpack compose single activity application?
问题
- 在我的单一活动应用程序中,我希望能够从通知操作按钮导航到特定屏幕,使用Compose。根据这份文档,我决定使用深链接导航。问题是,当我点击通知操作按钮时,它会在导航到预期屏幕之前重新启动我的活动。我不希望如果我的活动在后台打开时它重新启动。
这是我所做的:
Manifest.xml
这是我在应用程序清单中指定的内容:
<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>
根导航图
这是我的根导航图中的深链接声明:
composable(
route = "screenRoute",
deepLinks = listOf(navDeepLink { uriPattern = "myApp://screenRoute" })
) {
ComposableScreen()
}
挂起意图
这是我用于通知操作按钮的挂起意图:
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)
}
我以为我在这里做错了什么,因为我没有找到有关这种重新启动的任何信息。所以我下载了使用深链接的官方Compose导航codelabs,它在使用来自意图的深链接时也会重新启动活动。
所以我的问题是:
- 在单一活动应用程序中,是否有可能实现从通知中的深链接导航而不重新启动它?
- 如果不行,实现这个工作流程(从通知中打开特定的Composable而不重新启动)的方法是什么?我应该从通知操作按钮发送广播,然后在我的应用程序内部使用深链接导航吗?
- 深链接导致活动重新启动是否因为它是主活动(启动器)?
谢谢。
英文:
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:
- Is it possible to achieve deep link navigation from notification in single activity app without restarting it ?
- 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 ?
- Is the activity restarting from deep links because it's the main activity (launcher) ?
Thanks
答案1
得分: 2
问题
我看到有两个问题:
- 在清单文件中,您还没有将启动模式设置为单一任务
kotlin android:launchMode="singleTask"
- 使用 TaskStackBuilder 会无论如何重新创建一个活动。
可能的解决方案 - 自行处理深度链接
我认为有多种解决方案可以解决您的问题。但是,这里有一个可能的解决方案。
更新启动模式
为了确保您始终只有一个应用程序实例,您需要将启动模式设置为 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>
冷启动和热启动
冷启动
当您的应用程序尚未启动并且打开深度链接时,应用程序将启动并创建活动。因此,在 onStart 方法中,您将能够处理深度链接:
override fun onStart() {
super.onStart()
intent?.data?.let { /* 处理深度链接 */ }
// 消耗深度链接
intent = null
}
热启动
当您的应用程序已经在运行时,点击深度链接将将应用程序置于前台,并触发一个具有意图的观察者。
setContent {
DisposableEffect(Unit) {
val listener = Consumer<Intent> { intent ->
// 处理深度链接
}
addOnNewIntentListener(listener)
onDispose { removeOnNewIntentListener(listener) }
}
}
使用 VM 处理深度链接
在冷启动情况下,您不在可组合的范围内。为了解决这个问题,您可以将您的 VM 用作视图的事件发射器。
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
}
在冷启动情况下,调用 handleDeeplink(uri)
方法
override fun onStart() {
super.onStart()
// 要处理冷深度链接意图,我们需要保留它并将其替换为 null
intent?.data?.let { myViewModel.handleDeeplink(it) }
intent = null
}
在热启动情况下,也要调用它
DisposableEffect(Unit) {
val listener = Consumer<Intent> { intent ->
intent.data?.let {
myViewModel.handleDeeplink(it)
}
}
addOnNewIntentListener(listener)
onDispose { removeOnNewIntentListener(listener) }
}
现在,在您的主要组合中,将事件收集为状态并在收到事件时导航到深度链接。不要忘记消耗它,因为我们在这里使用了 stateFlow。
val event by myViewModel.event.collectAsState()
LaunchedEffect(event) {
when (val currentEvent = event) {
is Event.NavigateWithDeeplink -> navController.navigate(currentEvent.deeplink)
Event.None -> Unit
}
myViewModel.consumeEvent()
}
创建 PendingIntent
正如我所说,使用 TaskStackBuilder 会重新创建一个活动。
不要使用它来创建挂起意图,而是自己创建它
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
)
英文:
Problems
I see two problems here :
- In the manifest, you haven't set the launch mode to single task
kotlin 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
)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论