📢 Attention Salesforce Certified Trailblazers! Maintain your credentials and link your Trailhead and Webassessor accounts by April 19th. Learn more.
close

Implement Secure Offline Storage with SmartStore

Learning Objectives

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

  • Understand basic SmartStore terms and concepts.
  • Use SmartStore to issue SELECT queries.
  • Register, populate, and manage SmartStore data on your preferred target platform (iOS, Android, hybrid, React Native).
  • Use the SmartStore Inspector.

Using SmartStore for Offline Storage

Mobile devices can lose connection at any time, and environments such as hospitals and airplanes often prohibit connectivity. To handle these situations, it’s important that your mobile apps continue to function when they go offline.

Mobile SDK provides SmartStore, a multithreaded, secure solution for offline storage on mobile devices. With SmartStore, your customers can continue working with data in a secure environment even when the device loses connectivity. When you couple SmartStore with SmartSync Data Framework, you can easily keep local SmartStore data in sync with the Salesforce server when connectivity resumes.

SmartStore stores data as JSON documents in a simple, single-table database. You can define indexes for this database, and you can query the data either with SmartStore helper methods that implement standard queries, or with custom queries using SmartStore’s Smart SQL language.

Soups

SmartStore stores offline data in logical collections known as soups. A SmartStore soup represents a single table in the underlying SQLite database, or store, and typically maps to a standard or custom Salesforce object. Soups contain soup elements. Each element is a JSON object that mirrors a single database row. To streamline data access, you define indexes for each soup. You use these indexes to query the soup with either SmartStore helper methods or SmartStore’s Smart SQL query language. SmartStore indexes also make your life easier by supporting full-text search queries.

It’s helpful to think of soups as tables, and stores as databases. You can define as many soups as you like in an application. As self-contained data sets, soups don’t have predefined relationships to each other, but you can use Smart SQL joins to query across them. Also, in native apps you can write to multiple soups within a transaction.

Warning

Warning

SmartStore data is volatile. In most cases, its lifespan is tied to the authenticated user and to OAuth token states. When the user logs out of the app, SmartStore deletes all soup data associated with that user. Similarly, when the OAuth refresh token is revoked or expires, the user’s app state is reset, and all data in SmartStore is purged. When designing your app, consider the volatility of SmartStore data, especially if your organization sets a short lifetime for the refresh token.

Smart SQL

SmartStore supports the Smart SQL query language for free-form SELECT statements. Only SELECT statements and indexed paths are supported. Smart SQL combines all standard SQL SELECT constructs with special syntax for referencing soups and soup fields. This approach gives you maximum control and flexibility, including the ability to use joins.

Syntax

Syntax is identical to the standard SQL SELECT specification but with the following adaptations:

Usage Syntax
To specify a column {<soupName>:<path>}
To specify a table {<soupName>}
To refer to the entire soup entry JSON string {<soupName>:_soup}
To refer to the internal soup entry ID {<soupName>:_soupEntryId}
To refer to the last modified date {<soupName>:_soupLastModifiedDate}

Sample Queries

Consider two soups: one named Employees, and another named Departments. The Employees soup contains standard fields such as:

  • First name (firstName)
  • Last name (lastName)
  • Department code (deptCode)
  • Employee ID (employeeId)
  • Manager ID (managerId)

The Departments soup contains:

  • Name (name)
  • Department code (deptCode)

Here are some examples of basic Smart SQL queries using these soups:

select {employees:firstName}, {employees:lastName} 
from {employees} order by {employees:lastName}

select {departments:name} 
from {departments} 
order by {departments:deptCode}

Joins

Smart SQL also allows you to use joins. For example:

select {departments:name}, {employees:firstName} || ' ' || {employees:lastName}  
from {employees}, {departments}  
where {departments:deptCode} = {employees:deptCode}  
order by {departments:name}, {employees:lastName}

You can even do self-joins:

select mgr.{employees:lastName}, e.{employees:lastName}  
from {employees} as mgr, {employees} as e  
where mgr.{employees:employeeId} = e.{employees:managerId}
Note

Note

Doing a join on a JSON1 index requires a slightly extended syntax. For example, instead of
select {soup1:path1} from {soup1}, {soup2}
use
select {soup1}.{soup1:path1} from {soup1}, {soup2}

Aggregate Functions

Smart SQL supports the use of aggregate functions such as:
  • COUNT
  • SUM
  • AVG
For example:
select {account:name}, 
    count({opportunity:name}),
    sum({opportunity:amount}),
    avg({opportunity:amount}),
    {account:id},
    {opportunity:accountid} 
from {account},
    {opportunity} 
where {account:id} = {opportunity:accountid} 
group by {account:name}

Registering a Soup

Before using a soup, you register it. If the soup doesn’t exist, registering creates it. If it does exist, registering gives you access to it. Mobile SDK provides methods for defining a soup’s name and indexes, and then registering it. However, in native apps those APIs are the “old way”. Beginning with Mobile SDK 6.0, you can register soups in native apps by loading a JSON configuration file. A single configuration file can define all your app's soups. Better yet, you can use the same configuration file in Android, iOS, hybrid, and React Native versions of your app.

You can use configuration files to register soups in either the default user store or the default global store. For other named stores or external stores, you can register soups only by coding.

