. . .

Offline Handling in SDKs

When you use our SDKs you can utilize the built-in offline handling. Not all SDKs support it yet, but it’s built into the Android, C#, ObjectiveC, Swift and JavaScript SDKs already. The TypeScript SDK also supports offline handling for GET requests.

What is offline handling?

Offline handling is the management of the data when the device of the user is offline. Without offline handling, when the device is offline, requests to the ApiOmat server fail and you can neither store nor receive any data. With offline handling, both problems are solved:

  1. Outgoing save and delete requests get queued to be sent later (as soon as the device is online again)

  2. Responses to requests that fetch data get saved while the device is still online, so when doing a request when the device is offline you get the data from a saved response

The data gets saved either in persistent storage or in an in-memory storage, depending on the configured strategy, as explained below.

Configuration

You as a developer might have different requirements towards the offline handling, so we provide several configuration options. You can pick a behaviour (cache strategy) and the classes for which you want to use persistent storage.

Strategies

There are several stategies available that should cover most of the use cases for offline handling:

  1. NETWORK_ELSE_CACHE: Only use the cache if the server is unreachable or returns 304 (local and remote data are the same)

  2. NETWORK_ONLY: Don’t use caching (on save as well as read)

  3. CACHE_THEN_NETWORK: First read from cache, than send a request to the server (leading to a second callback)

  4. CACHE_ELSE_NETWORK: Use the cache, but if nothing is found there, send a request

NETWORK_ELSE_CACHE is the default cache strategy.

To set a strategy, you call a method in the Datastore. For example:

Android
Datastore.setCachingStrategy(Datastore.AOMCacheStrategy.NETWORK_ONLY, this.getApplicationContext());
Objective-C
[AOMDatastore setCachingStrategy:AOM_NETWORK_ONLY];
C#
Datastore.SetCachingStrategy(Datastore.AOMCacheStrategy.NETWORK_ONLY);
JavaScript
Apiomat.Datastore.setCachingStrategy(Apiomat.AOMCacheStrategy.NETWORK_ONLY);
TypeScript
Datastore.Instance.setCachingStrategy(AOMCacheStrategy.NETWORK_ONLY);

Persistence

Depending on the cache strategy (see above), responses to requests usually get stored. This storage is not persistent though, because it resides in-memory. This might be enough when the device loses the mobile internet connection for a moment while the app stays opened, but offline handling needs persistent storage, so that the user can close and re-open your app or reboot his mobile device and still see data instead of a blank page.

There are two ways to enable persistent storage:

  1. You can either enable it for a class in general. From then on, all requests related to that class are being stored persistently.

  2. Or you can enable it for a single request. You can also enable it for a class and then disable it for a single request.

Option 2 has a higher priority, which means that even if you configured the Datastore to save data of the class User to persistent storage, when you call the method with the parameter useOfflineStorage set to false, the data won’t be saved in persistent storage.

Examples:

Enabling persistence for a class:

Android
// Assuming Datastore is already configured
Datastore.setContext(this.getApplicationContext()); // Set the context of the current activity
Datastore.setOfflineUsageForClass(User.class, true); // Enable persistence for the user class
User user = User.getUsers("userName==\"John\""); // The response to this request gets saved in persistent storage
// Assuming the device goes offline now
user = User.getUsers("userName==\"John\""); // Instead of sending a request to the server, the response is read from the storage
Objective-C
NSArray user = nil;
[AOMUser setStoreOffline:TRUE];
// The response to this request gets saved in persistent storage
[AOMUser getAsyncWithQuery:@"userName==\"John\"" withBlock:^(NSMutableArray *models, NSError *error) {
if(!error)
{
user = models;
}
}];
 
// Assuming the device goes offline now
// Instead of sending a request to the server, the response is read from the storage
AOMUser getAsyncWithQuery:@"userName==\"John\"" withBlock:^(NSMutableArray *models, NSError *error) {
if(!error)
{
user = models;
}
}];
C#
// Assuming Datastore is already configured
Datastore.InitOfflineHandler(); //Initialize the offline handler
Datastore.Instance.SetOfflineUsageForType(typeof(User), true);// Enable persistence for the user class
IList<User> users = User.GetUsersAsync("userName==\"John\"").Result; // The response to this request gets saved in persistent storage
// Assuming the device goes offline now
users = User.GetUsersAsync("userName==\"John\"").Result; // Instead of sending a request to the server, the response is read from the storage
JavaScript
// Assuming Datastore is already configured
Apiomat.Datastore.getInstance().setOfflineUsageForClass(Apiomat.User, true); // Enable persistence for the user class
var users;
Apiomat.User.getUsers("userName==\"John\"", { // The response to this request gets saved in persistent storage
onOk : function(result) {
users = result;
}
onError : function(error) {
console.log("An error occurred: ", error);
}
});
// Assuming the device goes offline now
Apiomat.User.getUsers("userName==\"John\"", { // Instead of sending a request to the server, the response is read from the storage
onOk : function(result) {
users = result;
}
onError : function(error) {
console.log("An error occurred: ", error);
}
});
TypeScript
// Assuming Datastore is already configured
await Datastore.Instance.initOfflineHandler();
Datastore.Instance.setOfflineUsageForClass(User, true);
 
// it will automatically use the cache for storing responses
let users = User.getUsers('userName==\"John\"'); // The response to this request gets saved in persistent storage
 
// Assuming the device goes offline now
users = User.getUsers('userName==\"John\"'); // Instead of sending a request to the server, the response is read from the cache

In C# you can pass the database path for the offline-storage-database as string-parameter for the "InitOfflineHandler"-method. If you don't pass it or pass null, the current directory of your binaries will be used to store the file. This seems to work without any problems, but iOS and Android-apps built with Xamarin will need to set a path. Otherwise, the offline storage will just not work.

Enabling persistence for just one request:

Android
// Assuming Datastore is already configured
Datastore.setContext(this.getApplicationContext()); // Set the context of the current activity
User user = User.getUsers("userName==\"John\"", true); // The response to this request gets saved in persistent storage
// Assuming the device goes offline now
user = User.getUsers("userName==\"John\"", true); // Instead of sending a request to the server, the response is read from the storage
Objective-C
NSArray user = nil;
// The response to this request gets saved in persistent storage
[AOMUser getAsyncWithQuery:@"userName==\"John\"" withBlock:^(NSMutableArray *models, NSError *error) {
/* Load finished */
if(!error)
{
user = models;
}
} andStoreOffline:TRUE];
// Assuming the device goes offline now
[AOMUser getAsyncWithQuery:@"userName==\"John\"" withBlock:^(NSMutableArray *models, NSError *error) {
/* Load finished */
if(!error)
{
user = models;
}
} andStoreOffline:TRUE];
C#
// Assuming Datastore is already configured
Datastore.InitOfflineHandler(); //Initialize the offline handler
IList<User> users = User.GetUsersAsync("userName==\"John\"", true).Result; // The response to this request gets saved in persistent storage
// Assuming the device goes offline now
users = User.GetUsersAsync("userName==\"John\"", true).Result; // Instead of sending a request to the server, the response is read from the storage
JavaScript
var user;
Apiomat.User.getUsers("userName==\"John\"", { // The response to this request gets saved in persistent storage
onOk : function(result) {
users = result;
}
onError : function(error) {
console.log("An error occurred: ", error);
}
}, true);
// Assuming the device goes offline now
Apiomat.User.getUsers("userName==\"John\"", { // Instead of sending a request to the server, the response is read from the storage
onOk : function(result) {
users = result;
}
onError : function(error) {
console.log("An error occurred: ", error);
}
});
TypeScript
// Assuming Datastore is already configured
await Datastore.Instance.initOfflineHandler();
 
let users = User.getUsers('userName==\"John\"', false, true); // The response to this request gets saved in persistent storage
 
// Assuming the device goes offline now
users = User.getUsers('userName==\"John\"'); // Instead of sending a request to the server, the response is read from the cache

For Android and Android SQLite:

Regarding the context mentioned in the code example for Android: In Android, an SQLite Database gets used as persistent storage. The database needs to know about the application context. You have to set it either directly or by setting a caching strategy. Otherwise an exception will be thrown.

For Android SQLite:

Don't create an instance of a dao class by yourself. Dao classes represent your sqlite tables and each class automatically creates an dao object for you.

For C#:

A SQLite Database gets used as persistent storage. You have to initialize the OfflineHandler for that - either by calling the InitOfflineHandler()-Method or by setting a caching strategy. Otherwise an exception will be thrown.

You can pass the database path for the offline-storage-database as string-parameter for the "InitOfflineHandler"-method. If you don't pass it or pass null, the current directory of your binaries will be used to store the file. This seems to work without any problems, but iOS and Android-apps built with Xamarin will need to set a path. Otherwise, the offline storage will just not work.

For iOS:

  • You must add the SystemConfiguration.framework so that the SDK can check the state of connection

  • In the Objective C SDK offline handling can currently only be used with the asynchronous methods.

For TypeScript:

By default a SQLite Database gets used as persistent storage. You have to initialize the OfflineHandler for that - either by calling the initOfflineHandler()-Method or by setting a caching strategy. Otherwise an exception will be thrown.

The OfflineHandler can also be configured to use a different database. By default the sql.js node module which creates a sqlite database file if you are using the TypeScript SDK in an node environment and if you are using the SDK in an browser environment, the sql.js module will use the LocalStorage. For Browser usage, you also have to configure the used entities (DAO classes) - every entity will have its own database table. See here for a detailed overview about the configuration of the OfflineHandler.

Images and Files

When an attribute of a class is an image or file, the persistence configuration will be the same as for the class.

Example:

You create a class MyUserWithImage, which inherits from the standard User class, but has an additional image attribute.
You set the Datastore to use persistent storage for this class (MyUserWithImage). Then you load the image attribute. The response (the image) will be saved at the persistent storage.

The same works for files.

There's a limit for offline storage in the device. The limit gets ignored when uploading while the device is online (nothing gets stored this way anyway), but an exception gets thrown if the device is offline. When downloading a file, it only gets cached (either in memory or persistently, depending on the setting) if the limit isn't exceeded, otherwise an error will be logged (but no exception thrown). This limit can be configured via the Datastore and defaults to 15 MB.

References

References to other classes are stored persistently according to the setting of the class of the reference.

Example:

You create a class Schedule. And you create a class Student, which inherits from the standard class User. You add a reference attribute in the Student class to the Schedule class (a student has a schedule). You set the Datastore to use persistent storage for the Schedule class. You load a student. It won’t get saved persistently. You load the schedule attribute on the student. The response will be saved to persistent storage.

Collections and Queries

When fetching a collection of objects, the fetched collection gets stored. This includes saving the contained single objects, which has several implications:

  • Data is not saved redundantly, which saves space

  • The data of the objects you keep online is as up to date as possible. For example, an object gets fetched once on device A. Then it gets changed on device B and updated in the backend. And later device A fetches a collection that contains that object. Now when loading the single object on device A, it’s already up to date.

  • The collections are as up to date as possible as well. For example: A collection gets fetched and stored. Then the device goes offline. During that time, one object gets changed and saved. The change is not reflected in the backend yet, but in the stored object. Now, while still being offline, when fetching the collection that contains the object, all data is up to date.

Now that a collection and its items are stored, you can fetch it even if the device is offline.

There's a difference between fetching a collection (with optional query) the common way and a specialized offline way.

  • Common online + offline (Method for example MyClass.getMyClasssAsync(query)):

    • When using a query when fetching a collection, the query string gets associated with the resulting collection. So:

    • 1) For fetching the collection offline, you have to use the same query.

    • 2) Due to the "as up to date as possible"-policy mentioned earlier, an object could have been updated in the meantime with a value that doesn't fit the query anymore, but still gets included in the query. For example: You query "x < 10" and get two objects, one with x = 2, one with x = 3. Now one object gets changed to x = 15. When fetching the collection with the same query - "x < 10", the x=15 object will be included. This is because when using this method, the full client-side query capabilities aren't used.

  • Offline only (Method for example MyClass.getMyClasssFromPersistentStorageAsync(query, order)):

    • Some SDKs offer full client-side query capabilities. You can use SQLite "where" and "order" clauses to query the data that's stored persistently in the SQLite storage.

    • 1) Data that's stored in the in-memory cache won't be taken into consideration when using this method!

    • 2) Data that's not in the offline storage, but only online, won't be taken into consideration when using this method!

As mentioned in the note above, there's an extra method for querying data from the SQLite DB that's used in some SDKs as persistent offline storage, with full SQLite query capabilities:

// whereClause and orderByClause can contain SQLite compliant where and order clauses
public static async Task<IList<MyClass>> GetMyClasssFromPersistentStorageAsync(string whereClause = null, string orderByClause = null)

Usage example:

IList<Product> products = Product.GetProductsFromPersistentStorageAsync("Price < 50");
// where and orderBy can contain SQLite compliant where and order clauses, limit is a number
public static async getMyClasssFromPersistentStorage(where: string = "", orderBy: string = "", limit?: number): Promise<MyClass[]>

Usage example:

const products = await Product.getProductsFromPersistentStorage("Price < 50");
// where can contain SQLite compliant where clause
public static List<? extends AbstractClientDataModel> GetObjectsFromPersistentStorageWithWhereClause( String whereClause )

Usage example:

( List<Product> ) products = ( List<Product> ) Product
.GetObjectsFromPersistentStorageWithWhereClause( "Price < 50" );

Deleting objects and collections from storage

In some use cases you might want to keep data in the backend, but you need to remove it from the device storage.

To remove an object from storage:

Android
MyClass myClassObject = new MyClass();
myClassObject.save(); // save() without parameter includes a subsequent load, which saves the object in cache or persistent storage (depending on configuration)
 
myClassObject.deleteFromStorage(); // Deletes the object from storage
C#
MyClass myClassObject = new MyClass();
await myClassObject.SaveAsync(); // SaveAsync() without parameter includes a subsequent load, which saves the object in cache or persistent storage (depending on configuration)
 
myClassObject.DeleteFromStorage(); // Deletes the object from storage
JavaScript
var myClassObject = new Apiomat.MyClass();
myClassObject.save({ // save() without parameter includes a subsequent load, which saves the object in cache or persistent storage (depending on configuration)
onOk : function(obj) {
Apiomat.Datastore.getInstance().deleteObjectFromStorage(myClassObject.getID(), true); // use "true" to remove object from persistent storage, otherwise from cache
},
onError : function(error) {
console.log("An error occurred: ", error);
}
});
TypeScript
const myClassObject = new MyClass();
await myClassObject.save(); // save() without parameter includes a subsequent load, which saves the object in cache or persistent storage (depending on configuration)
await myClassObject.deleteFromStorage();

To remove all objects of a previously fetched collection from storage:

Android
List<MyClass> myClassObjectList = MyClass.getMyClasss("myAttribute > 5"); // Fetches all objects filtered by the query
 
MyClass.deleteAllFromStorage("myAttribute > 5"); // This must be the same query!
C#
IList<MyClass> myClassObjectList = await MyClass.GetMyClasssAsync("myAttribute > 5"); // Fetches all objects filtered by the query
 
MyClass.RemoveFromStorage("myAttribute > 5"); // This must be the same query!
JavaScript
Apiomat.MyClass.getMyClasss("myAttribute > 5"), { // Fetches all objects filtered by the query
onOk : function(result) {
Apiomat.MyClass.deleteAllFromStorage("myAttribute > 5"), { // This must be the same query!
onOk : function(obj) {
// continue here
},
onError : function(error) {
console.log("An error occurred: ", error);
}
});
},
onError : function(error) {
console.log("An error occurred: ", error);
}
});
TypeScript
const myClassObjectList = await MyClass.GetMyClasss("myAttribute > 5"); // Fetches all objects filtered by the query
 
MyClass.deleteAllFromStorage("myAttribute > 5"); // This must be the same query!

1) This only deletes the objects of a collection that was previously fetched with GetAll<className>s() / GetAll<className>sAsync(). If you fetched references objects, for example with myClass.loadMyReferencedObjects(), the objects of the class "MyReferencedObjects" won't be deleted when calling MyReferencedObjects.RemoveFromStorage().

2) If you loaded two collections, each with a different query, and both contain a specific object, this object won't be deleted if you delete one of the collections. Only the collection (meta data) is deleted, so that when calling GetAll<className>s(), no offline objects will be found (which is what you want in most cases).

To remove all objects of a class:

C#
IList<MyClass> myClassObjectList = await MyClass.GetMyClasssAsync("myAttribute > 5"); // Fetches all objects filtered by the query
MyClass myObject = myClassObjectList[0];
List<OtherClass> otherClassObjectList = await myObject.LoadOtherClasssAsync("otherAttribute < 10"); // Fetches referenced objects filtered by the query
List<OtherClass> otherClassObjectList2 = await OtherClass.GetOtherClasssAsync("otherAttribute > 20"); // Fetches all objects filtered by the query
 
// Now one collection with MyClass objects and two collections with OtherClass objects are stored.
// Each collection can be removed on its own, when using the respective query.
// For example: OtherClass.RemoveFromStorage("otherAttribute < 10");
// But some OtherClass objects would still remain.
// To delete ALL objects of the class:
OtherClass.RemoveAllFromStorage(); // Deletes ALL OtherClass objects, independent of the previously loaded collections.
TypeScript
const myClassObjectList = await MyClass.GetMyClasss("myAttribute > 5"); // Fetches all objects filtered by the query
const myObject = myClassObjectList[0];
const otherClassObjectList = await myObject.LoadOtherClasss("otherAttribute < 10"); // Fetches referenced objects filtered by the query
const otherClassObjectList2 = await OtherClass.GetOtherClasss("otherAttribute > 20"); // Fetches all objects filtered by the query
 
// Now one collection with MyClass objects and two collections with OtherClass objects are stored.
// Each collection can be removed on its own, when using the respective query.
// For example: await OtherClass.deleteAllFromStorage("otherAttribute < 10");
// But some OtherClass objects would still remain.
// To delete ALL objects of the class:
await OtherClass.removeAllFromStorage(); // Deletes ALL OtherClass objects, independent of the previously loaded collections.

When using the REST API directly

When you use the REST API directly you can’t take advantage of the ready-made functionality built into the SDKs. But just like the SDKs use the REST API, you can build your own offline handling with the available information.

You have to check the device connectivity and availability of offline data by yourself, but you can leverage the “If-None-Match”-Header. This is usually only used for ETags of collections, but we also use it for single objects (modified since). For example:

You fetch a collection of objects. You receive the result and an ETag in the “If-None-Match”-Header. You save both. When sending the request again, load the saved ETag and set it as value of the “If-None-Match”-Header. You either receive a HTTP 304 (not modified) response, meaning that the data hasn’t changed and you can use the data that’s in the storage. Or you receive a new collection with a new ETag. Override the old data and ETag.

You fetch a single object. You receive the result and a last modified date in the “If-None-Match”-Header. Use this information the same way as the ETag of collections.

Note: When fetching collections with queries the server will neither consider nor set an “If-None-Match”-Header.

Additional notes

When using offline handling you will need to add the following permission to the Android manifest: “android.permission.ACCESS_NETWORK_STATE”