Клонируем проект, берем кофе, что-нибудь вкусненькое  —  и вперед.

Что потребуется

Для принудительной отправки уведомлений воспользуемся службой обмена облачными сообщениями Firebase Cloud Messaging, или FCM. Добавляем Firebase в проект, в частности:

  • создаем проект в консоли Firebase;
  • регистрируем приложение в Firebase, название пакета то же, что указано для проекта;
  • загружаем google-services.json, он получается при создании приложения или находится в Project Settings («Настройки проекта») > General («Общие») > Your apps («Ваши приложения»):
  • добавляем плагин в build.gradle.kts(Project:) и в build.gradle.kts(Module:);
  • добавляем SDK-пакет Firebase в зависимости build.gradle.kts(Module:).

Следуем инструкции.

Если в проекте несколько приложений, все они наверняка содержатся в массиве client в google-services.json. И тогда уведомление в итоге может отправиться в приложение, отличное от целевого в этом списке.

Удаляем все нецелевые, куда этот json-файл не добавляется.

Симулятор/эмулятор

Проверяем:

  • наличие сервисов Google Play, Google API недостаточно;
  • интернет-подключение, открываем любую страницу браузера в симуляторе.

Проблемы вроде java.io.IOException: AUTHENTICATION_FAILED, SERVICE_NOT_AVAILABLE или невозможности получения токена устройства решаются:

  • холодной загрузкой устройства:
  • перезапуском Android Studio;
  • перезагрузкой компьютера;
  • отключением VPN, холодной загрузкой и повторным включением устройства.

Повторяем пару раз описанный выше процесс, это же Android.

Обзор

Сделаем акцент на:

  • Извлечении токена.
  • Настройке MyFirebaseMessagingService.
  • Обработке сообщения, получаемого в закрытом/фоновом/неактивном/приоритетном приложении.

Уведомление состоит из сообщения и полезной нагрузки ― данных. В данных имеется единый ключ count, например count: some_integer.

Вот конфигурация проекта:

  • один Activity как отдельный экран для действия пользователя;
  • две NavigationViewModel с определением NavHost, маршрутами каждой из которых указывается на разные ViewModel:

А вот стек переходов, где каждым уведомлением активируется новая DetailViewModel, добавляемая в стек:

По этой конфигурации понятно, как:

  • пользователю показывается конкретное уведомление, только пока он не авторизуется и не окажется на главном экране, например HomeViewModel;
  • в стек добавляются экземпляры модели DetailViewModel, каждый из которых соответствует новому уведомлению, получаемому пользователем еще при просмотре предыдущего;
  • данные из полученного сообщения передаются в каждую DetailViewModel по отдельности и сохраняются, например показывается count, полученный в DetailViewModel;
  • происходит возвращение к предыдущему уведомлению DetailViewModel, на котором остановился пользователь.

На короткой гифке, хоть по записи экрана это и трудно понять, в приложение отправляются два уведомления: первое с count = 1, второе с count = 2:

Итак, начнем.

Загружайте проект и добавляйте свой google-service.json с названием пакета Android com.example.fcmnotificationdemo.

Манифест Android

Добавим еще кое-что.

1) Служба MyFirebaseMessagingService

Ею расширяется FirebaseMessagingService. Эту службу необходимо расширить, чтобы не только получать уведомления о приложениях в фоновом режиме, уведомления в приоритетных приложениях, полезную нагрузку данных, но и обрабатывать сообщения, в частности отправлять исходящие и т. д.

Добавляем в appplication следующее:

<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

Создадим службу MyFirebaseMessagingService позже.

2) LaunchMode

Задаем значение singleTop, singleTask или singleInstance  —  в зависимости от настроек проекта. В моем случае с единственным Activity всеми тремя выполняется одно и то же. Приложение может перезапускаться при каждом нажатии пользователя на баннер с показываемым вами уведомлением, даже когда приложение стало приоритетным.

Переходим в application > activity и добавляем:

android:launchMode="singleTop"

Вот какой теперь раздел приложения в AndroidManifest.xml:

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FCMNotificationDemo"
tools:targetApi="31">
<activity
android:name=".MainActivity"

android:launchMode="singleTop"

android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.FCMNotificationDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

</application>

Каналы уведомлений добавлять необязательно, пока пропустим.

build.gradle.kts(Module:)

Добавим в проект необходимый SDK-пакет.

При настройке Firebase добавляется implementation(platform(“com.google.firebase:firebase-bom:32.6.0”)).

Чтобы использовать Firebase Cloud Messaging с Kotlin, добавим в build.gradle.kts(Module:) также implementation(“com.google.firebase:firebase-messaging-ktx:23.4.1”).

Переходим к проекту.

PushNotificationManager

Прежде чем создавать подкласс MyFirebaseMessagingService из FirebaseMessagingService, сделаем объект PushNotificationManager.

Это класс, которым:

  • полученные в уведомлении данные сохраняются и передаются в DetailViewModel;
  • обрабатывается связанное с токеном действие, например его регистрация на сервере.

Применение диплинков для передачи данных из объекта уведомлений intent и перехода к конкретному представлению проблематично: если для объекта уведомлений intent задать Uri, приложение перезапускается и для одного intent активируется и onCreate, и onNewIntent.

Сделаем этот класс проще, в suspend-функцию registerTokenOnServer добавляем delay:

object PushNotificationManager {
private var countReceived: Int = 0

fun setDataReceived(count: Int) {
this.countReceived = count
}

fun getDataReceived(): Int {
return this.countReceived
}

suspend fun registerTokenOnServer(token: String) {
delay(2000)
}

}

MyFirebaseMessagingService

Обзор

Чтобы получать сообщения и показывать, например, наверху баннер с уведомлением, создадим MyFirebaseMessagingService, которым расширяется FirebaseMessagingService. Переопределим метод onMessageReceived, а также onDeletedMessages, чтобы знать, когда пользователь удаляет сообщение.

Но в метод onMessageReceived из FCM доставляются не все сообщения, имеется два исключения:

  • сообщения-уведомления доставляются, когда приложение фоновое/неактивное;
  • сообщения с уведомлением и полезной нагрузкой данных доставляются, когда приложение фоновое/неактивное  —  это нам и нужно.

Эта табличка взята из официального документа Google, но вообще-то учитывается три сценария.

Для сообщений, доставляемых в область уведомлений панели задач, и добавляемых в дополнение к intent данных в зависимости от состояния приложения: задача завершенная/фоновая/неактивная  —  они обрабатываются в разных функциях MainActivity: onCreate или onNewIntent. Подробнее  —  ниже, а пока кое-что объясним.

Вот как переопределяется onMessageReceived. Парсим данные, например count. Чтобы добавить в intent и потом получить обратно, передаем их в функцию sendNotification без диплинков.

Данные напрямую в PushNotificationManager не передаются: хотя intent при взаимодействии с приложением отображается, немедленное нажатие пользователем этого объекта не гарантируется. Если пользователь игнорирует intent и возвращается к нему позже, нажав на уведомление в соответствующей области панели задач, данные все еще нужно вернуть.

Так обрабатываются только те данные, которые доставляются, когда приложение приоритетное:

override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d("Push notification", "Message received")

if (remoteMessage.data.isEmpty() || remoteMessage.notification == null) {
return
}

Log.d("Push notification", "Message data payload: ${remoteMessage.data}")

val messageData = remoteMessage.data

var count: Int? = null
if( "count" in messageData )
count = messageData["count"]?.toInt()

if (count == null) {
return
}

sendNotification(remoteMessage.notification!!, count)

}

sendNotification

Сгенерируем и собственный объект уведомлений intent как результат полученного FCM сообщения:

