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

На чтение
9 мин
Дата обновления
05.11.2026

Интеграция 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 мог его не показать.