trailhead

Learning Objectives

After completing this unit, you'll be able to:

  • Understand basic SmartSync Data Framework concepts.
  • Use the SmartSync Data Framework library to synchronize data between the user’s local device and the Salesforce server.

Using SmartSync Data Framework to Synchronize Offline Changes

The SmartSync Data Framework library makes it easy for customers to synchronize Salesforce data on their mobile apps with Salesforce servers. In particular, SmartSync Data Framework enables customers whose devices have lost connectivity to continue working on Salesforce data. When connectivity is restored, SmartSync Data Framework synchronizes any changes made to local records in SmartStore with the corresponding records on the Salesforce server. SmartSync Data Framework predefines cache policies for fine-tuning interactions between cached data and server data in offline and online scenarios. SmartSync Data Framework convenience methods automate common network activities—fetching sObject metadata, fetching a list of most recently used objects, and building SOQL and SOSL queries.

Using SmartSync Data Framework in Native iOS Apps

When you create native apps with forceios, you get SmartSync Data Framework for free—forceios automatically includes the SmartSync Data Framework library. You, the developer, decide whether to use its functionality in your app. If you do take advantage of SmartSync Data Framework, you use the Sync Manager to synchronize data between the user’s local device and the Salesforce server. The Sync Manager provides methods for syncing down (from the server to the device) or up (from the device to the server).

All data requests in SmartSync Data Framework apps are asynchronous. “Asynchronous” means that the sync method you call returns the server’s response in a callback method or update block that you define. Each sync-down or sync-up method call returns a sync state object that contains the following information:

  • Sync operation ID. You can check the progress of the operation at any time by passing this ID to the sync manager’s getSyncStatus method.
  • Your sync parameters (soup name, target for sync down operations, options for sync up operations).
  • Type of operation (up or down).
  • Progress percentage (integer, 0–100).
  • Total number of records in the transaction.

Syncing Down

To download sObjects from the server to your local SmartSync Data Framework soup, use one of the following SFSmartSyncSyncManager methods:

- (SFSyncState*) 
    syncDownWithTarget:(SFSyncTarget*)target 
              soupName:(NSString*)soupName 
           updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock; 

- (SFSyncState*) 
syncDownWithTarget:(SFSyncTarget*)target 
           options:(SFSyncOptions*)options
          soupName:(NSString*)soupName 
       updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock; 

You define a target that provides the list of sObjects to be downloaded. To provide an explicit list, you use an NSDictionary instance. Otherwise, you can define the target with a query string. The SFSyncTarget class provides factory methods for creating target objects from a SOQL, SOSL, or MRU query. Mobile SDK reports the progress of the sync operation through the callback method or update block that you provide.

In addition, you specify the name of the SmartStore soup that receives the downloaded data. This soup is required to have an indexed string field named __local__. The options parameter lets you control what happens to locally modified records. You can choose one of the following behaviors:

  • Overwrite modified local records and lose all local changes.
  • Preserve all local changes and locally modified records.
Important

Important

If you use the syncDownWithTarget:soupName:updateBlock: method, which doesn’t take an options parameter, SmartSync Data Framework can overwrite existing sObjects in the cache. To preserve local changes in such cases, always sync up before syncing down.

Syncing Up

To apply local changes on the server using the default sync up target, use the following SFSmartSyncSyncManager method:

- (SFSyncState*) 
    syncUpWithOptions:(SFSyncOptions*)options 
             soupName:(NSString*)soupName 
          updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock;
If you’ve implemented a custom sync up target, you can use it by calling this SFSmartSyncSyncManager method:
- (SFSyncState*) syncUpWithTarget:(SFSyncUpTarget*)target
                          options:(SFSyncOptions*)options
                         soupName:(NSString*)soupName
                      updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock;

This method updates the server with data from the given SmartStore soup. It looks for created, updated, or deleted records in the soup and replicates those changes on the server. The options parameter specifies a list of fields to be updated. Locally created objects must include an attributes field containing a type field that specifies the sObject type. For example, for an account named Acme, use:

{Id:”local_x”, Name: Acme, attributes: {type:”Account”}}

Example

The native SmartSyncExplorer sample app demonstrates how to use SmartSync Data Framework with Contact records. In iOS, this sample defines a ContactSObjectData class that represents a Salesforce Contact record as an Objective-C object. The sample also defines several classes that support the ContactSObjectData class:
  • ContactSObjectDataSpec
  • SObjectData
  • SObjectDataSpec
  • SObjectDataFieldSpec
  • SObjectDataManager