When you register a soup, you create an empty named structure in memory that’s waiting for data. You typically initialize the soup with data from a Salesforce organization. To obtain the Salesforce data, you use Mobile SDK’s standard REST request mechanism. When a successful REST response arrives, you extract the data from the response object and then upsert it into your soup.

During soup creation, errors can happen for various reasons, including:
  • An invalid or bad soup name
  • No index (at least one index must be specified)
  • Other unexpected errors, such as a database error

Soup Structure

To define a soup, you provide a soup name and a list of one or more index specifications. Indexes are based on soup fields. You're not required to provide an index spec for every field you store in the soup. For example, if you're using the soup as a simple key-value store, use a single index specification of type string. Once the soups are created, SmartStore uses the indexes to track any insert, update, or delete operations.

SmartStore supports the following index data types.
  • string
  • integer
  • floating
  • full_text
  • json1

Defining Indexes

In native apps, you can use configuration files to register soups and define their indexes. In hybrid and React Native apps, you use JavaScript code. A few rules apply in every case.
  • Index paths are case-sensitive and can include compound paths, such as Owner.Name.
  • Index entries that are missing any fields described in an index spec array are not tracked in that index.
  • The type of the index applies only to the index. When you query an indexed field (for example, “select {soup:path} from {soup}”), the query returns data in the type that you specified in the index specification.
  • Index columns can contain null fields.
  • You can specify index paths that point to internal (non-leaf) nodes. You can use internal paths with like and match (full-text) queries. Use the string type when you define internal node paths.
    For example, consider this element in a soup named “spies”:
    {  
       "first_name":"James",
       "last_name":"Bond",
       "address":{  
          "street_number":10,
          "street_name":"downing",
          "city":"london"
       }
    }
    In this case, “address” is an internal node because it has children. Through the index on the path “address”, you can use a like or match query to find the “city” value—“london”—in “address”. For example:
    SELECT {spies:first_name, spies:last_name} FROM spies WHERE {spies:address} LIKE 'london'

Configuration File Format

Here's a theoretical example of a JSON configuration file that defines two soups—soup1 and soup2—and demonstrates the full gamut of index data types.
{  "soups": [
    {
      "soupName": "soup1",
      "indexes": [
        { "path": "stringField1", "type": "string"},
        { "path": "integerField1", "type": "integer"},
        { "path": "floatingField1", "type": "floating"},
        { "path": "json1Field1", "type": "json1"},
        { "path": "ftsField1", "type": "full_text"}
      ]
    },
    {
      "soupName": "soup2",
      "indexes": [
        { "path": "stringField2", "type": "string"},
        { "path": "integerField2", "type": "integer"},
        { "path": "floatingField2", "type": "floating"},
        { "path": "json1Field2", "type": "json1"},
        { "path": "ftsField2", "type": "full_text"}
      ]
    }
  ]
}
You can also register your soups with code rather than a configuration file. Consult the Salesforce Mobile SDK Development Guide for details.
Note

Note

If your code and your configuration file both register a soup with the same name, Mobile SDK ignores definition in the configuration file.

Example

The following configuration file registers a single soup based on account records. This soup indexes the name, ID, and owner (or parent) ID fields. Soup names are not required to match the name of a source Salesforce object, but an obvious allusion is usually a good choice.
{  "soups": [
    {
      "soupName": "account",
      "indexes": [
        { "path": "Name", "type": "string"},
        { "path": "Id", "type": "string"},
        { "path": "OwnerId", "type": "string"}
      ]
    }
  ]
}

Inserting or Updating Soup Entries

To insert or update soup entries—letting SmartStore determine which action is appropriate—you use an upsert method. For example, a hybrid app can use one of these JavaScript versions:

navigator.smartStore.upsertSoupEntries(isGlobalStore, soupName, 
    entries[], successCallback, errorCallback)
navigator.smartStore.upsertSoupEntries(storeConfig, soupName, 
    entries[], successCallback, errorCallback)
You provide the soup's name, an array of entries formatted as JSON strings, and optional success and error callback functions. The only difference between the two methods is the first parameter. If your soup lives in a named user or global store, you use the storeConfig parameter to provide the type of store and its name. For example:
{isGlobalStore:true, name:"AcmeSales"}
If you're using the default store global or user store, you can simply pass true to indicate a global store. Otherwise, you can omit the argument. This parameter is optional and defaults to false. It is not present in native methods, which read the same information from related objects.
Note

Note

To track soup entries for insertion, updating, and deletion, SmartStore adds a few fields to each entry:
_soupEntryId
The primary key for the soup entry in the table underlying the soup.
_soupLastModifiedDate
Number of milliseconds since 1/1/1970. To convert to a JavaScript date, use new Date(entry._soupLastModifiedDate). To convert a date to the corresponding number of milliseconds since 1/1/1970, use date.getTime().
When inserting or updating soup entries, SmartStore sets these fields automatically. When removing or retrieving specific entries, you can reference them by _soupEntryId. However, you never directly edit either of these fields.

If _soupEntryId is already set in any of the entries presented for upsert, SmartStore updates the soup entry that matches that ID. If an upsert entry doesn’t have a _soupEntryId slot, or its _soupEntryId doesn’t match an existing soup entry, SmartStore inserts the entry and overwrites its _soupEntryId.