private fun sendNotification(notification: RemoteMessage.Notification, count: Int) {
val intent = Intent(
this,
MainActivity::class.java
)
intent.putExtra("count", count.toString())

val requestCode = System.currentTimeMillis().toInt()
val pendingIntent = PendingIntent.getActivity(
this,
requestCode,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)


val channelId = "FCMDemoChannel"
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(notification.title)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(notification.body)
)
.setShowWhen(true)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent)
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager

val channel = NotificationChannel(
channelId,
notification.title,
NotificationManager.IMPORTANCE_HIGH
)

channel.setShowBadge(true)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
manager.createNotificationChannel(channel)

val notificationId = System.currentTimeMillis().toInt()
manager.notify(notificationId, builder.build())
}

Из примера FCM исключена строка intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), потому что:

  • режим запуска activity уже задан: singleTop;
  • так пользователь останется, где находится, не пытаясь ничего перезапускать.

Рабочим этот метод становится благодаря двум важным обстоятельствам.

Первое

Чтобы активировать intent.putExtra при использовании intent с PendingIntent, задаем флаги PendingIntent для включения PendingIntent.FLAG_UPDATE_CURRENT.

Второе

Не используем для requestCode константу. А то предыдущий Intent перезапишется входящим  —  из-за задания флагов для включения PendingIntent.FLAG_UPDATE_CURRENT. Например, получили два уведомления от FCM в таком порядке:

  • count: 1;
  • count: 5.

Если задать в requestCode константу, то при нажатии в области уведомлений на оба intent в DetailViewModel отображается 5.

onNewToken

Для обновления DeviceToken, который при повторном генерировании сохраняется на сервере, переопределяем функцию onNewToken:

@OptIn(DelicateCoroutinesApi::class)
override fun onNewToken(token: String) {
Log.d("Push notification", "Token Refreshed ${token}")

GlobalScope.launch {
PushNotificationManager.registerTokenOnServer(token)
}

}

LoginViewMode, HomeViewModel, DetailViewMode

В этих трех ViewModel содержится один Composable. Прежде чем переходить к важнейшей для управления навигацией части  —  MainActivity, разберем их код.

LoginViewMode

class LoginViewModel: ViewModel() {

@Composable
fun Run(onLoginPressed: () -> Unit) {

Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = onLoginPressed) {
Text("Log In")
}
}
}
}

HomeViewModel

class HomeViewModel: ViewModel() {

@Composable
fun Run() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Home")
}
}
}

DetailViewMode

Показываемые здесь пользователю данные сохраняются во ViewModel переменной count. Это очень удобно, когда ожидается несколько копий одной и той же ViewModel в связи с тем, что пользователь нажимает на новое уведомление при просмотре старого:

class DetailViewModel: ViewModel() {
private var count = 0

fun setUpData(count: Int) {
this.count = count
}

@Composable
fun Run(
navigateUp: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = navigateUp) {
Text("Back")
}
Spacer(modifier = Modifier.height(50.dp))

Text("Data Received: ${count}")

}

}
}

Обзор MainActivity

Разберем теперь MainActivity, это класс для:

  • запрашивания у пользователя разрешения на получение push-уведомления и токена устройства;
  • обработки данных, которые доставляются, когда приложение приоритетное, фоновое и вовсе не запускается;
  • перехода к конкретной ViewModel  —  Composable;
  • перехода к месту назначения, только если пользователь авторизуется, например HomeViewModel наверху.

Запрашивание разрешения и получение токена

Объяснение имеется в официальной документации. Если токен не получается из-за java.io.IOException: AUTHENTICATION_FAILED или SERVICE_NOT_AVAILABLE, пробуем решения из начала статьи: холодная загрузка, перезапуск и т. д.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

FirebaseMessaging.getInstance().isAutoInitEnabled = true
askNotificationPermission()
retrieveFCMToken()
}

private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.d("push notification device token", "failed with error: ${task.exception}")
return@OnCompleteListener
}
val token = task.result
Log.d("push notification device token", "token received: $token")
lifecycleScope.launch {
PushNotificationManager.registerTokenOnServer(token)
}
})
}


