如何在Android Jetpack Compose单一活动应用程序中从通知导航到特定屏幕?

huangapple go评论140阅读模式
英文:

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

问题

  1. 在我的单一活动应用程序中,我希望能够从通知操作按钮导航到特定屏幕,使用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,它在使用来自意图的深链接时也会重新启动活动。

所以我的问题是:

  1. 在单一活动应用程序中,是否有可能实现从通知中的深链接导航而不重新启动它?
  2. 如果不行,实现这个工作流程(从通知中打开特定的Composable而不重新启动)的方法是什么?我应该从通知操作按钮发送广播,然后在我的应用程序内部使用深链接导航吗?
  3. 深链接导致活动重新启动是否因为它是主活动(启动器)?

谢谢。

英文:

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:

&lt;activity
    android:name=&quot;.ui.AppActivity&quot;
    android:launchMode=&quot;standard&quot;
    android:exported=&quot;true&quot;&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
    &lt;/intent-filter&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.VIEW&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;
         &lt;data android:scheme=&quot;myApp&quot; android:host=&quot;screenRoute&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/activity&gt;

Root nav graph

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

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

Pending intent

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

val intent = Intent().apply {
    action = Intent.ACTION_VIEW
    data = &quot;myApp://screenRoute&quot;.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

答案1

得分: 2

问题

我看到有两个问题:

  • 在清单文件中,您还没有将启动模式设置为单一任务 kotlin android:launchMode=&quot;singleTask&quot;
  • 使用 TaskStackBuilder 会无论如何重新创建一个活动。

可能的解决方案 - 自行处理深度链接

我认为有多种解决方案可以解决您的问题。但是,这里有一个可能的解决方案。

更新启动模式

为了确保您始终只有一个应用程序实例,您需要将启动模式设置为 singleTask。

&lt;activity
    android:name=&quot;.ui.AppActivity&quot;
    android:launchMode=&quot;singleTask&quot;
    android:exported=&quot;true&quot;&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
    &lt;/intent-filter&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.VIEW&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;
         &lt;data android:scheme=&quot;myApp&quot; android:host=&quot;screenRoute&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/activity&gt;

冷启动和热启动

冷启动

当您的应用程序尚未启动并且打开深度链接时,应用程序将启动并创建活动。因此,在 onStart 方法中,您将能够处理深度链接:

override fun onStart() {
        super.onStart()
        intent?.data?.let { /* 处理深度链接 */ }
        // 消耗深度链接
        intent = null
    }
热启动

当您的应用程序已经在运行时,点击深度链接将将应用程序置于前台,并触发一个具有意图的观察者。

setContent {
            DisposableEffect(Unit) {
                val listener = Consumer&lt;Intent&gt; { intent -&gt;
                    // 处理深度链接
                }
                addOnNewIntentListener(listener)
                onDispose { removeOnNewIntentListener(listener) }
            }
        }

使用 VM 处理深度链接

在冷启动情况下,您不在可组合的范围内。为了解决这个问题,您可以将您的 VM 用作视图的事件发射器。

class MyViewModel : ViewModel() {
    val event = MutableStateFlow&lt;Event&gt;(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&lt;Intent&gt; { intent -&gt;
                        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 -&gt; navController.navigate(currentEvent.deeplink)
                        Event.None -&gt; 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=&quot;singleTask&quot;
  • Using TaskStackBuilder will recreate an activity no matter what you do.

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

&lt;activity
    android:name=&quot;.ui.AppActivity&quot;
    android:launchMode=&quot;singleTask&quot;
    android:exported=&quot;true&quot;&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.MAIN&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot; /&gt;
    &lt;/intent-filter&gt;
    &lt;intent-filter&gt;
         &lt;action android:name=&quot;android.intent.action.VIEW&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
         &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;
         &lt;data android:scheme=&quot;myApp&quot; android:host=&quot;screenRoute&quot; /&gt;
    &lt;/intent-filter&gt;
&lt;/activity&gt;

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&lt;Intent&gt; { intent -&gt;
                    // 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&lt;Event&gt;(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&lt;Intent&gt; { intent -&gt;
                        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 -&gt; navController.navigate(currentEvent.deeplink)
                        Event.None -&gt; 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
        )

huangapple
  • 本文由 发表于 2023年7月13日 16:42:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/76677468.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定