Querying and Managing SmartStore

Mobile SDK for iOS native apps provides factory methods that build "query spec" objects based on your input. The factory methods build queries based on the following WHERE operators:
  • = ("exact" operator)
  • LIKE
  • MATCH (full text search extension)
  • BETWEEN, <=, >= (range queries)
  • ALL

These query objects handle simple, straightforward queries and spare you the trouble of writing them yourself. To turn up the nuance in more complex situations, you can write your own Smart SQL query and pass it to the Smart SQL factory method. After you obtain a query object, you pass it to an execution method to retrieve the specified soup data.

Managing a Soup

Sometimes you want to clean up unused data, or improve the performance of a soup by changing its indexes. For such tasks, SmartStore provides a suite of methods that perform diagnostic, maintenance, and management tasks. Use these methods to get information about a soup or store, edit a soup’s structure, or delete soups and stores.

More specifically, with these methods you can
  • Get the size of the underlying database
  • Get all stores (user or global)
  • Check whether a soup with the given name exists
  • Retrieve a soup’s spec and its index specs
  • Alter a soup’s configuration
  • Reindex a soup
  • Clear all records from a soup
  • Remove a soup from the store
  • Remove a store
  • Remove all stores (user or global)
These methods are available on all platforms for all app types.

Using the SmartStore Inspector

During testing, it’s helpful to see if your code is handling SmartStore data as you intended. The SmartStore Inspector provides a UI tool for that purpose. With it, you can:

  • Examine soup metadata, such as soup names and index specs for any soup
  • Clear a soup’s contents
  • Perform Smart SQL queries

For easiest access, launch the SmartStore Inspector through the Dev Support dialog box. This tool is available starting in Mobile SDK 6.0.

How you access the Dev Support menu depends on your development environment. To launch the dialog box, use one of the following options.

Android

Do one of the following:
  • When your app is running in the Android emulator, use the Command+m (Mac) or Ctrl+m (Windows) keyboard shortcut.
  • In a system command shell, run: adb shell input keyevent 82

iOS

  • On a physical device, use the shake gesture to bring up the Dev Support menu, and then choose SmartStore Inspector.
  • In the iOS simulator, select the Hardware | Shake Gesture menu item, or use the ^+Command+z keyboard shortcut.

Using SmartStore in Native iOS Apps

Adding the SmartStore module to new native iOS apps requires no extra effort. Any native forceios app you create automatically includes the SmartStore library.

Registering Soups with a Configuration File

For iOS native apps, Mobile SDK looks for configuration files under / (top level) in the Resources bundle.

  1. Add configuration files to your project.
    1. In the Xcode Project navigator, select the project node.
    2. In the Editor window, select Build Phases.
    3. Expand Copy Bundle Resources.
    4. Click + (”Add items”).
    5. Select your soup configuration file. If your file is not already in an Xcode project folder:
      1. To select your file in Finder, click Add Other....
      2. When prompted to create groups, click Finish.
  2. Add a single line of code for each file you provide. Be sure to make this call before you try to access other SmartStore methods.
    • To load a userstore.json file, use one of the following:
      SmartStoreSDKManager.shared().setupUserStoreFromDefaultConfig()
    • To load a globalstore.json file, use one of the following:
      SalesforceManager.shared().setupGlobalStoreFromDefaultConfig()
Note

Note

To use a config file in React Native apps, be sure to make these calls in the AppDelegate class after the launch message is sent. For example:

- (instancetype)init
{
    ...

    [SmartSyncSDKManager sharedManager].postLaunchAction = ^(SFSDKLaunchAction launchActionList) {
        //
        // If you wish to register for push notifications, uncomment the line below.  Note that,
        // if you want to receive push notifications from Salesforce, you will also need to
        // implement the application:didRegisterForRemoteNotificationsWithDeviceToken: method (below).
        //
        //[[SFPushNotificationManager sharedInstance] registerForRemoteNotifications];
        [SFSDKLogger log:[weakSelf class] level:DDLogLevelInfo format:@"Post-launch: launch actions taken: %@", 
            [SmartSyncSDKManager launchActionsStringRepresentation:launchActionList]];
        [[SmartStoreSDKManager sharedManager] setupUserStoreFromDefaultConfig];
        [weakSelf setupRootViewController];
    };
    ...
However, for React Native, it's easier to set up SmartStore soups in JavaScript code. In any case, be sure to call setup methods before calling any soup manipulation method.

Populating a Soup

When you register a soup, you create an empty named structure in memory that’s waiting for data. To populate the soup with data from Salesforce, use the standard REST request mechanism to obtain the data. When a successful REST response arrives, extract the data from the response object and then upsert it into your soup. For coding details, see the example at the end of this iOS section.

Querying Soup Data

In iOS, you create query spec objects by calling class methods on the SFQuerySpec class. For example, in Objective-C, the newSmartQuerySpec:withPageSize: method returns an SFQuerySpec object that encapsulates a given Smart SQL query string:
var querySpec = store.buildSmartQuerySpec(
    smartSql: "select {account:Name} from {account}",
    pageSize: 10)
The page size parameter determines how many records are sent in each page of results. These methods allow greater flexibility than other query factory functions because you provide your own Smart SQL SELECT statement. For example, the following code issues a query that calls the SQL COUNT function. Since COUNT returns a single value, the only possible page size is one.

The following code issues a query that calls the SQL COUNT function. Since COUNT returns a single value, the only possible page size is one.

To run a query, pass your SFQuerySpec object to the query() method on the SFSmartStore object.
var querySpec = store.buildSmartQuerySpec(
    smartSql: "select count(*) from {employees}",
    pageSize: 1)

Managing a Soup

To use Objective-C soup management APIs in a native iOS app, import SmartStore/SFSmartStore.h. You call soup management methods on a SFSmartStore shared instance. Obtain the shared instance by using one of the following SFSmartStore class methods.

To obtain the SmartStore instance for the current user:
var store = SmartStore.shared(withName: storeName)
To obtain the SmartStore instance for a specified user:
var store = SmartStore.shared(withName: storeName, forUserAccount: user)
For example, to call the removeSoup: management method:
self.store = [SFSmartStore sharedStoreWithName:kDefaultSmartStoreName];
if ([self.store soupExists:@"Accounts"]) {
    [self.store removeSoup:@"Accounts"];
}

Example

In this example, you create a SmartStore soup and upsert the queried list of contact names into that soup. You then change the Swift template app flow to populate the table view from the soup instead of directly from the REST response. If you’re not familiar with Xcode project structure, consult the Xcode Help.
  1. Using forceios, create a native Swift project similar to the following example:
    $ forceios create
    Enter your application type (native_swift or native, leave empty for native_swift): <Press RETURN>
    Enter your application name: <Enter any name you like>
    Enter your package name: com.testapps.ios
    Enter your organization name (Acme, Inc.): TestAppsRUs
    Enter output directory for your app (leave empty for the current directory): <Press RETURN or enter a directory name>
    
  2. In your project’s root directory, create a userstore.json file with the following content.
    { "soups": [
        {
        "soupName": "User",
        "indexes": [
            { "path": "Name", "type": "string"},
            { "path": "Id", "type": "string"}                        
            ]
        }    
    ]}
  3. Open your app's .xcworkspace file in Xcode.
  4. Add your configuration file to your project.
    1. In the Xcode Project navigator, select the project node.
    2. In the Editor window, select Build Phases.
    3. Expand Copy Bundle Resources.
    4. Click + (”Add items”).
    5. Select your soup configuration file. If your file is not already in an Xcode project folder:
      1. To select your file in Finder, click Add Other....
      2. Click Open, then click Finish.
  5. In your project’s source code folder, select Classes/AppDelegate.swift.
  6. In the application(_:didFinishLaunchingWithOptions:) callback method, load userstore.json definitions in the call to AuthHelper.loginIfRequired.
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
    {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.initializeAppViewState();
        ...
    
        AuthHelper.loginIfRequired { [weak self] in
            SmartSyncSDKManager.shared.setupUserStoreFromDefaultConfig()
            self?.setupRootViewController()
        }
        return true
    }
    Your app is now set up to load your SmartStore configuration file at startup. This action creates the soups you specified as empty tables. Let's configure the RootViewController class to use SmartStore.
  7. In RootViewController.swift, import SmartStore:
    import SmartStore
  8. At the top of the RootViewController class, declare a variable for a SmartStore instance.
    class RootViewController : UITableViewController
    {
        var dataRows = [NSDictionary]()
        var store = SmartStore.shared(withName: SmartStore.defaultStoreName)!
  9. On the next line, declare a constant that defines an OSLog component.
    class RootViewController : UITableViewController
    {
        var dataRows = [NSDictionary]()
        var store = SmartStore.shared(withName: SmartStore.defaultStoreName)!
        let mylog = OSLog(subsystem: "com.testapp.swift", category: "tutorial")
  10. In the loadView() method, find the call to .query and add the Id field to the SOQL statement.
    let request = RestClient.shared.request(forQuery: "SELECT Name, Id FROM User LIMIT 10")
  11. Add the highlighted lines in the trailing closure of the send(request:onFailure:onSuccess:) method.
    }){ [weak self] (response, urlResponse) in
            guard let strongSelf = self,
                let jsonResponse = response as? Dictionary<String,Any>,
                let result = jsonResponse ["records"] as? [Dictionary<String,Any>]
            else {
                return
            }
    
            SalesforceLogger.d(type(of:strongSelf),message:"Invoked: \(request)")
            if ((strongSelf.store.soupExists(forName: "User"))) {
                strongSelf.store.clearSoup("User")
                strongSelf.store.upsert(entries: result, forSoupNamed: "User")
                os_log("\nSmartStore loaded records.", log: strongSelf.mylog, type: .debug)
            }
            DispatchQueue.main.async {
                strongSelf.dataRows = result
                strongSelf.tableView.reloadData()
            }
        }
    } // end of loadView
    This code checks whether the User soup exists. If the soup exists, the code clears all data from the soup, and then upserts the retrieved records.
  12. Launch the app, then check your work using the Dev Tools menu.
    1. To bring up the menu, type control + command + z if you’re using the iOS emulator, or shake your iOS device.
    2. Click Inspect SmartStore.
    3. To list your User soup and number of records, click Soups.
      Note

      Note

      If you get a "Query: No soups found" message, chances are you have an error in your userstore.json file.