To sync Contact data down to the SmartStore soup, the refreshRemoteData method of SObjectDataManager creates a SFSyncDownTarget object using a SOQL string. This query string is built with information from the Contact object. The syncDownWithTarget:options:soupName:updateBlock: method of SFSmartSyncSyncManager takes this target and the name of the soup that receives the returned data. In this example, the target specifies one option, SFSyncStateMergeModeLeaveIfChanged, to prevent SmartSync Data Framework from overwriting local changes. This method also requires an update block that is called when the sync operation has either succeeded or failed. Here, the update block saves the sync operation ID so that the method can call the reSync:updateBlock: method after the first sync down operation.
- (void)refreshRemoteData:(void (^)(void))completionBlock {
    if (![self.store soupExists:self.dataSpec.soupName]) {
        [self registerSoup];
    }

    __weak SObjectDataManager *weakSelf = self;
    SFSyncSyncManagerUpdateBlock updateBlock = ^(SFSyncState* sync) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if ([sync isDone] || [sync hasFailed]) {
            strongSelf.syncDownId = sync.syncId;
            [strongSelf refreshLocalData:completionBlock];
        }
    };
    if (self.syncDownId == 0) {
        // First time.
        NSString *soqlQuery = 
            [NSString stringWithFormat:@"SELECT %@ FROM %@ LIMIT %lu", 
                [self.dataSpec.fieldNames componentsJoinedByString:@","], 
                self.dataSpec.objectType, 
                (unsigned long)kSyncLimit];
        SFSyncOptions *syncOptions = 
            [SFSyncOptions 
                newSyncOptionsForSyncDown:SFSyncStateMergeModeLeaveIfChanged];
        SFSyncDownTarget *syncTarget = 
            [SFSoqlSyncDownTarget newSyncTarget:soqlQuery];
        [self.syncMgr syncDownWithTarget:syncTarget 
                                 options:syncOptions 
                                soupName:self.dataSpec.soupName 
                             updateBlock:updateBlock];
    } else {
        // Subsequent times.
        [self.syncMgr reSync:[NSNumber numberWithInteger:self.syncDownId] 
                 updateBlock:updateBlock];
    }
}

If the sync down operation succeeds—that is, if the isDone method of SFSyncState returns YES—the specified soup receives the server data. The update block then passes control to the refreshLocalData method, which retrieves the data from the soup and updates the UI to reflect any changes.

- (void)refreshLocalData {
    if (![self.store soupExists:self.dataSpec.soupName]) {
        [self registerSoup];
    }
    
    SFQuerySpec *sobjectsQuerySpec = 
        [SFQuerySpec 
            newAllQuerySpec:self.dataSpec.soupName 
                   withOrderPath:self.dataSpec.orderByFieldName  
                  withOrder:kSFSoupQuerySortOrderAscending 
               withPageSize:kMaxQueryPageSize];
    NSError *queryError = nil;
    NSArray *queryResults = 
        [self.store 
            queryWithQuerySpec:sobjectsQuerySpec 
                     pageIndex:0 
                         error:&queryError];
    [self log:SFLogLevelDebug 
          msg:@"Got local query results. "
               "Populating data rows."];
    if (queryError) {
        [self log:SFLogLevelError 
              format:@"Error retrieving '%@' data "
                  "from SmartStore: %@", 
                  self.dataSpec.objectType, 
                  [queryError localizedDescription]];
        return;
    }
    
    self.fullDataRowList = [self populateDataRows:queryResults];
    [self log:SFLogLevelDebug 
        format:@"Finished generating data rows. "
                "Number of rows: %d.  Refreshing view.", 
                [self.fullDataRowList count]];
    self.dataRows = [self.fullDataRowList copy];
    if (completionBlock) completionBlock();
}

When it’s time to sync up to the server, you send the syncUp:withOptions:soupName:updateBlock: message to SFSmartSyncSyncManager with the same arguments used for syncing down: list of fields, name of source SmartStore soup, and an update block. The only coding difference is that you format the list of affected fields as an instance of SFSyncOptions instead of SFSyncTarget. Here’s how the SmartSyncExplorer sample sends the sync up message:

- (void)updateRemoteData:
    (SFSyncSyncManagerUpdateBlock)completionBlock {
    
    SFSyncOptions *syncOptions = 
        [SFSyncOptions 
            newSyncOptionsForSyncUp:self.dataSpec.fieldNames  
                          mergeMode:SFSyncStateMergeModeLeaveIfChanged]];
    [self.syncMgr syncUpWithOptions:syncOptions 
        soupName:self.dataSpec.soupName 
        updateBlock:^(SFSyncState* sync) {
            if ([sync isDone] || [sync hasFailed]) {
                completionBlock(sync);
            }
        }
    ];
}

