Skip to content
This repository has been archived by the owner on Jan 14, 2018. It is now read-only.

A User's Perspective on RoboSpice

Riccardo Ciovati edited this page Jan 6, 2015 · 2 revisions

RoboSpice addresses two important issues of the Android platform: (1) the difference between an Activity and a Service, and (2) how many Threads your application is running in.

These are two separate matters that must not be confused. The first affects whether or not a process will attempt to keep running in the background if the user navigates away from the app. The second affects whether you might be succeptible to application not responding failure--the dreaded ANR.

The difference between an Activity and Service is that a Service will continue to run in the background past the point where Android might have already shut it down had it been it an Activity. However, by default a Service runs in the same thread as the Activity that intends it--the so-called UI thread. Thus, if the Service blocks, it will block the user interface, leading to ANR. On the other hand, just because a separate Thread will not block the UI thread does not mean it will continue to run in the background like a Service once the app is no longer on top, for example if the device receives a phone call.

The leading Android ReST pattern has several roles to be filled: Dobjanschi presents three patterns. In slide 17 see Pattern A, slide 45 Pattern B, slide 47 Pattern C.

See Option A, page 17 Img

Although some specifics vary, in general you may understand the role of "Service" to be filled by SpiceService, the role of "Service Helper" to be filled by SpiceManager, that of "Processor" to be filled by RequestProcessor, and that of "ContentProvider" by CacheManager. The is the default manner of operation for RoboSpice. However, you can use RoboSpice to implement other patterns. For example, you can achive Dobjanschi's Option B by putting the SpiceManager behind a ContentProvider.

The SpiceService works the business of RoboSpice--networking and caching--while SpiceManager provides the client API for you, the progammer, to use to enjoy the benefits of RoboSpice.

##How it Works

You already know that once you start the SpiceService--it being a Service--it may continue to run even though the user has navigated away from the Activity that caused it. The SpiceManager, however, has a lifecycle connected with an Activity. Your app can start a second Activity that will construct its own, separate SpiceManager for you to use to interact with the same, original, running SpiceService from the first Activity.

SpiceManager implements the Runnable interface, doing all its business inside a run method consisting of an infinite loop that takes requests off its queue and adds them to the SpiceService. For you, SpiceManager provides two aptly-named methods, start() and stop(). The start() method constructs a new Thread, passing itself to be run. When you start a SpiceManager running, it gets bound to the SpiceService, which it connects to, by Intent, creating it if needed. Note well: before the SpiceService ever tries for the first time to take a request from its queue, it is already in a separate Thread.

Since one of our goals is to avoid the ANR, when our view requests data, it must do so using a method that returns immediately, rather than blocking until the data is available. Thus, since we cannot use the return value of the method to pass the requested data, we must use a listener callback. The method that RoboSpice provides is named execute, and it has two parameters: the request and the callback object. The callback object implements the RoboSpice RequestListener interface, which declares just two methods, one each for success and failure. The sole parameter of onRequestSuccess is the response data, and the sole parameter of onRequestFailure is a SpiceException. Be clear: failure does not raise an exception, rather it passes a SpiceException object as an argument to the failure callback.

As mentioned, SpiceManager has a queue of requests, that is, a thread-safe Java utility queue of CachedSpiceRequest objects. As the client, you add a Request to the Queue by calling the execute() method on the SpiceManager passing it your Request as the first argument, and your RequestListener as the second argument.

The SpiceService has a RequestProcessor, which it constructs using three things, the Application Context, a CacheManager that you configure yourself, and a Java Executor Service (part of the concurrency features of Java).

While it is running, the SpiceManager (again, not in the UI Thread) waits for CachedSpiceRequests to appear in its queue. As each CachedSpiceRequest, including its RequestListener, appears in its queue, the SpiceManager adds that request and litener to the SpiceService (which delegates to the RequestProcessor).

##RequestProcessor

The RequestProcessor keeps its own mapping from each request to that request's listeners. When the SpiceManager (via the SpiceService) adds a CachedSpiceRequest to the RequestProcessor, the RequestProcessor adds, as necessary, that request & listener to its Map, and then submits to the Executor Service a Runnable that, when run, will call the RequestProcessor's processRequest method on the request. By sumbitting the Runnable to the executor service, a Future is returned that the RequestProcessor adds to the request.

##Processing the Request

Suppose that you called execute() and your request was added to the queue of the SpiceManager, which was running in its own thread. When your request came to the fore of its queue, the SpiceManager added the request to the SpiceService, which adds the request to the RequestProcessor. RoboSpice comes with a RequestProcessor that may be suitable for your needs. If it is not, then you can drop in your own replacement.

###Using a Custom RequestProcessor

Customizing RoboSpice with your own RequestProcessor subclass is easy. Simply extend the RequestProcessor class, overriding the addRequest() method that SpiceService uses. Then, in your extension of SpiceService, override the createRequestProcessor() method to return your custom RequestProcessor extension. There are a few more details to consider, but that's the essential gist of it.

###Using the standard RequestProcessor

To understand what happens when the RequestProcessor as it is distributed with RoboSpice processes a request, start by remembering that the request instance is not just a SpiceRequest, but a CachedSpiceRequest, which means that it has a cache key and a timeout duration set on its constructor. The first thing the RequestProcessor will try to do is to load data from the cache using the key and according to the timeout value. If the RequestProcessor successfully retrieves timely data from the cache, then the request's RequestListeners are passed the data and the processing ends.