You’ve now created and populated a SmartStore soup. However, at this point your soup doesn’t actually serve a purpose. Let's make it more useful by populating the list view from SmartStore records rather than directly from the REST response.
  1. After the loadView() method, add a method named loadFromStore().
    func loadFromStore() {
    
    }
  2. In loadFromStore(), define a QuerySpec object that builds a SmartSQL query specification. Configure the query to extract the first 10 Name values from the User soup.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        
    }
  3. Call a method that runs the SmartStore query. Since the query method throws an exception, call it from a do...try...catch block.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)    
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            // ...
        } catch let e as Error? {
            
        }
        
    }
  4. Using a Swift guard for safety, convert the records returned into a dictionary of dictionaries. If the guard fails, log a debugger error and return.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            guard let rows = records as? [[String]] else {
                os_log("\nBad data returned from SmartStore query.", log: self.mylog, type: .debug)
                return
            }
            // ...
        } catch let e as Error? {
    
        }
        
    }
  5. Transfer the names returned by the SmartStore query to the view’s dataRows member .
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            guard let rows = records as? [[String]] else {
                os_log("\nBad data returned from SmartStore query.", log: self.mylog, type: .debug)
                return
            }
            self.dataRows = rows.map({ row in
                return ["Name": row[0]]
            })
            // ...
        } catch let e as Error? {
    
        }
    
    }
  6. Using the DispatchQueue system object, switch to the main thread and refresh the view’s displayed data.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            guard let rows = records as? [[String]] else {
                os_log("\nBad data returned from SmartStore query.", log: self.mylog, type: .debug)
                return
            }
            self.dataRows = rows.map({ row in
                return ["Name": row[0]]
            })
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        } catch let e as Error? {
    
        }
    
    }
  7. Finally, in the catch block, call the logger to log an error description.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            guard let rows = records as? [[String]] else {
                os_log("\nBad data returned from SmartStore query.", log: self.mylog, type: .debug)
                return
            }
            self.dataRows = rows.map({ row in
                return ["Name": row[0]]
            })
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        } catch let e as Error? {
            os_log("\n%{public}@", log: self.mylog, type: .debug, e!.localizedDescription)
        }
    
    }
    Here's the completed loadFromStore() method.
    func loadFromStore() {
        let querySpec = QuerySpec.buildSmartQuerySpec(
            smartSql: "select {User:Name}, {User:Id} from {User}", 
            pageSize: 10)
        
        do {
            let records = try self.store.query(using: querySpec!, startingFromPageIndex: 0)
            guard let rows = records as? [[String]] else {
                os_log("\nBad data returned from SmartStore query.", log: self.mylog, type: .debug)
                return
            }
            self.dataRows = rows.map({ row in
                return ["Name": row[0]]
            })
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        } catch let e as Error? {
            os_log("\n%{public}@", log: self.mylog, type: .debug, e!.localizedDescription)
        }
    
    }
    The work done by this function obviously matches what’s already in loadView(), so let’s get rid of the duplication.
  8. Scroll back to the loadView() method and remove the existing code that reloads the view’s data.
    }){ [weak self] (response, urlResponse) in
            guard let strongSelf = self,
                let jsonResponse = response as? Dictionary<String,Any>,
                let result = jsonResponse ["records"] as? [Dictionary<String,Any>]
            else {
                return
            }
            if ((strongSelf.store.soupExists("User"))) {
                strongSelf.store.clearSoup("User")
                strongSelf.store.upsert(entries: result, forSoupNamed: "User")
                os_log("\nSmartStore loaded records.", log: strongSelf.mylog, type: .debug)
            }
    
            // Remove this block: 
            // DispatchQueue.main.async {
            //    strongSelf.dataRows = result
            //    strongSelf.tableView.reloadData()
            //}
        }
    } // end of loadView
  9. Using the strongSelf context variable, call your new loadFromStore() method immediately after the upsert(entries:forSoupNamed:) call.
    }){ [weak self] (response, urlResponse) in
            guard let strongSelf = self,
                let jsonResponse = response as? Dictionary<String,Any>,
                let result = jsonResponse ["records"] as? [Dictionary<String,Any>]
            else {
                return
            }
            if ((strongSelf.store.soupExists("User"))) {
                strongSelf.store.clearSoup("User")
                strongSelf.store.upsert(entries: result, forSoupNamed: "User")
             // Insert this call
                strongSelf.loadFromStore()
                os_log("\nSmartStore loaded records.", log: strongSelf.mylog, type: .debug)
            }
    
            // Remove this block: 
            // DispatchQueue.main.async {
            //    strongSelf.dataRows = result
            //    strongSelf.tableView.reloadData()
            //}
        }
    } // end of loadView
When you retest your app, you see that the table view is populated as before, but from SmartStore rather than a live REST response. In the real world, you'd create an editing interface for the User list, and then upsert your customers' edits to SmartStore. The customer could then continue working on the User list even if the mobile device lost connectivity. When connectivity is restored, you could then merge the customer’s work to the server—and also resync SmartStore—using SmartSync Data Framework.

Using SmartStore in Native Android Apps