If the update block provided here determines that the sync operation has finished, it calls the completion block that’s passed into updateRemoteData. A user initiates a syncing operation by tapping a button. Therefore, to see the definition of the completion block, look at the syncUpDown button handler in ContactListViewController.m. The handler calls updateRemoteData with the following block.

[self.dataMgr updateRemoteData:^(SFSyncState *syncProgressDetails) 
{
    dispatch_async(dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        strongSelf.navigationItem.rightBarButtonItem.enabled = YES;

        // When the sync failed it means not everything could be synced up
	 // it doesn't necessarily mean nothing could be synced up
	 // Therefore we refresh regardless of success status
	 [strongSelf.dataMgr refreshLocalData:completionBlock];
	 [strongSelf.dataMgr refreshRemoteData:completionBlock]; 

        // We’ll again call refreshLocalData when completing

	 // Letting the user know whether the sync succeeded
	 if ([syncProgressDetails isDone]) {
	     [strongSelf showToast:@"Sync complete!"];
	 } else if ([syncProgressDetails hasFailed]) {
	     [strongSelf showToast:@"Sync failed."];
	 }
    });
}];

If the sync up operation succeeded, this block first refreshes the display on the device, along with a “Sync complete!” confirmation toast. Regardless of the status of the sync operation, this method refreshes local and remote data. This step covers any partial successes as well as completions.

Using SmartSync Data Framework in Native Android Apps

You use the Sync Manager to synchronize data between the user’s local device and the Salesforce server. The Sync Manager provides methods for syncing down (from the server to the device) or up (from the device to the server).

All data requests in SmartSync Data Framework apps are asynchronous. “Asynchronous” means that the sync method you call returns the server’s response in a callback method or update block that you define. Each sync-down or sync-up method returns a sync state object containing the following information:

  • Sync operation ID. You can check the progress of the operation at any time by passing this ID to the sync manager’s getSyncStatus method.
  • Your sync parameters (soup name, target for sync down operations, options for sync up operations).
  • Type of operation (up or down).
  • Progress percentage (integer, 0–100).
  • Total number of records in the transaction.

Syncing Down

To download sObjects from the server to your local SmartSync Data Framework soup, use one of the following SyncManager methods:

public SyncState syncDown(SyncTarget target, String soupName, 
    SyncUpdateCallback callback) throws JSONException;

public SyncState syncDown(SyncTarget target, SyncOptions options, 
    String soupName, SyncUpdateCallback callback) 
    throws JSONException;

You define a target that provides the list of sObjects to be downloaded. To provide an explicit list, use a JSONObject. Alternatively, you can define the target with a query string. The SFSyncTarget class provides factory methods for creating target objects from a SOQL, SOSL, or MRU query. Mobile SDK reports the progress of the sync operation through the callback method or update block that you provide.

In addition, you specify the name of the SmartStore soup that receives the downloaded data. This soup is required to have an indexed string field named __local__. The options parameter lets you control what happens to locally modified records. You can choose one of the following behaviors:

  • Overwrite modified local records and lose all local changes. Set the options parameter to the following value: SyncOptions.optionsForSyncDown(MergeMode.OVERWRITE)
  • Preserve all local changes and locally modified records. Set the options parameter to the following value: SyncOptions.optionsForSyncDown(MergeMode.LEAVE_IF_CHANGED)
Important

Important

If you use a version of syncDown that doesn’t take an options parameter, existing sObjects in the cache can be overwritten. To preserve local changes, always run sync up before running sync down.

Syncing Up

To apply local changes on the server, use the following SyncManager method:

public SyncState syncUp(SyncUpTarget target, SyncOptions options, String soupName, 
    SyncUpdateCallback callback) throws JSONException

This method updates the server with data from the given SmartStore soup. It looks for created, updated, or deleted records in the soup and replicates those changes on the server. The options parameter specifies a list of fields to be updated. Locally created objects must include an attributes field containing a type field that specifies the sObject type. For example, for an account named Acme, use:

{Id:”local_x”, Name: Acme, attributes: {type:”Account”}}

Example

