Since the early days of Android, I've been looking for a robust way to build Android apps, keep the IO operations out of the UI Thread, avoid duplicated network calls, cache relevant things, update the cIache at the right time, etc... with the cleaner syntax possible.
This blog post won't give a precise implementation, but one possible way to structure an app with a good balance between flexibility, readability and robustness.
Some existing solutions
At the beginning of Android, most people were relying on AsyncTasks for long running processes. Basically: it sucked, there's already a lot of articles on this subject. Later on, Honeycomb introduced Loaders which were better for supporting configuration changes. In 2012, Robospicecame out, based on an Android Service
running in the background. Thisinfographic shows how it works.
It's great compared to AsyncTask
, but I still have some problems with it. Here's the average code to make a request with Robospice, in an Activity. No need to read it precisely, it's just to give you an idea:
FollowersRequest request = new FollowersRequest(user);
lastRequestCacheKey = request.createCacheKey();
spiceManager.execute(request, lastRequestCacheKey,
DurationInMillis.ONE_MINUTE,
new RequestListener<FollowerList> {
@Override
public void onRequestSuccess(FollowerList listFollowers) {
// On failure
}
@Override
public void onRequestFailure(SpiceException e) {
// On success
}
});
And the Request that goes with it:
public class FollowersRequest extends SpringAndroidSpiceRequest<FollowerList> {
private String user;
public FollowersRequest(String user) {
super(FollowerList.class);
this.user = user;
}
@Override
public FollowerList loadDataFromNetwork() throws Exception {
String url = format("https://api.github.com/users/%s/followers", user);
return getRestTemplate().getForObject(url, FollowerList.class);
}
public String createCacheKey() {
return "followers." + user;
}
}
Problems
- This code looks awful and you have to do it for each request!
- You need to create a
SpiceRequest
subclass for each type of request - You need to create a
RequestListener
for each request - If the cache expires shortly, the user will have to wait at each call
- If the cache expires after a long time, the user could see obsolete data
- The
RequestListener
keeps an implicit reference to the activity, what about memory leaks?
Not so good…
Concise and robust in five steps
When I started working on Candyshop, I tried something else. I mixed different libraries with very interesting features and tried to make something concise yet robust.
- AndroidAnnotations, for @Background, @EBean, etc...
- Spring RestTemplate for REST network calls, well integrated with AndroidAnnotations
- SnappyDB for fast disk caching of java objects
- EventBus as an event bus
Here's a global schema of what I'll explain in the next parts.
Step 1 — An easy to use cache system
You'll need a persistent cache system. Keep it simple.
@EBean
public class Cache {
public static enum CacheKey { USER, CONTACTS, ... }
public <T> T get(CacheKey key, Class<T> returnType) { ... }
public void put(CacheKey key, Object value) { ... }
}
Step 2 — A REST client
I give this as an example, just make sure the logic of the REST API you're using stays in one place.
@Rest(rootUrl = "http://anything.com")
public interface CandyshopApi {
@Get("/api/contacts/")
ContactsWrapper fetchContacts();
@Get("/api/user/")
User fetchUser();
}
Step 3 — An application-wide event bus
Instantiate it in a strategic place, accessible from anywhere in the app, the Application
object is a good candidate for that.
public class CandyshopApplication extends Application {
public final static EventBus BUS = new EventBus();
...
}
Step 4 — An Activity which needs some data!
My solution is, like Robospice, based on a service, but not an Android one. A regular singleton object, shared accross the app. We'll see the code of that service in step 5. But right now, let's see how the Activity code looks like, because this is what I wanted to simplify the most in the first place!
@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {
// Inject the service
@Bean protected AppService appService;
// Once everything is loaded…
@AfterViews public void afterViews() {
// … request the user and his contacts (returns immediately)
appService.getUser();
appService.getContacts();
}
/*
The result of the previous calls will
come as events through the EventBus.
We'll probably update the UI, so we
need to use @UiThread.
*/
@UiThread public void onEvent(UserFetchedEvent e) {
...
}
@UiThread public void onEvent(ContactsFetchedEvent e) {
...
}
// Register the activity in the event bus when it starts
@Override protected void onStart() {
super.onStart();
BUS.register(this);
}
// Unregister it when it stops
@Override protected void onStop() {
super.onStop();
BUS.unregister(this);
}
}
One line to request the user, one line to express the fact we'll receive an answer for that request. Same thing for contacts. Sounds really good!
Step 5 — A singleton service
As I said in step 4, the service I'm using is not an Android service. I actually started with one, but I changed my mind. The reason issimplicity. Services are meant to be used when you need to have something running while no activity is displayed, or when you want to make some code available to other apps. That's not exactly what I wanted. Using a simple singleton allows me to avoid using ServiceConnection
, Binder
, etc...
There's many things to say here. Let's start with a schema to show you what happens when we called getUser()
and getContacts()
from the Activity. Then I'll explain the code.
You can imagine each serial is a thread.
What you see here is what I really like about this model; the view is immediately filled with cached data, so most of the time the user doesn't have to wait. Then, when the up-to-date result arrives from the server, the displayed information is replaced. The counterpart of this is that you need to ensure the activity can receive the same type of response multiple times. Keep it in mind while creating the activity and you'll be okay.
Okay, let's see some code!
// As I said, a simple class, with a singleton scope
@EBean(scope = EBean.Scope.Singleton)
public class AppService {
// (Explained later)
public static final String NETWORK = "NETWORK";
public static final String CACHE = "CACHE";
// Inject the cache (step 1)
@Bean protected Cache cache;
// Inject the rest client (step 2)
@RestService protected CandyshopApi candyshopApi;
// This is what the activity calls, it's public
@Background(serial = CACHE)
public void getContacts() {
// Try to load the existing cache
ContactsFetchedEvent cachedResult =
cache.get(KEY_CONTACTS, ContactsFetchedEvent.class);
// If there's something in cache, send the event
if (cachedResult != null) BUS.post(cachedResult);
// Then load from server, asynchronously
getContactsAsync();
}
@Background(serial = NETWORK)
private void getContactsAsync() {
// Fetch the contacts (network access)
ContactsWrapper contacts = candyshopApi.fetchContacts();
// Create the resulting event
ContactsFetchedEvent event = new ContactsFetchedEvent(contacts);
// Store the event in cache (replace existing if any)
cache.put(KEY_CONTACTS, event);
// Post the event
BUS.post(event);
}
}
That's a lot of code for a single request! Actually, I exploded it to make it more explanatory, but it's always the same pattern so you can easily create helpers to make single-lined methods. For example getUser()
would look like this:
@Background(serial = CACHE)
public void getUser() {
postIfPresent(KEY_USER, UserFetchedEvent.class);
getUserAsync();
}
@Background(serial = NETWORK)
private void getUserAsync() {
cacheThenPost(KEY_USER, new UserFetchedEvent(candyshopApi.fetchUser()));
}
So what about the serial
thing? Here's what the doc says:
By default, all
@Background
annotated methods are run in parallel. Two methods using the sameserial
are guaranteed to be run on the same thread, sequentially (ie. one after the other).
Running network calls one after the other may have performance impacts, but it's so much easier to deal with GET-after-POST kind of things with it, that I'm ready to sacrifice a little performance. Moreover, you can easily tune the serials afterward to improve performance if you notice anything. Currently in Candyshop, I use four different serials.
To conclude
The solution I described here is a draft, it's the basic idea I started with, a few months ago. As of today, I've been able to solve all particular cases I encountered, and I really enjoy working with it so far. There's a few other awesome things I'd like to share about this model, like error management, cache expiration, POST requests, cancelling of useless ops, but I'm really grateful you read so far, so I won't push it!
What about you? Did you find the design of your dreams, one that you enjoy working with on a daily basis?
转自:http://blog.joanzapata.com/robust-architecture-for-an-android-app/
http://blog.jobbole.com/66606/