All forcedroid and forceios native apps include SmartStore and SmartSync Data Framework libraries. However, existing Android native apps may require a few custom setup steps.
  1. In your Android native project, open MainApplication.java.
  2. Add the following import statement if it’s not already present:
    import com.salesforce.androidsdk.smartsync.app.SmartSyncSDKManager;
  3. Find the line that calls initNative(). For example:
    SalesforceSDKManager.initNative(getApplicationContext(), new NativeKeyImpl(), MainActivity.class);
  4. If initNative() is called on SalesforceSDKManager, change SalesforceSDKManager to SmartSyncSDKManager:
    SmartSyncSDKManager.initNative(getApplicationContext(), new NativeKeyImpl(), MainActivity.class);

Registering Soups with a Configuration File

  1. Place your soup configuration files in the /res/raw/ project folder.
  2. Add a single line of code for each file you provide.
    • To load a userstore.json file, use
      SmartStoreSDKManager.getInstance().setupUserStoreFromDefaultConfig();
    • To load a globalstore.json file, use
      SmartStoreSDKManager.getInstance().setupGlobalStoreFromDefaultConfig();
Note

Note

In React Native apps, call setup methods at the end of the MainApplication.onCreate() method. For example:

@Override
public void onCreate() {
super.onCreate();
    SalesforceReactSDKManager.initReactNative(getApplicationContext(), 
        new ReactNativeKeyImpl(), MainActivity.class);
    ...

    /*
    * Un-comment the line below to enable push notifications in this app.
    * Replace 'pnInterface' with your implementation of 'PushNotificationInterface'.
    * Add your Google package ID in 'bootonfig.xml', as the value
    * for the key 'androidPushNotificationClientId'.
    */
    // SalesforceReactSDKManager.getInstance().setPushNotificationReceiver(pnInterface);

    SmartStoreSDKManager.getInstance().setupUserStoreFromDefaultConfig();
}
In other native apps, you have more flexibility. Just be sure to call setup methods before calling any soup manipulation method.

Populating a Soup

When you register a soup, you create an empty named structure in memory that’s waiting for data. To populate the soup with data from Salesforce, use the standard REST request mechanism to obtain the data. When a successful REST response arrives, extract the data from the response object and then upsert it into your soup:

public void populateAccounts() throws UnsupportedEncodingException {
    final RestRequest restRequest =
        RestRequest.getRequestForQuery(
            ApiVersionStrings.getVersionNumber(SalesforceSDKManager.getInstance().getAppContext()), 
            "SELECT Name, Id, OwnerId FROM Account");
    client.sendAsync(restRequest, new RestClient.AsyncRequestCallback() {
        @Override
        public void onSuccess(RestRequest request, RestResponse result) {
            result.consumeQuietly(); // always call before switching to main thread (unlike here)
            try {
                JSONArray records = result.asJSONObject().getJSONArray("records");
                insertAccounts(records);
            } catch (Exception e) {
                onError(e);
            } finally {
                Log.println(Log.INFO, "REST Success!", "\nSmartStore insertion successful");
            }
        }
        @Override
        public void onError(Exception e)
        {
            Log.e(TAG, e.getLocalizedMessage());
        }
    });
}

/**
 * Inserts accounts into the accounts soup.
 *
 * @param accounts Accounts.
 */
public void insertAccounts(JSONArray accounts)
{
    try {
        if (accounts != null) {
            for (int i = 0; i < accounts.length(); i++) {
                if (accounts.get(i) != null) {
                    try {
                        smartStore.upsert("Accounts", accounts.getJSONObject(i));
                    }
                    catch (JSONException exc) {
                        Log.e(TAG, "Error occurred while attempting to insert account. "
                                +  "Please verify validity of JSON data set.");
                    }
                }
            }
        }
    }
    catch (JSONException e) {
        Log.e(TAG, "Error occurred while attempting to insert accounts. "
                + "Please verify validity of JSON data set.");
    }
}

Querying Soup Data with Smart SQL

In Android, you create query spec objects by calling static factory methods on the QuerySpec class. For example, the buildSmartQuerySpec method creates a Smart SQL object that encapsulates a given query string:
public static QuerySpec buildSmartQuerySpec(String smartSql, int pageSize)
To execute the query, you pass the returned QuerySpec object to the SmartStore.query() method. This function allows greater flexibility than other query factory functions because you provide your own Smart SQL SELECT statement. The pageSize parameter determines how many records are sent in each page of results.

To run a query through a QuerySpec object, pass it to the query() method on the SmartStore object. The following code issues a query that calls the SQL COUNT function. Since COUNT returns a single value, the only possible page size is one.

try {
    JSONArray result =
        store.query(QuerySpec.buildSmartQuerySpec(
            "select count(*) from {Accounts}", 1), 0);
    // result should be [[ n ]] if there are n employees
    Log.println(Log.INFO, "REST Success!", "\nFound " + 
        result.getString(0) + " accounts.");
} catch (JSONException e) {
    Log.e(TAG, "Error occurred while counting the number of account records. "
        +  "Please verify validity of JSON data set.");
}

Managing a Soup

To use soup management APIs in a native Android app, you call methods on the shared SmartStore instance:
SmartStore smartStore = 
    SmartStoreSDKManager.getInstance().getSmartStore();
smartStore.clearSoup("user1Soup");

Example