The native SmartSyncExplorer sample app demonstrates how to use SmartSync Data Framework with Contact records. In Android, it defines a ContactObject class that represents a Salesforce Contact record as a Java object. To sync Contact data down to the SmartStore soup, the syncDown() method creates a sync target from a SOQL query that’s built with information from the ContactObject instance.

In the following snippet, note the use of SOQLBuilder. SOQLBuilder is a SmartSync Data Framework factory class that makes it easy to specify a SOQL query dynamically in a format that reads like an actual SOQL string. Each SOQLBuilder property setter returns a new SOQLBuilder object built from the calling object, which allows you to chain the method calls in a single logical statement. After you’ve specified all parts of the SOQL query, you call build() to create the final SOQL string.

public synchronized void syncDown() {
	
    smartStore.registerSoup(ContactListLoader.CONTACT_SOUP, CONTACTS_INDEX_SPEC);
       
    final SyncUpdateCallback callback = new SyncUpdateCallback() {

           
        @Override
           
        public void onUpdate(SyncState sync) {
	        
            if (Status.DONE.equals(sync.getStatus())) {
	        	
                fireLoadCompleteIntent();
	        
            }
           
        }
       
    };
       
    try {
           
        if (syncId == -1) {
               
            final SyncOptions options = 
                SyncOptions.optionsForSyncDown(SyncState.MergeMode.LEAVE_IF_CHANGED);
               
            final String soqlQuery = 
                SOQLBuilder.getInstanceWithFields(
                    ContactObject.CONTACT_FIELDS_SYNC_DOWN).from(Constants.CONTACT).limit(ContactListLoader.LIMIT).build();
               
            final SyncDownTarget target = new SoqlSyncDownTarget(soqlQuery);
               
            final SyncState sync = syncMgr.syncDown(target, options,
               		
                ContactListLoader.CONTACT_SOUP, callback);
               
            syncId = sync.getId();
           
        } else {
               
            syncMgr.reSync(syncId, callback);
           
        }
       
    } catch (JSONException e) {
           
        Log.e(TAG, "JSONException occurred while parsing", e);
       
    } catch (SmartSyncException e) {
           
        Log.e(TAG, "SmartSyncException occurred while attempting to sync down", e);
	
    }

}

If the sync down operation succeeds—that is, if SyncState.isDone() equals true—the received data goes into the specified soup. The callback method then only needs to fire an intent that reloads the data in the Contact list.

When it’s time to sync up to the server, you call syncUp() with the same arguments as syncDown(): list of fields, name of source SmartStore soup, and an update callback. The only coding difference is that you format the list of affected fields as an instance of SyncOptions instead of SyncTarget. Here’s the way it’s handled in the SmartSyncExplorer sample:

public synchronized void syncUp() {
       
    final SyncUpTarget target = new SyncUpTarget();
       
    final SyncOptions options = 
        SyncOptions.optionsForSyncUp(Arrays.asList(ContactObject.CONTACT_FIELDS_SYNC_UP),
               
            MergeMode.LEAVE_IF_CHANGED);

	
    try {
		
        syncMgr.syncUp(target, options, ContactListLoader.CONTACT_SOUP, 
            new SyncUpdateCallback() {

			
                @Override
			
                public void onUpdate(SyncState sync) {
		        
                    if (Status.DONE.equals(sync.getStatus())) {
					
                        syncDown();
				
                    }
			
                }
		
            });
	
    } catch (JSONException e) {
           
        Log.e(TAG, "JSONException occurred while parsing", e);
	
    } catch (SmartSyncException e) {
           
        Log.e(TAG, "SmartSyncException occurred while attempting to sync up", e);
	
    }

}

In the internal SyncUpdateCallback implementation, this example takes the extra step of calling syncDown() when sync up is done. This step guarantees that the SmartStore soup remains up-to-date with any recent changes made to Contacts on the server.

Using SmartSync Data Framework in Hybrid Apps

The SmartSync Data Framework for hybrid apps is a Mobile SDK library that represents Salesforce objects as JavaScript objects. Using SmartSync Data Framework in a hybrid app, you can create models of Salesforce objects and manipulate the underlying records just by changing the model data. If you perform a SOQL or SOSL query, you receive the resulting records in a model collection rather than as a JSON string.