private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
if (isGranted) {
}
}

private fun askNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
return
} else if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) {
} else {
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
}

Получение данных Extras в Intent

onNewIntent

Согласно таблице данные, доставляемые, когда приложение фоновое, добавляются автоматически как дополнительный intent, а intent доставляется в функцию onNewIntent.

Кроме того, в intent вручную добавили putExtras, создаваемый при доставке уведомления, когда приложение приоритетное, поэтому при вызове intent?.extras в onNewIntent получаются данные уведомлений для следующих случаев:

  • уведомление доставляется, когда приложение фоновое;
  • уведомление доставляется, когда приложение приоритетное и пользователь нажимает на intent, пока приложение приоритетное;
  • как в случае, когда уведомление доставляется в приоритетном приложении, intent показывается пользователю во время работы с приложением, игнорируется пользователем, и он решает открыть его позже из области уведомлений, пока приложение запускается в фоновом/неактивном состоянии, не завершено. Объяснение затянулось, но нужно обдумать все возможности:
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)

// уведомление присылается, когда приложение неактивное/фоновое, данные включаются в дополнительный «intent»
val count: Int? = intent?.extras?.getString("count")?.toInt()
count?.let {
Log.d("push notification from background", "count : $count")
PushNotificationManager.setDataReceived(count = count)
}
}

Чтобы переходить к конкретному месту назначения, кое-что добавим.

onCreate

Аналогично данные, которые содержатся в уведомлении и доставляются, пока приложение запускается, тоже добавляются как дополнительный intent. Только на этот раз функция onNewIntent не активируется: получаем ее в onCreate.

А здесь при вызове intent.extras получаются данные для таких случаев:

  • уведомление доставляется, когда приложение закрыто;
  • уведомление доставляется в приоритетном приложении, intent показывается пользователю во время работы с приложением, игнорируется пользователем, и он решает открыть его позже из области уведомлений, когда приложение полностью завершено, закрыто:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

FirebaseMessaging.getInstance().isAutoInitEnabled = true
askNotificationPermission()
retrieveFCMToken()

// уведомление присылается, когда приложение завершено
val count: Int? = intent?.extras?.getString("count")?.toInt()
Log.d("push notification from background", "count : $count")
count?.let {
PushNotificationManager.setDataReceived(count = count)
}
}

Навигация при пользовательском взаимодействии

Чтобы из MainActivity перейти к DetailViewModel, понадобится способ получить NavController в MainNavigationViewModel. Но к этой ViewModel нет прямого доступа, так как у нас имеется другая RootNavigationViewModel  —  фактически корень содержимого, заданного в MainActivity с парой других маршрутов.

MainNavigationViewModel

Обратимся к MainNavigationViewModel. Как показано на схеме в начале статьи, будет два маршрута: HOME и DETAIL. Они сопоставляются с HomeViewModel и DetailViewModel:

enum class MainNavigationRoute {
HOME,
DETAIL
}

class MainNavigationViewModel: ViewModel() {
lateinit var navController: NavHostController
private var homeViewModel = HomeViewModel()
private var detailViewModels: MutableList<DetailViewModel> = mutableListOf()

fun isNavControllerInitialized(): Boolean {
return this::navController.isInitialized
}

fun showPushNotification() {
Log.d("show push notification", "")
val newPushModel = DetailViewModel()
newPushModel.setUpData(PushNotificationManager.getDataReceived())
detailViewModels.add(newPushModel)

navController.navigate(MainNavigationRoute.DETAIL.name) {
restoreState = false
launchSingleTop = false
}
}

@Composable
fun Run(){

navController = rememberNavController()
NavHost(
navController = navController,
startDestination = MainNavigationRoute.HOME.name)
{

composable(MainNavigationRoute.HOME.name) {
homeViewModel.Run()
}

composable(MainNavigationRoute.DETAIL.name) {
if (detailViewModels.isEmpty()) {
return@composable
}
detailViewModels.last().Run(
navigateUp = {
navController.navigateUp()
detailViewModels.removeLast()
}
)
}
}
}
}

