-
Notifications
You must be signed in to change notification settings - Fork 545
A User's Perspective on RoboSpice
RoboSpice addresses two important issues of the Android platform: (1) the difference between an Activity
and a Service
, and (2) how many Thread
s 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.
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
).
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 RequestListener
s 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 RequestListener
s 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 RequestListener
s 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 RequestListener
s 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 Persister
s 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.
Persister
s 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
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;