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.
What’s offline handling?
Offline handling is the management of the data when the user's device is offline. Requests to the ApiOmat server fail and storing/ receiving capabilities are blocked without offline handling when the device is offline. With offline handling, both problems are solved:
-
Outgoing save and delete requests are queued to be sent later (as soon as the device is online again)
-
Responses to requests that fetch data get saved while the device is still online, so you get the data from a saved response when doing an offline request.
The data gets saved either in persistent storage or in an in-memory storage, depending on the configured strategy, as explained below.
Configuration
As a developer, you might have different requirements towards the offline handling, so we provide several configuration options. You can pick a behavior (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:
-
NETWORK_ELSE_CACHE: Only use the cache if the server is unreachable or returns 304 (local and remote data are the same)
-
NETWORK_ONLY: Don’t use caching (on save as well as read)
-
CACHE_THEN_NETWORK: First read from cache, than send a request to the server (leading to a second callback)
-
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:
Datastore.setCachingStrategy(Datastore.AOMCacheStrategy.NETWORK_ONLY,
this
.getApplicationContext());
[AOMDatastore setCachingStrategy:AOM_NETWORK_ONLY];
DataStore.sharedInstance.config.cacheStrategy = .NetworkOnly
Datastore.SetCachingStrategy(Datastore.AOMCacheStrategy.NETWORK_ONLY);
Apiomat.Datastore.setCachingStrategy(Apiomat.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 open, but offline handling needs persistent storage, so that the user can close and re-open your app or reboot their mobile device and still see data instead of a blank page.
There are two ways to enable persistent storage:
-
You can either enable it for a class in general. From then on, all requests related to that class will be stored persistently.
-
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:
// 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
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;
}
}];
let user = [User]()
User.usePersistentStore =
true
// The response to this request gets saved in persistent storage
User.loadList(query:
"userName==\"John\""
, usePersistentStorage: User.usePersistentStore) { (models, error) in
if
error ==
false
{
user = models as? [User]
}
}
// Assuming the device goes offline now
// Instead of sending a request to the server, the response is read from the storage
User.loadList(query:
"userName==\"John\""
, usePersistentStorage: User.usePersistentStore) { (models, error) in
if
(!error){
user = models
}
}
// 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
// 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);
}
});
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:
// 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
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];
var user: [User]?
// The response to this request gets saved in persistent storage
User.loadList(query:
"userName==\"John\""
, usePersistentStorage:
true
) { (models, error) in
/* Load finished */
if
error ==
false
{
user = models as? [User]
}
}
// Assuming the device goes offline now
User.loadList(query:
"userName==\"John\""
, usePersistentStorage:
true
) { (models, error) in
/* Load finished */
if
error ==
false
{
user = models
}
}
// 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
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);
}
});
For Android:
Regarding the context mentioned in the code example for Android: In Android, an SQLite Database is 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 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 just won't work.
For iOS:
-
You must add the SystemConfiguration.framework so that the SDK can check the state of connection
-
The Objective C SDK offline handling can currently only be used with the asynchronous methods.
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 will only be 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 with 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"
);
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:
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
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
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);
}
});
To remove all objects of a previously fetched collection from storage:
List<MyClass> myClassObjectList = MyClass.getMyClasss(
"myAttribute > 5"
);
// Fetches all objects filtered by the query
MyClass.deleteAllFromStorage(
"myAttribute > 5"
);
// This must be the same query!
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!
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);
}
});
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:
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.
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”