You can easily add SmartStore support to a forcedroid native app. Let's reconfigure the JSON import file to create two soups, one for each sObject query. We can then populate the soups at the same time that we populate the list view.
  1. Open your project directory in Android Studio.
  2. In the app/res folder, create a folder named “raw”.
  3. Right-click app/res/raw and select New | File. Name the file userstore.json.
  4. Add the following text to the new file:
    { "soups": [
        {
        "soupName": "Account",
        "indexes": [
            { "path": "Name", "type": "string"},
            { "path": "Id", "type": "string"},
            { "path": "OwnerId", "type": "string"},
            ]
        },
        {
        "soupName": "Contact",
        "indexes": [ 
            { "path": "Name", "type": "string"},
            { "path": "Id", "type": "string"},
            { "path": "OwnerId", "type": "string"},
            ]
        }
    ]}
  5. Open MainActivity.java and import these files:
    import com.salesforce.androidsdk.smartstore.app.SmartStoreSDKManager;
    import com.salesforce.androidsdk.smartstore.store.IndexSpec;
    import com.salesforce.androidsdk.smartstore.store.QuerySpec;
    import com.salesforce.androidsdk.smartstore.store.SmartStore;
    import com.salesforce.androidsdk.smartstore.ui.SmartStoreInspectorActivity;
  6. At the top of the MainActivity class, declare a private variable to point to the SmartStore shared instance, and another to track which sObject we're handling:
    private SmartStore smartStore;private String objectType;
  7. In the onCreate(Bundle savedInstanceState) method, import the soup definitions from your userstore.json file:
    smartStore = SmartStoreSDKManager.getInstance().getSmartStore();
    if (!smartStore.hasSoup("Account") && !smartStore.hasSoup("Contact")) {
        SmartStoreSDKManager.getInstance().setupUserStoreFromDefaultConfig();
    } else {
        // Delete existing records in preparation for new server data
        smartStore.clearSoup("Account");
        smartStore.clearSoup("Contact");
    }
  8. In the onFetchContactsClick(View v) method, clear the Contact soup to avoid creating duplicate records:
    smartStore.clearSoup("Contact");
    objectType = "Contact";
  9. In the onFetchAccountsClick(View v) method, clear the Account soup to avoid creating duplicate records:
    smartStore.clearSoup("Account");
    objectType = "Account";
  10. In the client.sendAsync() method, call upsert() in the for loop that inserts the JSON response into the listAdapter object:
    for (int i = 0; i < records.length(); i++) {
    	listAdapter.add(records.getJSONObject(i).getString("Name"));
    	try {
    	    smartStore.upsert((objectType, records.getJSONObject(i));
    	} catch (Exception e) {
    	    onError(e);
    	}
    }
  11. Launch the app, then check your work using the Dev Tools menu.
    • To bring up the menu, type Command + m (Mac) or Ctrl + m (Windows).
    • Click Inspect SmartStore.
    • To see a list of your soups and number of records in each, click Soups.
    Note

    Note

    If you get "Query: No soups found", chances are you have an error in your userstore.json file.

You’ve now created and populated two SmartStore soups, but at this point they don’t serve a useful purpose. In the real world, you'd create an editing interface for the Account and Contact lists, and then upsert the customer’s changes to SmartStore. When the customer’s device regained connectivity, you could then merge changes to the server with SmartSync Data Framework.

Using SmartStore in Hybrid Apps

Registering Soups with a Configuration File

In hybrid apps, Mobile SDK automatically loads SmartStore configuration files. You’re responsible for putting configuration files in the required location, as follows:
  1. Copy the configuration file to your hybrid project’s top-level www/ directory (for example, MyProject/www/).
  2. At a Terminal window or Windows command prompt, cd to your project directory (for example, MyProject/).
  3. Run: cordova prepare

Populating a Soup

To obtain Salesforce records, hybrid apps use the standard force.query() function from the JavaScript library. You use the success callback to upsert the data from the record set into the soup.
force.query("SELECT Name,Id FROM Contact", 
  onSuccessSfdcContacts, onErrorSfdc); 
force.query("SELECT Name,Id FROM Contact", 
    onSuccessSfdcContacts, onErrorSfdc); var sfSmartstore = function() {
    return cordova.require("com.salesforce.plugin.smartstore");};
function onSuccessSfdcContacts(response) {
    logToConsole()("onSuccessSfdcContacts: received " + 
        response.totalSize + “ contacts");
    var entries = [];
    
    response.records.forEach(function(contact, i) {
           entries.push(contact);
    });
    
    if (entries.length > 0) {
        sfSmartstore().upsertSoupEntries(CONTACTS_SOUP_NAME,
            entries,
            function(items) {
                var statusTxt = "upserted: " + items.length + 
                    " contacts";
                logToConsole()(statusTxt);
            }, 
         onErrorUpsert);
    }
}

function onErrorSfdc(param) {
    logToConsole()("onErrorSfdc: " + param);
}function onErrorUpsert(param) {
    logToConsole()("onErrorUpsert: " + param);
}

Querying Soup Data with Smart SQL

In hybrid apps, you create query spec objects by calling functions on the com.salesforce.plugin.smartstore plugin's SmartStore object. For example, the buildSmartQuerySpec() function executes a Smart SQL query:

smartstore.buildSmartQuerySpec(smartSql, [pageSize])

where smartSql is the query to be executed. This function allows greater flexibility than other query factory functions because you provide your own SELECT statement. pageSize is optional and defaults to 10.

The following code issues a query that calls the Smart SQL COUNT function on a soup named “employees”.

var querySpec = 
    navigator.smartstore.buildSmartQuerySpec(
        "select count(*) from {employees}", 1);

navigator.smartstore.runSmartQuery(querySpec, function(cursor) { 
    // result should be [[ n ]] if there are n employees
});

Managing a Soup

Each soup management function in JavaScript takes two callback functions: a success callback that returns the requested data, and an error callback. Success callbacks vary according to the soup management functions that use them. Error callbacks take a single argument, which contains an error description string. For example, you can define an error callback function as follows:
function(e) { alert(“ERROR: “ + e);}
To call a soup management function in JavaScript, first invoke the Cordova plug-in to initialize the SmartStore object. You then use the SmartStore object to call the soup management function. The following example defines named callback functions discretely, but you can also define them inline and anonymously.
var sfSmartstore = function() {
    return cordova.require("com.salesforce.plugin.smartstore");};

function onSuccessRemoveSoup(param) {
    logToConsole()("onSuccessRemoveSoup: " + param);
    $("#div_soup_status_line").html("Soup removed: " 
        + SAMPLE_SOUP_NAME);
}

function onErrorRemoveSoup(param) {
    logToConsole()("onErrorRemoveSoup: " + param);
    $("#div_soup_status_line").html("removeSoup ERROR");
}

sfSmartstore().removeSoup(SAMPLE_SOUP_NAME,
     onSuccessRemoveSoup, 
     onErrorRemoveSoup);

Using SmartStore in React Native Apps

React Native apps have much in common with hybrid apps. Usually, SmartStore functions for the two platforms share identical signatures. However, several significant differences apply.
  • In React Native apps, you write code in ES2015. Think of ES2015 as a futuristic version of JavaScript. While you can use the same JavaScript syntax as in hybrid apps, you can also take advantage of new streamlined coding conventions.
  • React Native SmartStore doesn’t use backbone.js.
  • You can’t use Cordova hybrid libraries or plug-ins in React Native. Instead, you import React Native SmartStore modules.
To use the SmartStore API, you import the smartstore module. To use the Salesforce API—most importantly for making queries to retrieve Salesforce records—you import the net module. You can import both modules in a single statement:
import {net, smartstore} from 'react-native-force';

Registering a Soup

For React Native, it's probably easier to use JavaScript code instead of configuration files to set up SmartStore soups. That's because you can write the code once and use it for both iOS and Android. For example:
smartstore.registerSoup(false,
    "contacts", 
    [ {path:"Id", type:"string"}, 
    {path:"FirstName", type:"full_text"}, 
    {path:"LastName", type:"full_text"},    
    {path:"__local__", type:"string"} ],
    () => syncDown()
);
If you do choose to use soup configuration files in React Native, you edit your project’s native container app—the Java, Kotlin, Objective-C, or Swift wrapper. You then follow the steps for adding soup configuration files to a native Android or iOS project.

Populating a Soup

To fill your soup with Salesforce data, begin by querying for records using the standard Salesforce API. The net module provides a set of wrapper functions that simplify the network calls. You pass a query string and success and error callbacks. In the success callback, you use the smartstore module to upsert records from the query response into your soup. (If this strategy sounds familiar, you must have read the hybrid section!)
net.query("SELECT Name,Id FROM Contact", 
        onSuccessSfdcContacts, onErrorSfdc);
Here’s a success callback example.
function onSuccessSfdcContacts(response) {
    logToConsole()("onSuccessSfdcContacts: received " + 
        response.totalSize + “ contacts");
    var entries = [];
    $.each(response.records, function(i, contact) {
           entries.push(contact);
           logToConsole()("name: " + contact.Name);
    });
    
    if (entries.length > 0) {
        smartstore().upsertSoupEntries(CONTACTS_SOUP_NAME,
            entries,
            function(items) {
                var statusTxt = "upserted: " + items.length + 
                    " contacts";
                logToConsole()(statusTxt);
            }, 
            onErrorUpsert);
    }
}

Querying Soup Data with Smart SQL

In React Native, you create query spec objects by calling functions on the smartstore module. For example, the buildSmartQuerySpec() function constructs a Smart SQL query object:

buildSmartQuerySpec(smartSql, [pageSize])

In this function, smartSql is the query to be executed. This function allows greater flexibility than other query factory functions because you provide your own SELECT statement. pageSize is optional and defaults to 10.

Once you’ve built the smart query object, you pass it to the runSmartQuery() function, providing a success callback to handle the response. The following code builds and runs a query that calls the SQL COUNT function.

var querySpec = 
    smartstore.buildSmartQuerySpec(
        "select count(*) from {employees}", 1);

smartstore.runSmartQuery(querySpec, function(cursor) { 
    // result should be [[ n ]] if there are n employees
});

Using SmartStore Management Functions

Soup management functions follow the same pattern as other React Native functions. You define success and error callbacks, and you call the function on the smartstore module. The number of parameters passed to the success callback can vary depending on the function. Error callbacks always take only an error description string argument.

Resources

retargeting