Кроме обычного объявления NavHost, здесь стоит обратить внимание на:

  • Переменную navController.

Чтобы получить к ней доступ вне функции Composable, не объявляем ее там, а сохраняем как lateinit var во ViewModel.

  • Функцию isNavControllerInitialized.

Ею подтверждается, что lateinit var navController инициализирована до того, как мы попытаемся перейти с ее помощью из MainActivity к другому маршруту.

  • detailViewModels: MutableList<DetailViewModel>.

Копий может быть несколько, поэтому то, что имеется на данный момент, отслеживаем с помощью MutableList. Инициализируем новую каждый раз, когда пытаемся показать данные нового push-уведомления. И удаляем detailViewModels.removeLast, когда пользователь нажимает кнопку back («Назад») в DetailViewModel. А самую новую получаем, вызывая last внутри composable в NavHost.

  • Функцию showPushNotification.

Данные push-уведомлений показываются ею в новой DetailViewModel. Также с ее помощью:

  • получаются данные из PushNotificationManager;
  • создается новая DetailViewModel;
  • данные передаются в DetailViewModel;
  • осуществляется навигация.

Не инициализируем новую DetailViewModel в NavHost composable, то есть после вызова navigate. Иначе новая страница появится, но без отображения содержимого. Ведь при каждом вызове navController.navigate этот composable запускается несколько раз.

RootNavigationViewModel

А это ViewModel, к которой имеется доступ из MainActivity:

enum class LoginNavigationRoute {
LOGIN,
MAIN
}

class RootNavigationViewModel: ViewModel() {
lateinit var navController: NavHostController

private var mainNavigationViewModel = MainNavigationViewModel()
private var loginViewModel = LoginViewModel()

fun getMainNavigationViewModel(): MainNavigationViewModel? {
if (!this::navController.isInitialized) {
return null
}
val backStackEntry = navController.currentBackStackEntry
if (backStackEntry?.destination?.route == LoginNavigationRoute.MAIN.name) {
if (mainNavigationViewModel.isNavControllerInitialized() ) {
return mainNavigationViewModel
}
return null
}
return null
}

@Composable
fun Run() {
navController = rememberNavController()
NavHost(
navController = navController,
startDestination = LoginNavigationRoute.LOGIN.name)
{

composable(LoginNavigationRoute.LOGIN.name) {
loginViewModel.Run(
onLoginPressed = { navController.navigate(LoginNavigationRoute.MAIN.name) }
)
}

composable(LoginNavigationRoute.MAIN.name) {
mainNavigationViewModel.Run()
}
}
}
}

Структура аналогична MainNavigationViewModel с дополнительной важной функцией:

getMainNavigationViewModel

Очевидно, var mainNavigationViewModel объявляется, как только создается экземпляр RootNavigationViewModel. Поэтому возвращается всегда, но смысл функции не в этом.

mainNavigationViewModel возвращаем, только если она находится в верхней части стека переходов, когда пользователь авторизовался, то есть backStackEntry?.destination?.route == LoginNavigationRoute.MAIN.name, и она действительно сформировалась. Для этого проверяем, инициализирована ли navController в mainNavigationViewModel. Иначе возвращается Null.

Посмотрим, как с помощью этой функции пользователю показывается DetailViewModel при взаимодействии с уведомлением, и только если он авторизовался.

MainActivity

Сначала просто объявляем RootNavigationViewModel как переменную внутри класса и setContent:

class MainActivity : ComponentActivity() {
private var rootNavigationViewModel = RootNavigationViewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//...и остальное, что у нас там выше
setContent {
rootNavigationViewModel.Run()
}
}
}

