Use Android Datastore and FCM For Realtime App Updates
In this post I show you how to use Firebase Cloud Messaging and the Android Datastore to provide realtime UI updates in app.

In this post I will show you how I implemented a way to use Android Datastore and FCM ( Firebase Cloud Messaging ) to provide real time UI updates.

The Need

I needed a way for my app to update the UI as new data is available. The app utilizes Firebase Cloud Messaging to flag there is new data available via a notification. It also needed a way to have it update the UI without user intervention. I decided to implement a switching boolean. Utilizing the data payload of the Firebase Cloud Messaging notification, I can send such a flag. When the notification is received, it will switch an Android Datastore preference boolean from false to true.

Resources

Below are posts where I have covered Firebase Cloud Messaging setup in app as well as on PHP server along with implementing Android Datastore. This post focuses on piecing some of these parts together.

Use FCM In Android Compose to Receive Push Notifications

Push Notifications With Firebase Cloud Messaging From PHP

Implementing A Singleton of Android DataStore To Save JWT

Code

UserPrefs

Create a new Android Datastore. I named it UserPrefs. To access the boolean used to create the "switching effect" I created a Flow<Boolean>. This is then created into a StateFlow in the ViewModel.

class UserPrefs {

    companion object {

        lateinit var dataStore: DataStore<Preferences>

        @Volatile
        private var INSTANCE: UserPrefs? = null

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

    fun getDoSyncUpdateFlow() =  dataStore.data.map {
        it[PrefKeys.DO_SYNC_UPDATE] ?: false
    }

    suspend fun updateDoSyncUpdate(doSyncUpdate : Boolean){
        dataStore.edit {
            it[PrefKeys.DO_SYNC_UPDATE] = doSyncUpdate
        }
    }

}

private object PrefKeys {
    val DO_SYNC_UPDATE = booleanPreferencesKey("do_sync_update")
}

UserViewModel

I initialized the UserPrefs Datastore in the ViewModel so I can expose the Flow to the Activity. This also allows me to update the value when needed.

class UserViewModel(val applicationContext: Application) : ViewModel() {

...

    val userPrefs = UserPrefs.getInstance(applicationContext)
    val getDoSyncUpdate = userPrefs?.getDoSyncUpdateFlow()
        ?.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
        
...

}

Activity

To access the StateFlow in the main Composable of the Activity

val doSyncRequest = viewModel.getDoSyncUpdate?.collectAsStateWithLifecycle()?.value

The default value is false, so we will listen when the value changes to true then run the appropriate UI update method.

if(doSyncRequest == true){
  //run UI update method here

}

Inside the above code is where you would place your method to update the UI, most likely through your ViewModel.

Below is what to place in your method after the UI is updated in your ViewModel to toggle the doSyncRequest boolean back to false. This will then allow the process to start over as needed.

runBlocking {
    userPrefs?.updateDoSyncUpdate(false)
}

PHP

To supply the FCM data payload with a unique id to inform us this is a UI update notification. This is done on your PHP server that builds and sends the notification to your app.

In this script we are sending the data payload with a key named "id". The value is just some random number but you can change to fit your needs.

<?php

require_once 'vendor/autoload.php'; 

use Google\Auth\Credentials\ServiceAccountCredentials;

class SendFCMMessages {

        function sendForSyncRequest($dev_token){
    
            $title = "Title";
            $body = "Notification Body";
    
            $access_token = $this->getAccessToken();
            if($access_token !== false){
                $this->sendForSyncRequestNotification($dev_token, $title, $body, $access_token); 
            }
        }
    
        function getAccessToken() {

             $scopes = [\Google\Service\FirebaseCloudMessaging::FIREBASE_MESSAGING]; // Define the scopes you need
            $credentials = new ServiceAccountCredentials($scopes, [
                'client_email' => $_ENV['fcm_client_email'],
                'private_key' => $_ENV['fcm_pk']
            ]); 
            
            return $credentials->fetchAuthToken()['access_token'];
        }   
        
        function sendForSyncRequestNotification($dev_token, $title, $body, $accessToken) {
    
            $payload = [
                'title' => $title,
                'body' => $body,
            ];
    
            //custom data fields
            $data = [
                'id' => "-10"
            ];
    
            $fcm = [
                'message' => [
                'token' => $dev_token,
                    'notification' => $payload,
                    'data' => $data
                ],
            ];
                
            $headers = [
                'Authorization: Bearer ' . $accessToken,
                'Content-Type: application/json'
            ];
    
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, 'https://fcm.googleapis.com/v1/projects/'.$_ENV['fcm_project_id'].'/messages:send');
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fcm));
            $result = curl_exec($ch);
            curl_close($ch);
        }

?>        

Now to run the script above, use the following in your server code when you want to send a notification to your app.

NOTE

-The include is the file for the PHP script above.

-The fcm_token is selected from a database that holds the user FCM tokens.

include("send_fcm_msg.php");
$fcmMessager = new SendFCMMessages();
$fcmMessager->sendForSyncRequest(
    $row['fcm_token']);

Back To Android App

We are now going to capture the message using a service that we created utilizing the FCM library.

class MyMessagingService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        //update users db on server
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        if(message.data.isNotEmpty()){
            val userPrefs = UserPrefs.getInstance(applicationContext)
            if(message.data["id"] == "-10"){
                runBlocking {
                    userPrefs?.updateDoSyncUpdate(true)
                }
            }
        }
        //if needed, create notification for user here
    }

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

All done.

Recap

  1. When the data is updated on the server, the server will create and send a notification via FCM to the app.
  2. The app will receive the notification and check if the data payload with a key of "id" is equal to -10. If so, then the service will switch the preference boolean to true.
  3. While the ViewModel is listening to the StateFlow of the Datastore preference, it will read the switch to true and run the UI update method you desire.
  4. When the method is finished running, it will finally switch the preference item back to false so it is ready to listen again.

Hope this was helpful.

Leave a Reply

Your email address will not be published. Required fields are marked *