paint-brush
Building a Kotlin Mobile App with the Salesforce SDK: Synchronizing Databy@MichaelB
487 reads
487 reads

Building a Kotlin Mobile App with the Salesforce SDK: Synchronizing Data

by MichaelMay 2nd, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This is our final post in our three-part series demonstrating how to use the Salesforce Mobile SDK to build an Android app that works with Salesforce platform. This post will show you how to synchronize data from your Salesforce org to your mobile device and handle scenarios such as network loss. Mobile Sync has you map your phone’s local data to the data in Salesforce. It also requires you to define operations for fetching and pushing data—what it calls `syncDown` and `syncUp.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Building a Kotlin Mobile App with the Salesforce SDK: Synchronizing Data
Michael HackerNoon profile picture

This is our final post in our three-part series demonstrating how to use the Salesforce Mobile SDK to build an Android app that works with the Salesforce platform. In our first post, we showed you how to connect to your org. Our second post showed you how to edit and add data to your org from your app. This post will show you how to synchronize data from your Salesforce org to your mobile device and handle scenarios such as network loss. Let’s get right into it!

Working with Mobile Sync

One of the hardest aspects of mobile development is dealing with data synchronization. How do you handle the situation when you need to add a new broker, but you’re offline? Or what if two agents are updating the same broker—how can you handle the merging of those two changes?


With the Salesforce Mobile SDK, these real-world issues are handled for you by a system called Mobile Sync. Mobile Sync has you map your phone’s local data to the data in Salesforce; it also requires you to define operations for fetching and pushing data—what it calls syncDown and syncUp.

Define the shape of data to be synced

To get started with Mobile Sync, create a file in res/raw called brokerstore.json:


{
 "soups": [
   {
     "soupName": "brokers",
     "indexes": [
       { "path": "Id", "type": "string"},
       { "path": "Name", "type": "string"},
       { "path": "Title__c", "type": "string"},
       { "path": "Phone__c", "type": "string"},
       { "path": "Mobile_Phone__c", "type": "string"},
       { "path": "Email__c", "type": "string"},
       { "path": "Picture__c", "type": "string"},
       { "path": "__local__", "type": "string"},
       { "path": "__locally_created__", "type": "string"},
       { "path": "__locally_updated__", "type": "string"},
       { "path": "__locally_deleted__", "type": "string"},
       { "path": "__sync_id__", "type": "integer"}
     ]
   }
 ]
}


This file defines the shape of the data on your phone, as well as some additional metadata that’s necessary for the sync.


Next, create a file called brokersync.json:


{
 "syncs": [
   {
     "syncName": "syncDownBrokers",
     "syncType": "syncDown",
     "soupName": "brokers",
     "target": {"type":"soql", "query":"SELECT Name, Title__c, Phone__c, Mobile_Phone__c, Email__c, Picture__c FROM Broker__c LIMIT 10000"},
     "options": {"mergeMode":"OVERWRITE"}
   },
   {
     "syncName": "syncUpBrokers",
     "syncType": "syncUp",
     "soupName": "brokers",
     "target": {"createFieldlist":["Name", "Title__c", "Phone__c", "Mobile_Phone__c", "Email__c", "Picture__c"]},
     "options": {"fieldlist":["Id", "Name", "Title__c", "Phone__c", "Mobile_Phone__c", "Email__c", "Picture__c"], "mergeMode":"LEAVE_IF_CHANGED"}
   }
 ]
}


These are the operations that Mobile Sync will use when syncing data down and up.


The code to complete the Mobile Sync process depends on several factors, such as when you want to perform a synchronization, as well as hooking into the Android event cycle when a device loses (and regains) connectivity.


The following code samples will show you a complete example of what needs to happen to get synchronization working, but they should be considered high-level concepts, and not necessarily production-ready enterprise-grade code.

Set up periodic sync

With that said, let’s take a look at how to implement synchronization. First, add this line to the end of our onResume(client: RestClient) method in MainActivity.kt:


setupPeriodicSync();


Next, we’ll add a new variable and a new function to the MainActivity class:


private val SYNC_CONTENT_AUTHORITY =
   "com.salesforce.samples.mobilesyncexplorer.sync.brokersyncadapter"

private fun setupPeriodicSync() {
   val account = MobileSyncSDKManager.getInstance().userAccountManager.currentAccount

   ContentResolver.setSyncAutomatically(account, SYNC_CONTENT_AUTHORITY, true)
   ContentResolver.addPeriodicSync(
       account, SYNC_CONTENT_AUTHORITY,
       Bundle.EMPTY, 10
   )
}


Since we’re using ContentResolver in our function, let’s make sure to import it by adding this line alongside the other import statements near the top of MainActivity.kt:


import.android.content.ContentResolver


We have defined two methods that trigger synchronization. setupPeriodicSync will run a sync every 10 seconds. This is much too frequent for a production environment, but we’ll set it this way for demonstration purposes.

Mapping sync operations to our data and UI

We will show the next few code samples all at once, and discuss what they’re doing afterward.

In app/java/com.example.sfdc, create a new file called BrokerSyncAdapter.kt and paste these lines into it:


package com.example.sfdc

import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.accounts.UserAccountManager
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.example.sfdc.BrokerListLoader

class BrokerSyncAdapter
   (
   context: Context?, autoInitialize: Boolean,
   allowParallelSyncs: Boolean
) :
   AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
   override fun onPerformSync(
       account: Account, extras: Bundle, authority: String,
       provider: ContentProviderClient, syncResult: SyncResult
   ) {
       val syncDownOnly = extras.getBoolean(SYNC_DOWN_ONLY, false)
       val sdkManager = SalesforceSDKManager.getInstance()
       val accManager = sdkManager.userAccountManager
       if (sdkManager.isLoggingOut || accManager.authenticatedUsers == null) {
           return
       }
       if (account != null) {
           val user = sdkManager.userAccountManager.buildUserAccount(account)
           val contactLoader = BrokerListLoader(context, user)
           if (syncDownOnly) {
               contactLoader.syncDown()
           } else {
               contactLoader.syncUp() // does a sync up followed by a sync down
           }
       }
   }

   companion object {
       // Key for extras bundle
       const val SYNC_DOWN_ONLY = "syncDownOnly"
   }
}


Now, in that same folder, create BrokerListLoader.kt with these lines:


package com.example.sfdc

import android.content.AsyncTaskLoader
import android.content.Context
import android.content.Intent
import android.util.Log
import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager.MobileSyncException
import com.salesforce.androidsdk.mobilesync.manager.SyncManager.SyncUpdateCallback
import com.salesforce.androidsdk.mobilesync.util.SyncState
import com.salesforce.androidsdk.smartstore.store.QuerySpec
import com.salesforce.androidsdk.smartstore.store.SmartSqlHelper.SmartSqlException
import com.salesforce.androidsdk.smartstore.store.SmartStore
import org.json.JSONArray
import org.json.JSONException
import java.util.ArrayList

class BrokerListLoader(context: Context?, account: UserAccount?) :
   AsyncTaskLoader<List<String>?>(context) {
   private val smartStore: SmartStore
   private val syncMgr: SyncManager
   override fun loadInBackground(): List<String>? {
       if (!smartStore.hasSoup(BROKER_SOUP)) {
           return null
       }
       val querySpec = QuerySpec.buildAllQuerySpec(
           BROKER_SOUP,
           "Name", QuerySpec.Order.ascending, LIMIT
       )
       val results: JSONArray
       val brokers: MutableList<String> = ArrayList<String>()
       try {
           results = smartStore.query(querySpec, 0)
           for (i in 0 until results.length()) {
               brokers.add(results.getJSONObject(i).getString("Name"))
           }
       } catch (e: JSONException) {
           Log.e(TAG, "JSONException occurred while parsing", e)
       } catch (e: SmartSqlException) {
           Log.e(TAG, "SmartSqlException occurred while fetching data", e)
       }
       return brokers
   }

   @Synchronized
   fun syncUp() {
       try {
           syncMgr.reSync(
               SYNC_UP_NAME
           ) { sync ->
               if (SyncState.Status.DONE == sync.status) {
                   syncDown()
               }
           }
       } catch (e: JSONException) {
           Log.e(TAG, "JSONException occurred while parsing", e)
       } catch (e: MobileSyncException) {
           Log.e(TAG, "MobileSyncException occurred while attempting to sync up", e)
       }
   }

   /**
    * Pulls the latest records from the server.
    */
   @Synchronized
   fun syncDown() {
       try {
           syncMgr.reSync(
               SYNC_DOWN_NAME
           ) { sync ->
               if (SyncState.Status.DONE == sync.status) {
                   fireLoadCompleteIntent()
               }
           }
       } catch (e: JSONException) {
           Log.e(TAG, "JSONException occurred while parsing", e)
       } catch (e: MobileSyncException) {
           Log.e(TAG, "MobileSyncException occurred while attempting to sync down", e)
       }
   }

   private fun fireLoadCompleteIntent() {
       val intent = Intent(LOAD_COMPLETE_INTENT_ACTION)
       SalesforceSDKManager.getInstance().appContext.sendBroadcast(intent)
   }

   companion object {
       const val BROKER_SOUP = "brokers"
       const val LOAD_COMPLETE_INTENT_ACTION =
           "com.salesforce.samples.mobilesyncexplorer.loaders.LIST_LOAD_COMPLETE"
       private const val TAG = "BrokerListLoader"
       private const val SYNC_DOWN_NAME = "syncDownBrokers"
       private const val SYNC_UP_NAME = "syncUpBrokers"
       private const val LIMIT = 10000
   }

   init {
       val sdkManager = MobileSyncSDKManager.getInstance()
       smartStore = sdkManager.getSmartStore(account)
       syncMgr = SyncManager.getInstance(account)
       // Setup schema if needed
       sdkManager.setupUserStoreFromDefaultConfig()
       // Setup syncs if needed
       sdkManager.setupUserSyncsFromDefaultConfig()
   }
}


What did we just do? Each file has a specific role, and while the objects and fields will certainly be different for your app, the functions and philosophies of these classes are the same:


  • BrokerListLoader is responsible for mapping the sync operations you defined in brokersync.json with the Kotlin code that will actually perform the work. Notice that there are syncUp and syncDown methods that use the Mobile SDK’s SyncManager to load the JSON files and communicate back and forth with Salesforce.
  • BrokerSyncAdapter can perhaps best be thought of as the code that’s responsible for scheduling the synchronization. That is, it’s the entry point for BrokerListLoader, and can be called by UI elements (such as during a refresh button click) or Android system events (such as connectivity loss).


Lastly, we need to add one line to our AndroidManifest.xml file (in app/manifests). Our new sync functionality will need special Android permissions, which we ask the user to allow upon app installation at runtime. Add the following line with WRITE_SYNC_SETTINGS to the end of your manifest:


  <uses-permission android:name="com.example.sfdc.C2D_MESSAGE" />
  <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
</manifest>

Testing sync

Our last step is to test that mobile sync works. With the emulator running, you should be logged in and see a list of brokers. This list should mirror the broker list that you see in your web browser on your local machine. In your web browser, edit one of the broker names, and save that change.



Then, in your emulator, you can power off the phone or switch to another application and then switch back to your sfdc-mobile-app. You should see your list of broker names updated with the change you made in your browser:



And that’s all! Again, this is just a foundational building block for you to learn from. In just about 150 lines of code, we’ve devised a solution for a rather complicated series of moving parts:


  • Mapping Salesforce custom objects to JSON
  • Setting up a timer to periodically sync data back and forth between a Salesforce org and a mobile device
  • Handling errors (and recovering from issues) around network connectivity

Conclusion

The Salesforce Mobile SDK makes it tremendously easy to connect mobile devices with Salesforce data. In this post, you learned how to query and manipulate data from your phone, and saw the results reflected instantaneously on Salesforce. You also learned about Mobile Sync and its role in anticipating issues with connectivity.


And yet, with all of this information, there’s still so much more that the Salesforce Mobile SDK can do. Take a look at the complete documentation on working with the SDK. Or, if you prefer to do more coding, there are plenty of Trailhead tutorials to keep you busy. We can’t wait to see what you build!