Алгоритм перехода к DetailViewModel одинаков для onCreate и onNewIntent. Чтобы показать, содержатся ли в intent.extras данные count, добавим в count?.let после PushNotificationManager.setDataReceived(count = count) вот это:

lifecycleScope.launch {
while (rootNavigationViewModel.getMainNavigationViewModel() == null) {
Log.d("push notification", "not logged in, waiting...")
delay(100)
}

val mainNavigationController = rootNavigationViewModel.getMainNavigationViewModel()
mainNavigationController!!.showPushNotification()
return@launch
}
return

Здесь указывается, что если пользователь не находится на одном из маршрутов, определенных в MainNavigationViewModel, то есть HomeViewModel или DetailViewModel, то проверяется rootNavigationViewModel.getMainNavigationViewModel() == null и выполняется delay в ожидании авторизации пользователя.

По завершении проверки, чтобы создать новый экземпляр DetailViewModel с данными и перейти к нему, просто вызываем showPushNotification.

Вот что получается в MainActivity для onCreate и onNewIntent:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

FirebaseMessaging.getInstance().isAutoInitEnabled = true
askNotificationPermission()
retrieveFCMToken()

setContent {
rootNavigationViewModel.Run()
}

// уведомление присылается, когда приложение завершено
val count: Int? = intent?.extras?.getString("count")?.toInt()
Log.d("push notification", "on create count : $count")
count?.let {
PushNotificationManager.setDataReceived(count = count)
lifecycleScope.launch {
while (rootNavigationViewModel.getMainNavigationViewModel() == null) {
Log.d("push notification", "not logged in, waiting...")
delay(500)
}
val mainNavigationController = rootNavigationViewModel.getMainNavigationViewModel()
mainNavigationController!!.showPushNotification()
return@launch
}
return
}
}

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)

Log.d("push notification ", " on new intent extras? : ${intent?.extras}")

// уведомление присылается, когда приложение неактивное/фоновое, данные включаются в дополнительный «intent»
val count: Int? = intent?.extras?.getString("count")?.toInt()
count?.let {
Log.d("push notification ", " on new intent count : $count")
PushNotificationManager.setDataReceived(count = count)
lifecycleScope.launch {

while (rootNavigationViewModel.getMainNavigationViewModel() == null) {
Log.d("push notification", "not logged in, waiting...")
delay(100)
}

val mainNavigationController = rootNavigationViewModel.getMainNavigationViewModel()
mainNavigationController!!.showPushNotification()
return@launch
}
return
}
}

Хотя DetailViewModel отображается одним setContent вот так:

var detailViewModel = DetailViewModel()
setContent{
detailViewModel.Run()
}

Очень не рекомендую так делать, поскольку содержимое в этом случае становится корневым представлением activity: вернуться к тому, на чем остановились ранее, больше не получится.

Также, чтобы показать конкретный маршрут, вручную меняют startDestination в NavHost. Но так теряются стеки переходов назад, несколько копий ViewModel одна поверх другой не отображаются.

Протестируем

Чтобы протестировать настройку push-уведомлений, заходим слева в консоли FCM в Messaging («Обмен сообщениями») и нажимаем New campaign («Новая кампания»):

В выпадающем списке выбираем Notifications («Уведомления»):

Вводим заголовок и текст:

Выбираем целевое приложение:

В Additional options («Дополнительные параметры») добавляем данные count:

Возвращаемся в Notification и выбираем Send test message («Отправить тестовое сообщение»):

Вводим токен устройства, полученный в MainActivity, и нажимаем Test («Протестировать»):

Через пару секунд появляется уведомление. Если нет, проверяем подключение симулятора к интернету. Нажимаем на уведомление, авторизуемся и видим count:

Вот код.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Itsuki: Android/Kotlin/Jetpack Compose: Handle Push Notifications — Pass Data, Navigate to a Specific Composable, and Many More!

Предыдущая статьяПочему стоит использовать GoFr для разработки Golang-бэкенда?