Alternatively, if the cache did not have the data for that key or such data were too old, then the RequestProcessor will next try the network. In that case the RequestProcessor calls the request's loadDataFromNetwork() method, which you probably might have defined yourself. For example, if your request is a SpringAndroidSpiceRequest, and you want to do a ReST GET request, then your request could define its loadDataFromNewtwork() method to:

return getRestTemplate.getForObject(url, responseClass);

If an Exception is raised during network execution, then the request RequestListeners are notified of a NetworkException and processing ends. If the network is down, the process will end without calling loadDataFromNetwork and the request's listeners will be informed of failure with a NoNetworkException.

If the network request is successful then the RequestProcessor tries to save the data to the cache and, the data once cached, notifies the request listeners of the success and of the resulting data. If, despite a successful network load, the data cannot be cached, either because your loadDataFromNetwork() method returned null, or else because your cache key is null, then the request RequestListeners will be notified of success just as if the data had been saved to the cache. However, if a CacheSavingException is raised, then the request RequestListeners will be notified of the failure but only if the RequestProcessor setFailOnCacheError(true) has been set.

Be aware that before the RequestProcessor begins to process a request, it first sets the request's progress listener to be notified of progress while loading data from network.

All of the foregoing assumes that the given request is processable, which it might not be if CachedSpiceRequest.setProcessable() is used to change the default, in which case listeners will be notified the request was processed, but the processRequest() method in the RequestProcessor will return.

##Configuring Your Cache: Introducing CacheManager

The SpiceManager and RequestProcessor objects access the cache through the CacheManager. The methods of CacheManager take as an argument the class of data to be cached or retrieved. Based on that class, the CacheManager finds the appropriate ObjectPersister that will do the actual persistence. In order for your cache to work, you must have at least one ObjectPersister that can handle each class you're wanting to cache.

A RoboSpice client creates the CacheManager in a SpiceService extension by overriding the createCacheManager() method, and this is where you add your desired Persisters to the CacheManager. You must add them in the order you want them to be in the chain of responsibility for handling a particular class of object.

Persisters extend the abstract class ObjectPersister, which declares but does not implement methods such as loadDataFromCache and saveDataToCacheAndReturnData, as well as removeDataFromCache(), removeAllDataFromCache(), and others, including a method named canHandleClass() that takes a Class object and returns true or not depending on whether the Persister can handle that class.

The RequestProcessor that is distributed with RoboSpice uses the CacheManager in a couple places, first to check the cache for a request's data before trying to get the data from the network, and second, after a successful network request to insert the response data into the cache. The CacheManager's loadDataFromCache() method takes as arguments the class of data, the cache key, and the timeout duration in milliseconds. The saveDataToCacheAndReturnData method takes as arguments the data and cache key. The RequestProcessor also calls the CacheManager's removeDataFromCache() method if an exception is raised while saving or retriving cache data.

Likewise, the SpiceService uses the CacheManager in a couple places: it creates the CacheManager that the RequestProcessor is passed in its constructor. Beyond that, three SpiceService methods: getAllCacheKeys(), loadAllDataFromCache(), and getDataFromCache(), are delegates to CacheManager methods having the same signatures.

An ObjectPersister implements loadDataFromCache(class, cacheKey, timeout) and saveDataToCacheAndReturnData(data, cacheKey), and calls to those methods will be delegated through the CacheManager's chain-of-responsibility. ObjectPersister itself is abstract. For example, if you just need to cache a String, you might choose InFileStringObjectPersister. If you want to save your cached data in a database, on the other hand, you might choose as a Persister an instance of the concrete InDatabaseObjectPersister), which is included with the ORMLite extension to RoboSpice.

#Caching Using a Database: InDatabaseObjectPersister

InDatabaseObjectPersister, as an extension of ObjectPersister, defines the loadDataFromCache() method. For this it uses an instance of RoboSpiceDatabaseHelper that is passed into its constructor. It is also worth noting that the InDatabaseObjectPersister, in its constructor, executes the necessary SQL DDL statements to create the tables in the database.

##RoboSpiceDatabaseHelper

RoboSpiceDatabaseHelper is a concrete extension of the abstract OrmLiteSqliteOpenHelper, which in turn extends Android's native abstract SQLiteOpenHelper, which is used to manage database creation and version management. The OrmLite class adds methods including getDao(). It also defines a two-argument overloading of onCreate() that takes an Ormlite ConnectionSource in addition to the native Android SQLiteDatabase.

The RoboSpiceDatabaseHelper adds specific features for using a database. For example, for the purpose of querying and retrieving data from the database, the helper defines a method named queryForIdFromDatabase(), which takes an id String representing a database primary key and a class identifying the table, and returns an ormlite object backed by the apprapriate data from the database.

It is important to note here that some of these helper functions are specific to using the Robospice cache key, such as queryCacheKeyForIdFromDatabase method, which is a specialized version of queryForIdFromDatabase that is specific to the table of cache keys. The table of cache keys is where Robospice stores rows relating a cache key to: (1) the result class name, (2) the cache timestamp, and (3) the result id of the appropriate type. This matters because the database Persister uses cache entries as part of loading data from the cache. If the Persister's queryCacheKeyForIdFromDatabase method returns null, then loadDataFromCache will return null.

##Further Directions

One possible aspect of a sophisticated application would be the extension of InDatabaseObjectPersister to be specific to your data type. For example if you had a a model object Account then you might define a Persister subclass named InDatabaseAccountPersister.

#Review

Here are the important classes we've covered;