Version 1 - with external libraries
This is a perfect example where RxAndroid comes handy (or more general - any framework that supports event driven programming).
Let's say we have the following domain classes that allows us to fetch some data from web services:
Repository class:
public class Repository {
protected String name;
public Repository(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Service interface:
public interface GitService {
List<Repository> fetchRepositories();
}
First service implementation:
public class BitbucketService implements GitService {
@Override
public List<Repository> fetchRepositories() {
// Time consuming / IO consuming task.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// Swallow exception.
}
List<Repository> result = new ArrayList<>();
result.add(new Repository("http://some-fancy-repository.com/"));
return result;
}
}
Second service implementation:
public class GithubService implements GitService {
@Override
public List<Repository> fetchRepositories() {
// Time consuming / IO consuming task.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// Swallow exception.
}
List<Repository> result = new ArrayList<>();
result.add(new Repository("http://some-fancier-repository.com/"));
return result;
}
}
Having above we can easily create an observable (an object that looks if something had happened) that checks whether we have successfully downloaded data from both services. This responsibility have the following method:
public Observable<List<Repository>> fetchRepositories() {
// This is not the best place to instantiate services.
GitService github = new GithubService();
GitService bitbucket = new BitbucketService();
return Observable.zip(
Observable.create(subscriber -> {
subscriber.onNext(github.fetchRepositories());
}),
Observable.create(subscriber -> {
subscriber.onNext(bitbucket.fetchRepositories());
}),
(List<Repository> response1, List<Repository> response2) -> {
List<Repository> result = new ArrayList<>();
result.addAll(response1);
result.addAll(response2);
return result;
}
);
}
The only thing to do is to execute the task somewhere (example in onCreate
method):
@Override
protected void onCreate(Bundle savedInstanceState) {
(...)
AndroidObservable
.bindActivity(this, fetchRepositories())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(repos -> showRepositories(repos), error -> showErrors());
}
The code above is a place where magic happens. Here is:
- defined a context of the task,
- defined a necessity of creating async threads,
- defined that the result should be handled in UI thread when the hard job ends,
- defined which methods to call to handle results and errors.
Above, in subscribe
we are passing lambda calls to show results/errors to the user:
private void showRepositories(List<Repository> repositories) {
// Show repositories in your fragment.
}
private void showErrors() {
// Pops up some contextual information / help.
}
As Android currently uses SDK 1.7 there is a need to use a library that allows us to use lambdas in the 1.7-compliant code. Personally, I am using retrolambda for this case.
If you do not like lambdas - you always have a possibility of implementing anonymous classes wherever necessary otherwise.
This way we can avoid writing a lot of Android's boiler-plate code.
Version 2 - no external libraries
If you don't want to use external libraries you can achieve similar with AsyncTasks accompanied by Executor.
We will reuse described above domain classes: Repository
, GitService
, GithubService
and BitbucketService
.
As we want to poll how many tasks has been finished, let's introduce some kind of counter into our activity:
private AtomicInteger counter = new AtomicInteger(2);
We will share this object in our asynchronous tasks.
Next, we have to implement a task itself, for example like this:
public class FetchRepositories extends AsyncTask<Void, Void, List<Repository>> {
private AtomicInteger counter;
private GitService service;
public FetchRepositories(AtomicInteger counter, GitService service) {
this.counter = counter;
this.service = service;
}
@Override
protected List<Repository> doInBackground(Void... params) {
return service.fetchRepositories();
}
@Override
protected void onPostExecute(List<Repository> repositories) {
super.onPostExecute(repositories);
int tasksLeft = this.counter.decrementAndGet();
if(tasksLeft <= 0) {
Intent intent = new Intent();
intent.setAction(TASKS_FINISHED_ACTION);
sendBroadcast(intent);
}
}
}
Here is what has happened:
- in constructor we've injected shared counter and service that was used to fetch data,
- in
doInBackground
method we've delegated control to our dedicated service,
- in
onPostExecute
method we've tested if all expected tasks has finished,
- after all the tough job - a broadcast to the activity has been sent.
Next we have to receive potential broadcast that informs us the job has been done. For that case we implemented broadcast receiver:
public class FetchBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("onReceive", "All tasks has been finished.");
// No need to test
// if intent.getAction().equals(TASKS_FINISHED_ACTION) {}
// as we used filter.
}
}
Instead of logging message - you have to update your view.
Mentioned constant TASKS_FINISHED_ACTION
names your filter:
private static final String TASKS_FINISHED_ACTION = "some.intent.filter.TASKS_FINISHED";
Remember to initialize and register receiver and filters in both - your activity and manifest.
Activity:
private BroadcastReceiver receiver = new FetchBroadcastReceiver();
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter();
filter.addAction(TASKS_FINISHED_ACTION);
registerReceiver(receiver, filter);
}
Manifest (inside application tag):
<receiver android:name=".TestActivity$FetchBroadcastReceiver"/>
I put receiver class as public
in TestActivity
so it looks strange.
In manifest you have to also register your action (inside activity intent filter):
<action android:name="some.intent.filter.TASKS_FINISHED"/>
Remember to unregister your receiver in onPause()
method.
Having prepared activity you can execute your tasks somewhere (for example in onCreate
method like in the first example):
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
new FetchRepositories(counter, new GithubService())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new FetchRepositories(counter, new BitbucketService())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
// Below Honeycomb there was no parallel tasks.
new FetchRepositories(counter, new GithubService()).execute();
new FetchRepositories(counter, new BitbucketService()).execute();
}
As you can noticed, parallel tasks will run only on Honeycomb and above.
Before this version of Android thread pool can hold up to 1 task.
At least we've used some dependency injection and strategy patterns. :)