Hi, Habr! I want to tell you about the Chronos for Android library (API level> = 9), the purpose of which is to facilitate writing long operations, such as network requests, or database calls.
What problem we solve?It's no secret that for Android, the task of performing asynchronous operations has always been one of the most frequently encountered. Indeed, very few applications work exclusively offline, and where you can do without network interaction. And absolutely tiny part of them goes without reference to the device’s permanent memory, be it a database, Preferences or a regular file. However, throughout the history of the development of the system, we have never been offered a single, fairly convenient solution out of the box.

What solved the problem - a brief historyLet's take a look at the existing toolkit in the context of the task “work out the click on the“ authorization ”button”. Actually, what do we have?
1. Standard streams')
Button signInButton = (Button) findViewById(R.id.button_auth); signInButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { final Activity activity = AuthActivity.this; showProgress(); new Thread(new Runnable() { @Override public void run() { APIFactory.getApi().signIn(); activity.runOnUiThread(new Runnable() { @Override public void run() { goToMainContent(); } }); } }).start(); } });
This code is literally everything. It is difficult to read, memory flows in it, it cannot be canceled, the screen rotation is not processed in it, as well as any API call errors (and if they are processed, then everything will look completely indigestible).
2. AsynkTask Button signInButton = (Button) findViewById(R.id.button_auth); signInButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { new AuthTask().execute(); } }); private class AuthTask extends AsyncTask<Void, Void, Boolean>{ @Override protected void onPreExecute() { showProgress(); } @Override protected Boolean doInBackground(final Void... params) { try { APIFactory.getApi().signIn(); }catch (Exception e){ return false; } return true; } @Override protected void onPostExecute(final Boolean result) { if(!isCancelled() && result) { goToMainContent(); } } }
Already a little better, but still not enough. Appeared readable error handling, the ability to cancel. However, until now this code is not able to work properly when the screen is rotated at the time of the request to the API - the link to the Activity in which the class is defined flows away.
3. LoaderWhen Google introduced the Loaders, it seemed that they would become Silver bullet for asynchronous requests, shifting the classic AsyncTask and at that time. Unfortunately, the miracle did not happen. At the moment, Loaders are a rare guest in commercial projects, since they were very inconvenient to use. In this section, I will not give the code by analogy with the previous two. Instead, I recommend a curious reader to get acquainted with the official guide on this technology in order to assess the amount of code required by the Loader:
developer.android.com/reference/android/content/AsyncTaskLoader.html4. ServiceServices are good for performing long tasks that “hang” in the background during the use of the application. However, to launch operations, the result of which is needed here and now, the structure of services is not ideal. Mainly, the limitation is imposed by the method of data transmission via Intent, which, firstly, contains only a limited amount of data, and secondly, requires that the transmitted data be serializable in one way or another. On this technology, the popular library of
Robospice works .
What does Chronos offer?
Chronos does all the work for you in performing the task in a parallel thread and delivering the result or execution error to the main thread. Roughly speaking, this library provides a container for any kind of long operations.
There is a full-fledged wiki in the project, part of the code from there will be used in the article, but for a more complete guide, please contact
github.com/RedMadRobot/Chronos/wiki .
ExampleLet's solve a typical problem using Chronos: in the Activity, you need to request some object from a certain repository, access to which is long enough not to make a request in the UI stream. First, we write the code, and then we analyze what we did.
1. The first thing you need to connect Chronos to the project. To do this, simply write the dependency in the gradle:
compile 'com.redmadrobot:chronos:1.0.5'
2. Now we will describe Activity. The base class
ChronosActivity is one of the library components, however you can easily write its analogue, examples of which are in the documentation. Also Chronos can be used in fragments, the code will not differ.
class MyActivity extends ChronosActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Button startButton = (Button) findViewById(R.id.button_start); startButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { runOperation(new MyOperation()); } }); } public void onOperationFinished(final MyOperation.Result result) { if (result.isSuccessful()) { showData(result.getOutput()); } else { showDataLoadError(result.getError()); } } private void showData(BusinessObject data){
3. And finally, we describe the business logic of receiving data in the class
MyOperation :
class MyOperation extends ChronosOperation<BusinessObject> { @Nullable @Override public BusinessObject run() { final BusinessObject result ;
That's all. Let's see in detail what happens in this code. Start over.
Setting UI Class class MyActivity extends ChronosActivity {
To work with Chronos, the Acvitity base class or a fragment must either inherit from those offered in the library, or contain specific code in the life-cycle methods, examples can be seen in the
documentation .
Running operation runOperation(new MyOperation());
This is where the base method of the ChronosActivity class is called, to which the newly created activity is passed. Immediately after calling this method, Chronos will take the operation into the queue and begin its execution in a parallel thread.
Processing the result of the operation public void onOperationFinished(final MyOperation.Result result) { if (result.isSuccessful()) { showData(result.getOutput()); } else { showDataLoadError(result.getError()); } }
This method will be called after the operation is completed, or an exception is thrown during execution. Such handler methods must have a signature
public void onOperationFinished (ResultType) . The important point: the method will be called only between the onResume () and onPause () calls, that is, in it you can easily change the UI, without fear that it has already become invalid by that moment. Moreover, if the Activity was recreated due to rotation, going into the background, or other reasons, Chronos will return the result anyway (the only exception is that the system has run out of memory, in this case, to prevent OutOfMemory, Chronos can erase the old results data).
“Where does the call come from?”An attentive reader will notice that Activity does not implement any specific interfaces, so where does this method come from? The answer is from the code containing the reflection. The decision to make reflection instead of an interface was made because of TypeErasure in Java, which makes it impossible to simultaneously implement the same template interface with different parameters. That is, this is done so that in one Activity it is possible to process the result of any number of types of operations.
Setting the class of operation class MyOperation extends ChronosOperation<BusinessObject> {
The
ChronosOperation class encapsulates the business logic for retrieving an object of a particular type, in this case
BusinessObject . All user operations must be inherited from
ChronosOperation .
Business logic @Nullable @Override public BusinessObject run() { final BusinessObject result ;
This abstract method of the
ChronosOperation class
is responsible, in fact, for the business logic of receiving an object. It runs in a parallel thread, so you can do as long as you want in it, it will not cause lags in the application interface. Also, any exceptions thrown in it will be carefully passed to the caller without causing the application to crash.
Result Naming @NonNull @Override public Class<? extends ChronosOperationResult<BusinessObject>> getResultClass(){ return Result.class; } public final static class Result extends ChronosOperationResult<BusinessObject> { }
The following method and class are designed to enable the Activity code to write a result handler for each specific operation, specifying the class as the parameter type of the
onOperationFinished method. You can use the same result class for different operations if you want their result to be processed in the same way.
I summarize: we will collect the minimum set of code sections needed for working with Chronos.- Class of operation
- Operation invocation code in a UI object
- Result handling code in a UI object
What else is there?So why and why can Chronos be used?- Chronos assumes the transfer of data between threads, leaving you to worry only about business logic.
- Chronos takes into account all the nuances of the Life cycle of the Activity and fragments, delivering the result only when they are ready to process it, saving the data until then.
- Chronos does not leak memory. You no longer risk catching crashes because too many Activity objects flowed away.
- Chronos is covered by unit tests.
- And finally, Chronos is an open-source project. You can always take the code and rewrite it to fit your needs. Thanks to the tests, it will be easy for you to validate code changes.
Link to the project in GitHub . There you will find a complete library guide, usage examples and, of course, source code.
See also:
We put controllers on a diet: AndroidArchitectural design of mobile applications: part 1Architectural design of mobile applications: part 2