如何使用Salesforce SDK构建Kotlin移动应用程序和同步数据

146 阅读6分钟

这是我们由三部分组成的系列文章的最后一篇,展示了如何使用Salesforce Mobile SDK来构建与Salesforce平台一起使用的Android应用程序。在我们的第一篇文章中,我们向您展示了如何连接到您的组织。我们的第二篇文章向您展示了如何从您的应用程序编辑和添加数据到您的组织。这篇文章将向您展示如何将数据从您的Salesforce组织同步到您的移动设备,并处理网络丢失等情况。让我们直接进入正题!

使用移动同步工作

移动开发中最难的方面之一是处理数据同步问题。当你需要添加一个新的经纪人,但你是离线的,你如何处理这种情况?或者,如果两个经纪人正在更新同一个经纪人,你如何处理这两个变化的合并?

有了Salesforce Mobile SDK,这些现实世界中的问题都由一个叫做Mobile Sync的系统来为您处理。Mobile Sync让您将手机的本地数据映射到Salesforce中的数据;它还要求您定义获取和推送数据的操作--它称之为syncDownsyncUp

定义要同步的数据的形状

要开始使用移动同步,在res/raw 中创建一个文件,称为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"}
     ]
   }
 ]
}

这个文件定义了你手机上数据的形状,以及一些同步所需的额外元数据。

接下来,创建一个名为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"}
   }
 ]
}

这些是Mobile Sync在向下和向上同步数据时将使用的操作。

完成Mobile Sync过程的代码取决于几个因素,比如你想在什么时候执行同步,以及在设备失去(和恢复)连接时与Android的事件周期挂钩。

下面的代码样本将向你展示一个完整的例子,说明需要发生什么才能使同步工作,但它们应该被视为高层次的概念,而不一定是生产就绪的企业级代码。

设置定期同步

说到这里,让我们来看看如何实现同步。首先,在我们的MainActivity.kt 中的onResume(client: RestClient) 方法的末尾添加这一行。

setupPeriodicSync();

接下来,我们将在MainActivity 类中添加一个新变量和一个新函数。

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
   )
}

由于我们要在我们的函数中使用ContentResolver ,让我们确保导入它,在靠近MainActivity.kt 顶部的其他导入语句旁边添加这一行。

import.android.content.ContentResolver

我们已经定义了两个触发同步的方法。setupPeriodicSync 将每隔10 秒运行一次同步。这对生产环境来说太频繁了,但为了演示,我们将这样设置。

将同步操作映射到我们的数据和用户界面上

我们将一次性展示接下来的几个代码样本,并在之后讨论它们的作用。

app/java/com.example.sfdc ,创建一个名为BrokerSyncAdapter.kt 的新文件,并将这几行粘贴到其中。

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"
   }
}

现在,在同一个文件夹中,用这些行创建BrokerListLoader.kt

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()
   }
}

我们刚才做了什么?每个文件都有一个特定的角色,虽然你的应用程序的对象和字段肯定会有所不同,但这些类的功能和理念是相同的。

  • BrokerListLoader 负责将你在 中定义的同步操作与实际执行该工作的Kotlin代码进行映射。请注意,有 和 方法使用Mobile SDK的 来加载JSON文件并与Salesforce来回通信。brokersync.json syncUp syncDown SyncManager
  • BrokerSyncAdapter 也许最好的办法是把它看成是负责安排同步的代码。也就是说,它是 的入口,可以被UI元素(如在点击刷新按钮时)或Android系统事件(如连接丢失)调用。BrokerListLoader

最后,我们需要在我们的AndroidManifest.xml 文件中添加一行(在app/manifests )。我们的新同步功能将需要特殊的安卓权限,我们要求用户在运行时安装应用程序时允许这些权限。将以下一行与WRITE_SYNC_SETTINGS ,添加到你的清单的末尾。

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

测试同步

我们的最后一步是测试移动同步功能是否正常。在模拟器运行时,你应该登录并看到一个经纪商列表。这个列表应该是您在本地机器上的网络浏览器中看到的经纪人列表的镜像。在你的网络浏览器中,编辑其中一个经纪人的名字,并保存该更改。

image

然后,在你的模拟器中,你可以关闭手机电源或切换到另一个应用程序,然后再切换到你的sfdc-mobile-app 。你应该看到你的经纪商名称列表随着你在浏览器中的改变而更新。

image

这就是全部!同样,这只是一个基础性的构件,你可以从中学习。在短短的150行代码中,我们已经为一系列相当复杂的移动部件设计了一个解决方案。

  • 将Salesforce的自定义对象映射到JSON
  • 设置一个定时器,定期在Salesforce组织和移动设备之间来回同步数据
  • 处理网络连接方面的错误(并从问题中恢复)。

结论

Salesforce Mobile SDK 使得将移动设备与 Salesforce 数据连接起来变得非常容易。在这篇文章中,您学会了如何从您的手机上查询和操作数据,并看到结果即时反映在 Salesforce 上。你还了解了移动同步以及它在预测连接问题方面的作用。

然而,有了所有这些信息,Salesforce Mobile SDK 还能做很多事情。请看一下关于使用SDK的 完整文档。或者,如果你喜欢做更多的编码,有很多Trailhead教程 可以让你忙起来。我们迫不及待地想看到您的作品!