Use Firebase Cloud Messaging In Android Compose to Receive Notifications

You are currently viewing Use Firebase Cloud Messaging In Android Compose to Receive Notifications

In this post I will show you how I implemented Firebase Cloud Messaging in Android Compose to receive push notifications.

Firebase Cloud Messaging is useful to push notifications to a specific user or group of users. My previous post https://shift2dev.com/push-notifications-with-firebase-cloud-messaging-from-php-server/ shows how to implement the server side functionality to your project. This post will be referencing the client side implementation in Android Compose.

First you will need to create a project in your Firebase Console. Run through the setup wizard. You will need to download the google-services.json config file to include in your app. This file holds your Firebase Cloud Messaging API information to authenticate with Google.

Below I added Datastore to project to capture and retain the FCM device token. You can exclude if you want. It is not necessary to implement with Firebase Cloud Messaging but could be useful. In my case, I added this as a local way to identify if the app needs to request the token from startup and when the new token is dispatched. My project also ultimately stores the FCM tokens in a database. The tokens are sent/update in the database at the same times the TokenManager is updated.

Setup Reference:

https://firebase.google.com/docs/cloud-messaging/android/first-message

Configuration

You will need to add the google-services.json file to the root of your project. To do this, change the project view scope from Android to Project in the left pane.

Paste the file in the app directory

Next edit project level build.gradle and add the following

    alias(libs.plugins.gms.google.services) apply false

Edit the app level build.gradle and add the following

plugins {

...

    alias(libs.plugins.gms.google.services)
}

...

dependencies {

...

    //firebase
    implementation(platform(libs.firebase.bom))
    implementation(libs.firebase.analytics)
    implementation(libs.firebase.messaging.ktx)
    
    //datastore preferences
    implementation(libs.androidx.datastore.preferences)
    
...
    

Edit the libs.versions.toml

[versions]
...

gmsGoogleServices = "4.4.2"
firebaseBom = "33.8.0"

datastorePreferences = "1.1.2"

[libraries]

...

firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-messaging-ktx = { module = "com.google.firebase:firebase-messaging-ktx" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }

androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }

...

[plugins]

...

gms-google-services = { id = "com.google.gms.google-services", version.ref = "gmsGoogleServices" }

Edit the AndroidManifest.xml. We need to add permissions, the messaging service and some meta information for Firebase Cloud Messaging.

...
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.INTERNET" />

...

        <service
            android:name=".service.MyMessagingService"
            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="@string/default_notification_channel_id" />
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@mipmap/ic_launcher" />
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/dark_orange" />
    </application>

I have created a TokenManager. This will save the token for quick access if the app needs it later. As stated before, this is not necessary but I am including in case your project needs this functionality.

class TokenSingleton {

    companion object {

        lateinit var dataStore: DataStore<Preferences>

        @Volatile
        private var INSTANCE: TokenSingleton? = null

        fun getInstance(context : Context): TokenSingleton? {
            if (INSTANCE == null) {
                synchronized(this) {
                    if (INSTANCE == null) {
                        // create the singleton instance
                        INSTANCE = TokenSingleton()
                        dataStore = PreferenceDataStoreFactory.create(
                            corruptionHandler = ReplaceFileCorruptionHandler(
                                produceNewData = { emptyPreferences() },
                            ),
                            produceFile = {
                                context
                                    .preferencesDataStoreFile("test_fcm_datastore")
                            }
                        )
                    }
                }
            }
            return INSTANCE
        }
    }

    suspend fun getFCMToken() : String {
        val prefs = dataStore.data.first()
        return prefs[PrefKeys.FCM_TOKEN] ?: PrefKeys.EMPTY
    }

    suspend fun updateFCMToken(fcm_token : String){
        dataStore.edit {
            it[PrefKeys.FCM_TOKEN] = fcm_token
        }
    }

    suspend fun clearData(){
        dataStore.edit { it.clear() }
    }

}

private object PrefKeys {
    val FCM_TOKEN = stringPreferencesKey("fcm_token")
    val EMPTY = "HEADER.PAYLOAD.SIGNATURE"
}

In ViewModel, add the following functions

    public fun checkToken(event: UserIntent.updateCurrentUserData) {
        viewModelScope.launch {
        
            val fcm_token = runBlocking {
                TokenSingleton.getInstance(applicationContext)?.getFCMToken()
            }
            if(fcm_token == null || fcm_token.equals("HEADER.PAYLOAD.SIGNATURE")){
                getFCMToken()
            }
        }
    }

    private fun getFCMToken(event: UserIntent.getFCMToken){
        viewModelScope.launch {
            //val token = Firebase.messaging.token.await()
            Firebase.messaging.token.addOnCompleteListener { task ->
                if(!task.isSuccessful){
                    Log.d("FCM token", "Failed to generate token")
                    return@addOnCompleteListener
                }
                val token = task.result
                Log.d("FCM token:", token)
                runBlocking {
                    TokenSingleton.getInstance(applicationContext)?.updateFCMToken(token)
                }
            }

        }
    }

Create MyMessagingService – this is what creates the notification as well as updates the FCM token if a new token is dispatched

class MyMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)

        Log.d("FCM token:", token)
        runBlocking {
            TokenSingleton.getInstance(applicationContext)?.updateFCMToken(token)
        }

    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        message.notification?.let { msg ->
            createNotification(msg)
        }
    }

    private fun createNotification(message: RemoteMessage.Notification) {
        MyNotificationBuilder.create(
            this@MyMessagingService,
            applicationContext.resources.getInteger(R.integer.fcm_notification),
            getString(R.string.default_notification_channel_id),
            message.title.toString(),
            message.body.toString(),
            "FCM Test",
            "FCM Test Description",
            NotificationManager.IMPORTANCE_DEFAULT
        )
    }

    override fun onDeletedMessages() {
        super.onDeletedMessages()
    }
}

Create MyNotificationBuilder – this does exactly what the name implies, builds the notification

object MyNotificationBuilder {

    fun create(
        context: Context,
        notifyID : Int,
        channelId: String,
        title: String,
        content: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ) {
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.createNotificationChannel(
                createNotificationChannel(
                    channelId,
                    channelName,
                    channelDesc,
                    importance
                )
            )
        }
        val pendingIntent: PendingIntent =
            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)

        val builder = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle(title)
            .setContentText(content)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
        notificationManager.notify(notifyID, builder.build())
    }

    @SuppressLint("NewApi")
    private fun createNotificationChannel(
        channelId: String,
        channelName: String,
        channelDesc: String,
        importance: Int
    ): NotificationChannel {
        val channel = NotificationChannel(channelId, channelName, importance).apply {
            description = channelDesc
        }
        return channel
    }
}

Now you will just need to find a good place to call the ViewModel function to get a FCM token. I added a DisposableEffect that calls the function in onCreate

                val lifecycleOwner = LocalLifecycleOwner.current

                DisposableEffect(key1 = lifecycleOwner) {
                    val observer = LifecycleEventObserver { _, event ->
                      
                        if (event == Lifecycle.Event.ON_CREATE) {
                            viewModel.checkToken()
                        }

                    }

                    lifecycleOwner.lifecycle.addObserver(observer)
                    onDispose {
                        lifecycleOwner.lifecycle.removeObserver(observer)
                    }
                }

Test

To test the notification, you will need to go to your Firebase Console and create a new messaging campaign. The link at start of post will walk you through this process.

Hope this was useful.

Leave a Reply