Mobile SDK provides two options for using SmartSync Data Framework in hybrid apps:

  • com.salesforce.plugin.smartsync — The SmartSync Data Framework plugin provides JavaScript access to the native SmartSync Data Framework library’s basic “sync-down” and “sync-up” functionality. As a result, performance-intensive operations — network negotiations, parsing, SmartStore management — run on native threads that do not impact Web view operations. Use the plugin in simpler scenarios, such as syncing large numbers of records rapidly in a native thread rather than in the Web view.
  • smartsync.js — The SmartSync Data Framework JavaScript library provides a Force SObject data framework for more complex syncing operations. The library is based on backbone.js, an open-source JavaScript framework that defines an extensible data modeling mechanism. To understand this technology, see Backbone.js website.

Hybrid apps created with forcedroid or forceios in Mobile SDK 5.0 or later automatically include the SmartSync Data Framework plugin.

The SmartSync Data Framework plugin exposes two methods: syncDown() and syncUp(). When you use these methods, several important guidelines can make your life simpler:

  • To create, update, or delete records locally for syncing with the plugin, use Force.SObject from smartsync.js. SmartSync Data Framework expects some special fields on soup records that smartSync.js creates for you.
  • Similarly, to create the soup that you’ll use in your sync operations, use Force.StoreCache from smartsync.js.
  • If you’ve changed objects in the soup, always call syncUp() before calling syncDown().

Syncing Down

The SmartSync Data Framework plugin’s syncDown() method downloads specified sObjects into a SmartSync Data Framework soup:

cordova.require("com.salesforce.plugin.smartsync").syncDown
    (storeConfig, target, soupName, options, success, error);

The soupName parameter identifies the soup that is to receive the downloaded sObjects; target indicates which sObjects to download. The value of target can be any of the following strings:

{type:"soql", query:"<soql_query>"}
Downloads the sObjects returned by the given SOQL query
{type:"sosl", query:"<sosl_query>"}
Downloads the sObjects returned by the given SOSL query
{type:"mru", sobjectType:"<sobject_type>", fieldlist:"<fields_to_fetch>"}
Downloads the specified fields of the most recently used sObjects of the specified sObject type

If sObjects in the soup have the same ID as objects specified in the target, SmartSync Data Framework overwrites the duplicate objects in the soup.

For the options parameter, use one of the following values:

  • To overwrite local records that have been modified:
    {mergeMode:Force.MERGE_MODE_DOWNLOAD.OVERWRITE}
  • To preserve local records that have been modified:
    {mergeMode:Force.MERGE_MODE_DOWNLOAD.LEAVE_IF_CHANGED}
    With this value, locally modified records are not overwritten.

The success parameter specifies a callback function to be called once the sync has started. This function is called multiple times during the sync operation:

  • When the sync operation begins
  • When the internal REST request has completed
  • After each page of results is downloaded, until all results have been received

Status updates on the sync operation arrive via browser events. To listen for these updates, use the following code:

document.addEventListener("sync",
   function(event)
      {
         // event.detail contains the status of the sync operation
      }
);

The event.detail member contains a map with the following fields:

  • syncId—The ID for this sync operation
  • type—syncDown
  • target—The target you provided
  • soupName—The soup name you provided
  • options—{}
  • status—The sync status, which can be one of the following:
    • NEW
    • RUNNING
    • DONE
    • FAILED
  • progress—The percentage of total records downloaded so far (an integer 0–100)
  • totalSize—The number of records downloaded so far

Syncing Up

The SmartSync Data Framework plugin’s syncUp() method uploads created, deleted, or updated records in a SmartStore soup and creates, deletes, or updates the corresponding records on the Salesforce server:

cordova.require("com.salesforce.plugin.smartsync").syncUp
   ([storeConfig,] [target,] soupName, options, successCb, errorCb);
  • storeConfig (optional)—A map that defines two keys:
    • isGlobalStore—Boolean indicating whether the store is configured for global access.
    • storeName—Name of the store.
  • target—If you’ve defined a custom native sync up target, use this parameter to identify its class.
  • soupName—Identifies the soup from which to upload the sObjects.
  • options—A map with the following key:
    • fieldlist—A list of fields sent to the server.
  • successCb, errorCb—Success and error callback functions.

Status updates on the sync operation arrive via browser events. To listen for these updates, use the following code:

document.addEventListener("sync",
   function(event)
      {
         // event.detail contains the status of the sync operation
      }
);

The event.detail member contains a map with the following fields:

  • syncId—The ID for this sync operation
  • type—syncUp
  • target—{}
  • soupName—The soup name you provided
  • options—The list of fields to be uploaded
  • status—The sync status, which can be one of the following:
    • NEW
    • RUNNING
    • DONE
    • FAILED
  • progress—The percentage of total records uploaded so far (an integer 0–100)
  • totalSize—The number of records uploaded so far
retargeting