Как подключить push-уведомления Firebase Cloud Messaging в Android-приложении

Интеграция push состоит из нескольких независимых частей: Firebase регистрирует экземпляр приложения, FCM доставляет сообщение, Android разрешает или запрещает показ, а приложение выбирает поведение в открытом состоянии. Если проверять только наличие токена, ошибка может остаться на любом следующем этапе.
Соберём авторский тест «Обновление доступно» для Android-приложения на Kotlin с существующей MainActivity. Пример показа рассчитан на Android 8 и выше, использует AndroidX Core с NotificationCompat и требует подходящий compile SDK для показанных API. Сам Android-проект и отправка здесь не запускались.
Зафиксируйте используемую модель FCM
Маршрут ниже относится к совместимым legacy-сообщениям AndroidConfig и callback onMessageReceived. На ноябрь 2026 года документация также описывает preview AndroidConfigV2 с другими callbacks и автоматическим показом, включая foreground. Не смешивайте эти правила: ручной показ из старого примера нельзя без проверки переносить в V2.
Доступ к registration token в текущей документации помечен deprecated в пользу Firebase Installation ID. Однако официальный тест из Firebase Console пока описан через registration token. Здесь он нужен именно для этого ограниченного теста; постоянную серверную регистрацию проектируют по актуальной модели отдельно.
Подключите проект и библиотеку
В Firebase Console добавьте Android-приложение с applicationId вашей устанавливаемой сборки. Скачайте google-services.json и разместите в корне app-модуля. Не подменяйте applicationId названием Kotlin-пакета и учитывайте суффиксы debug-варианта.
В корневом Gradle-файле объявите com.google.gms.google-services, а в app-модуле примените плагин. Документация настройки на момент подготовки показывает версию плагина 4.5.0 и Firebase BoM 35.1.0. Фрагмент зависимостей:
implementation(platform("com.google.firebase:firebase-bom:35.1.0"))
implementation("com.google.firebase:firebase-messaging")
Это дополнение к существующему приложению, а не полный Gradle-проект. Для NotificationCompat нужна согласованная с проектом зависимость AndroidX Core. Выполните синхронизацию и сборку до проверки доставки. Серверный ключ или service account JSON в приложение не добавляют.
Создайте канал до первого фонового теста
На Android 8 и выше уведомления используют каналы. Для примера выберем постоянный идентификатор updates и название «Обновления». Создавайте его при запуске MainActivity вызовом ensureUpdatesChannel(this), чтобы канал уже существовал, когда приложение уйдёт в фон.
Повторное создание существующего канала безопасно, но не позволяет произвольно изменить его важность: пользователь управляет соответствующими настройками. Не создавайте новый channel ID при каждом сообщении. Для Android 13 и выше отдельно запросите POST_NOTIFICATIONS в Activity через подходящий механизм runtime permissions; создание канала этого согласия не заменяет.
Ниже — сервис и общая функция канала. Поместите их в пакет приложения, добавив свою строку package. Системная иконка используется только для минимального теста; для рабочего приложения подготовьте собственную подходящую иконку уведомления.
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
const val UPDATES_CHANNEL = "updates"
fun ensureUpdatesChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = context.getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(NotificationChannel(
UPDATES_CHANNEL, "Обновления", NotificationManager.IMPORTANCE_DEFAULT
))
}
}
class PushService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
Log.d("PushTest", "Test token: $token")
}
override fun onMessageReceived(message: RemoteMessage) {
ensureUpdatesChannel(this)
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) return
val text = message.notification?.body ?: message.data["text"] ?: return
val intent = Intent(this, MainActivity::class.java)
val pending = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, UPDATES_CHANNEL)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(message.notification?.title ?: "Обновление")
.setContentText(text)
.setContentIntent(pending)
.setAutoCancel(true)
.build()
getSystemService(NotificationManager::class.java).notify(1001, notification)
}
}Проверка разрешения перед notify предотвращает попытку обычного показа при отсутствии согласия на Android 13+. Запрос самого разрешения выполняется в пользовательском интерфейсе, а не из сервиса. Код открывает существующую MainActivity по нажатию; специальная навигация на экран новости здесь не реализована.
Идентификатор уведомления 1001 выбран для теста: следующее уведомление с тем же ID обновляет его. Это не схема уникальных уведомлений для всех событий. Внутри onMessageReceived не выполняйте длительную сетевую работу: документация FCM предупреждает о коротком окне обработки.
Реально запросите разрешение в Activity
Проверки в сервисе недостаточно: пользователь должен получить понятную возможность разрешить показ. Ниже минимальная тестовая MainActivity на ComponentActivity с AndroidX Activity и Core. В существующем приложении перенесите launcher и обработчик в свой экран, сохранив его интерфейс; это отдельный учебный экран с одной кнопкой.
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
class MainActivity : ComponentActivity() {
private val askNotifications = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
val text = if (granted) "Показ разрешён" else "Уведомления не разрешены"
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ensureUpdatesChannel(this)
setContentView(Button(this).apply {
text = "Разрешить уведомления"
setOnClickListener {
if (Build.VERSION.SDK_INT >= 33 &&
ContextCompat.checkSelfPermission(
this@MainActivity, Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED) {
askNotifications.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
Toast.makeText(this@MainActivity,
"Проверьте настройки канала и отправьте тест",
Toast.LENGTH_SHORT).show()
}
}
})
}
}Кнопка запускает системный запрос на Android 13+ при отсутствии разрешения. После согласия переведите приложение в фон и отправьте Console-тест. Если разрешение уже есть или устройство старше Android 13, отдельный запрос POST_NOTIFICATIONS не нужен, но настройки канала всё равно важны.
Отказ не обходят повторной отправкой push. При последующих обращениях учитывайте необходимость пояснения через shouldShowRequestPermissionRationale; если система больше не показывает диалог, предложите пользователю самостоятельно проверить настройки приложения. Не запрашивайте разрешение из FirebaseMessagingService или при каждом входящем сообщении.
Зарегистрируйте сервис в manifest
Объявление uses-permission помещается на уровне manifest, остальные элементы — внутри application. Если класс сервиса находится не в корневом пакете приложения, исправьте android:name.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Внутри application -->
<service android:name=".PushService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="updates" />default_notification_channel_id связывает фоновый показ FCM с подготовленным каналом. Убедитесь, что строка updates совпадает во всех местах. Декларация POST_NOTIFICATIONS в manifest не означает, что пользователь уже выдал runtime-разрешение.
Получите свежий токен и проверьте состояния
onNewToken вызывается при создании или обновлении токена, но для уже зарегистрированного экземпляра нужен также запрос текущего значения из Activity:
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token -> Log.d("PushTest", "Test token: $token") }
.addOnFailureListener { error -> Log.w("PushTest", "Token failed", error) }
Добавьте импорты FirebaseMessaging и Log. Полный токен выводите только в локальной диагностической сборке и уберите такой лог из рабочего варианта. После переустановки или очистки данных токен может измениться; не тестируйте новое приложение по старой скопированной строке.
В Firebase Console откройте DevOps & Engagement → Messaging, создайте notification campaign, задайте текст и выберите Send test message. Введите актуальный FCM registration token и нажмите Test. Устройство должно иметь совместимые Google Play services; проверьте сеть и разрешение показа.
| Тест legacy notification | Ожидаемый маршрут |
|---|---|
| Приложение в фоне | FCM SDK показывает уведомление; обычный onMessageReceived для этого сообщения не вызывается |
| Приложение открыто | Сообщение приходит в onMessageReceived, и наш код создаёт уведомление в канале updates |
| В notification нет body и отсутствует data.text | Наш обработчик завершится без собственного показа |
| У пользователя отключён показ | Наличие сообщения или токена не доказывает возможность увидеть уведомление |
Для legacy data-сообщений обработка отличается: они направляются в onMessageReceived, а комбинация notification + data в фоне показывает уведомление и передаёт data через extras запускающего Intent после нажатия. Данные нужно читать в соответствующем маршруте Activity; наш пример специальный разбор extras не реализует.
Диагностируйте по этапам
Нет успешной сборки — проверьте Gradle и JSON. Нет токена — сохраните ошибку регистрации, проверьте проект, applicationId, сеть и Play services. Токен есть, показа нет — проверьте runtime-разрешение, настройки канала и состояние приложения. В фоне отсутствует callback для notification — сначала сопоставьте это с таблицей, не переписывайте сервис вслепую.
Логируйте технический этап и идентификатор сообщения без личного содержимого. При переходе на V2 заново проверьте договорённость о типе сообщения и показе. Рабочая проверка должна объяснять весь путь: какой экземпляр получил сообщение, кто создал уведомление и почему Android мог его не показать.


