Android/Kotlin/Jetpack Compose: обработка push-уведомлений
Клонируем проект, берем кофе, что-нибудь вкусненькое — и вперед.
Что потребуется
Для принудительной отправки уведомлений воспользуемся службой обмена облачными сообщениями 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
:
Вот код.
Читайте также: