As of last weekend's release, the iOS and Android SDKs now provide client-side caching support (for offline behaviours).
I am preparing a blog post about this feature, but I was ill for a couple of days and am still playing catch-up, so thought I'd better post this :-)
Latest downloads are at
http://fatfractal.com/support/downloads/The example below is for IOS but the Android SDK works in pretty much exactly the same way. It's pretty straightforward to use; here's a little test harness which
- sets up local storage
- logs in
- creates some objects
- Retrieves some objects using a query, & store to cache
- simulates being offline
- Issue the query but don't ask to use the cache - should be an error
- Issue the same query again, asking to use the cache if offline - should work fine
#import <SenTestingKit/SenTestingKit.h>
#import <FFEF/FatFractal.h>
static FatFractal * ff;
static id<FFLocalStorage> localStorage;
@interface Basic : NSObject
@property (nonatomic) int anInt;
@end
@implementation Basic
@end
@interface BasicCachingTest : SenTestCase
@end
@implementation BasicCachingTest
- (void)setUp
{
[super setUp];
if (! ff) {
ff = [[FatFractal alloc] initWithBaseUrl:@"https://localhost:8443/TestApp"];
// Do any calls to registerClass:ForClazz: here, BEFORE setting the localStorage property
// This is essential when one is subclassing FFUser for example
localStorage = [[FFLocalStorageSQLite alloc] initWithDatabaseKey:@"CachingTests"];
[localStorage setDebug:NO];
[localStorage wipeAllData];
ff.localStorage = localStorage;
}
ff.debug = NO;
ff.simulatingOffline = NO;
}
- (void)tearDown
{
[super tearDown];
}
- (void) pause:(NSTimeInterval)pauseInterval
{
NSDate* cycle = [NSDate dateWithTimeIntervalSinceNow:pauseInterval];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:cycle];
}
- (NSError *) deleteObjsForQuery:(NSString *)query {
NSError *crudError;
NSArray * objs = [ff getArrayFromUri:query error:&crudError];
if (! crudError) {
for (id obj in objs) {
crudError = nil;
[ff deleteObj:obj error:&crudError];
STAssertNil(crudError, @"deleteObjectsForQuery %@ : Failed to delete %@", query, obj);
if (crudError)
break;
}
}
return crudError;
}
- (void) testBasicCreateAndRetrieve
{
// We're going to run this all synchronously but use the block methods which would be used in normal code
__block BOOL blockComplete = NO;
__block NSError *blockErr;
// Login
NSLog(@"***testBasicCreateAndRetrieve: logging in");
[ff loginWithUserName:@"TestUser_1" andPassword:@"TestUser_1" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
STAssertNil(theErr, @"Failed to login - error was: %@", [theErr localizedDescription]);
blockErr = theErr;
blockComplete = YES;
}];
while (!blockComplete) [self pause:0.001];
if (blockErr) return;
// Delete existing test objects
NSLog(@"***testBasicCreateAndRetrieve: deleting old test objects");
NSError *deleteErr = [self deleteObjsForQuery:@"/Basic"];
STAssertNil(deleteErr, @"Failed to delete existing objects");
if (deleteErr) return;
// Create 5 objects
NSLog(@"***testBasicCreateAndRetrieve: creating new test objects");
for (int i = 1; i <= 5; i++) {
blockErr = nil;
blockComplete = NO;
Basic * basic = [[Basic alloc] init]; basic.anInt = i;
[ff createObj:basic atUri:@"/Basic" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
blockErr = theErr;
STAssertNil(blockErr, @"Failed to create object - error was: %@", [blockErr localizedDescription]);
blockComplete = YES;
}];
while (!blockComplete) [self pause:0.001];
if (blockErr) return;
}
// Retrieve 2 of the objects via a query, and ask for the response to be cached
NSLog(@"***testBasicCreateAndRetrieve: retrieving objects - caching response");
blockErr = nil;
blockComplete = NO;
[[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionCacheResponse andBlock:^(FFReadResponse *response)
{
blockErr = [response error];
STAssertNil(blockErr, @"Failed to retrieve objects - error was: %@", [blockErr localizedDescription]);
blockComplete = YES;
}];
while (!blockComplete) [self pause:0.001];
if (blockErr) return;
// Simulate being offline
NSLog(@"***testBasicCreateAndRetrieve: simulating offline");
[ff setSimulatingOffline:YES];
// Issue the query again but don't ask to use the cache - should be an error
NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - NOT USING cache - should get an error response");
blockErr = nil;
blockComplete = NO;
[[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithBlock:^(FFReadResponse *response)
{
blockErr = [response error];
STAssertNotNil(blockErr, @"Should have failed to retrieve objects");
blockComplete = YES;
}];
while (!blockComplete) [self pause:0.001];
if (! blockErr) return;
// Issue the same query again, asking to use the cache if offline - should work fine
NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - USING cache");
blockErr = nil;
blockComplete = NO;
[[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionUseCachedIfOffline andBlock:^(FFReadResponse *response)
{
blockErr = [response error];
STAssertNil(blockErr, @"Failed to retrieve objects from cache - error was: %@", [blockErr localizedDescription]);
blockComplete = YES;
}];
while (!blockComplete) [self pause:0.001];
if (blockErr) return;
}
@end
FYI, the various read options which you can use (and of course you can use some of them together) are:
typedef NS_OPTIONS(NSInteger, FFReadOption) {
FFReadOptionAutoLoadRefs = (0x1 << 0), // When retrieving objects, automatically retrieve objects which they reference
FFReadOptionAutoLoadBlobs = (0x1 << 1), // When retrieving objects, automatically retrieve any BLOBs they contain
FFReadOptionCacheResponse = (0x1 << 2), // When retrieving, cache the response
FFReadOptionUseCachedOnly = (0x1 << 3), // When retrieving, ONLY try the cache - i.e. do not hit the network
FFReadOptionUseCachedIfCached = (0x1 << 4), // When retrieving, try the cache first - only hit the network if cache is empty
FFReadOptionUseCachedIfOffline = (0x1 << 5) // When retrieving, try the network first - if offline, then try the cache
};
Personally I tend to use
FFReadOptionCacheResponse | FFReadOptionUseCachedIfOffline for all of the queries that I wish to have available while offline.
One nice additional benefit - when using localStorage like this, the logged-in user is also cached - so if for example you log in and then the app is terminated, then, when the app restarts and the FatFractal object is created again, it will restore the logged-in user, and loggedIn will be true.
Give it a try, and let us know what you think!
- Gary