Developers Club geek daily blog

2 years, 11 months ago
Client-server applications are the most widespread and at the same time the most difficult in development. Problems arise at any stage, from choice of means for execution of requests to result caching methods. If you want to learn how it is possible to organize competently difficult architecture which will ensure stable functioning of your application, I ask under kat.

Android architecture of client-server application

Of course, already not 2010 when developers had to use the well-known patterns of A/B/C or in general to start AsyncTask-and and strongly to hit into tambourine. There was large number of different libraries which allow you to execute requests including asynchronously with little effort. These libraries are very interesting, and we too should begin with choice suitable. But for a start let's a little remember that at us already is.

Earlier in Android the only available means for execution of network requests was the client of Apache who is actually far from ideal, and not for nothing now Google strenuously tries to get rid of it in new applications. Later the class HttpUrlConnection became fruit of efforts of the Google developers. It has corrected situation not strongly. Still there was no opportunity to execute asynchronous requests though the model HttpUrlConnection + Loaders already is more or less operable.

2013 became very effective in this plan. There were remarkable libraries Volley and Retrofit. Volley — the library of more vista shot intended for work with network while Retrofit is specially created for work with REST Api. And the last library became the conventional standard when developing client-server applications.

At Retrofit, in comparison with other means, it is possible to select some primary benefits:
1) The extremely convenient and simple interface which provides full functionality for execution of any requests;
2) Flexible setup — it is possible to use any client for execution of request, any library for analysis of json, etc.;
3) Lack of need independently to execute parsing json-and — this work is performed by Gson library (and already not only Gson);
4) Convenient processing of result and errors;
5) Support of Rx that too is important factor today.

If you are not familiar with Retrofit library yet, it is a high time to study it. But I anyway will make small introduction, and at the same time we will a little consider new opportunities of version 2.0.0 (I advise also to look at presentation on Retrofit 2.0.0).

As example I have selected API for the airports for its maximum simplicity. And we solve the most banal problem — obtaining the list of the nearest airports.

First of all we need to connect all selected libraries and required dependences for Retrofit:
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1'
compile 'com.squareup.okhttp:okhttp:2.0.0'

We will receive the airports in the form of the object list of certain class.
Therefore this class should be created
public class Airport {

    @SerializedName("iata")
    private String mIata;

    @SerializedName("name")
    private String mName;

    @SerializedName("airport_name")
    private String mAirportName;

    public Airport() {
    }
}


We create service for requests:
public interface AirportsService {

    @GET("/places/coords_to_places_ru.json")
    Call<List<Airport>> airports(@Query("coords") String gps);

}

The note about Retrofit 2.0.0
Earlier for execution of synchronous and asynchronous requests we had to write different methods. Now in attempt to create service which contains void method, you receive error. In Retrofit 2.0.0 the Call interface encapsulates requests and allows to execute them synchronously or asynchronously.
Earlier
public interface AirportsService {

    @GET("/places/coords_to_places_ru.json")
    List<Airport> airports(@Query("coords") String gps);

    @GET("/places/coords_to_places_ru.json")
    void airports(@Query("coords") String gps, Callback<List<Airport>> callback);

}


Now
AirportsService service = ApiFactory.getAirportsService();
Call<List<Airport>> call = service.airports("55.749792,37.6324949");

//sync request
call.execute();

//async request
Callback<List<Airport>> callback = new RetrofitCallback<List<Airport>>() {
    @Override
    public void onResponse(Response<List<Airport>> response) {
        super.onResponse(response);
    }
};
call.enqueue(callback);


Now we will create supplementary methods:
public class ApiFactory {

    private static final int CONNECT_TIMEOUT = 15;
    private static final int WRITE_TIMEOUT = 60;
    private static final int TIMEOUT = 60;

    private static final OkHttpClient CLIENT = new OkHttpClient();

    static {
        CLIENT.setConnectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS);
        CLIENT.setWriteTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS);
        CLIENT.setReadTimeout(TIMEOUT, TimeUnit.SECONDS);
    }

    @NonNull
    public static AirportsService getAirportsService() {
        return getRetrofit().create(AirportsService.class);
    }

    @NonNull
    private static Retrofit getRetrofit() {
        return new Retrofit.Builder()
                .baseUrl(BuildConfig.API_ENDPOINT)
                .addConverterFactory(GsonConverterFactory.create())
                .client(CLIENT)
                .build();
    }
}

It's cool! Preparation is complete, and now we can execute request:
public class MainActivity extends AppCompatActivity implements Callback<List<Airport>> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AirportsService service = ApiFactory.getAirportsService();
        Call<List<Airport>> call = service.airports("55.749792,37.6324949");
        call.enqueue(this);
    }

    @Override
    public void onResponse(Response<List<Airport>> response) {
        if (response.isSuccess()) {
            List<Airport> airports = response.body();
            //do something here
        }
    }

    @Override
    public void onFailure(Throwable t) {
    }
}

All it seems very simple. We have with little effort created the necessary classes, and we can already do requests, receive result and process errors, and all this literally in 10 minutes. What is necessary?

However such approach is quite misleading. What will be if in runtime of request the user turns the device or in general will close application? With confidence it is possible to tell only that the necessary result is not guaranteed to you, and we have nearby left from initial problems. And requests and fragments do not add beauty to aktivit to your code in any way. Therefore it is time to return, at last, to the main subject of article — creation of architecture of client-server application.

In this situation we have some options. It is possible to use any library which ensures competent functioning with multithreading. Here Rx framework especially as Retrofit supports him is ideally suited. However to construct architecture with Rx or even it is simple to use functional reactive programming — these are nontrivial tasks. We will go on simpler way: let's use means which are offered to us by Android from box. Namely, loader.

Loadera have appeared in the API 11 version and still remain very powerful tool for side-by-side execution of requests. Of course, in loader it is possible to do in general anything, but usually they are used or for data reading from base, or for execution of network requests. And the most important advantage of loader — through the class LoaderManager they are connected with life cycle of Activity and Fragment. It allows to use them without fear that data will be lost when closing application or the result will return not in that kollbek.

Usually the work model with loader means the following steps:
1) We execute request and we receive result;
2) Somehow we cache result (most often in database);
3) We return result in Activity or Fragment.

Note
Such model is good that Activity or Fragment do not think, data are how exactly obtained. For example, from the server the error can return, but thus the loader will return the cached data.

Let's implement such model. I lower details of how work with database is implemented, if necessary you can look at example at Github (the link at the end of article). Here too the set of variations is possible, and I will consider them in turn, all their advantages and shortcomings, so far, at last, I will not reach model which I consider optimum.

Note
All loader have to work with universal data type that it was possible to use the LoaderCallbacks interface in one aktivita or fragment for different types of the loaded data. The first such type which occurs, Cursor is.

One more note
All models connected with loader have small shortcoming: for each request the separate loader is necessary. And it means that at change of architecture or, for example, transition to other database, we will face big refactoring that is not too good. As much as possible to bypass this problem, I will use base class for all loader and to store all possible general logic in it.

Loader + ContentProvider + asynchronous requests


Preconditions: there are classes for work with the SQLite database through ContentProvider, there is opportunity to save entities in this base.

In the context of this model it is extremely difficult to take out some general logic in base class, therefore in this case it only loader from whom it is convenient to be inherited for execution of asynchronous requests. Its contents does not belong directly to the considered architecture, therefore it in spoiler. However you can also use it in the applications:
BaseLoader
public class BaseLoader extends Loader<Cursor> {

    private Cursor mCursor;

    public BaseLoader(Context context) {
        super(context);
    }

    @Override
    public void deliverResult(Cursor cursor) {
        if (isReset()) {
            if (cursor != null) {
                cursor.close();
            }
            return;
        }
        Cursor oldCursor = mCursor;
        mCursor = cursor;

        if (isStarted()) {
            super.deliverResult(cursor);
        }

        if (oldCursor != null &&oldCursor != cursor &&!oldCursor.isClosed()) {
            oldCursor.close();
        }
    }

    @Override
    protected void onStartLoading() {
        if (mCursor != null) {
            deliverResult(mCursor);
        } else {
            forceLoad();
        }
    }

    @Override
    protected void onReset() {
        if (mCursor != null &&!mCursor.isClosed()) {
            mCursor.close();
        }
        mCursor = null;
    }

}


Then the loader for loading of the airports can look as follows:
public class AirportsLoader extends BaseLoader {

    private final String mGps;

    private final AirportsService mAirportsService;

    public AirportsLoader(Context context, String gps) {
        super(context);
        mGps = gps;
        mAirportsService = ApiFactory.getAirportsService();
    }

    @Override
    protected void onForceLoad() {
        Call<List<Airport>> call = mAirportsService.airports(mGps);
        call.enqueue(new RetrofitCallback<List<Airport>>() {
            @Override
            public void onResponse(Response<List<Airport>> response) {
                if (response.isSuccess()) {
                    AirportsTable.clear(getContext());
                    AirportsTable.save(getContext(), response.body());
                    Cursor cursor = getContext().getContentResolver().query(AirportsTable.URI,
                            null, null, null, null);
                    deliverResult(cursor);
                } else {
                    deliverResult(null);
                }
            }
        });
    }
}

And now we at last can use it in UI classes:
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getLoaderManager().initLoader(R.id.airports_loader, Bundle.EMPTY, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        switch (id) {
            case R.id.airports_loader:
                return new AirportsLoader(this, "55.749792,37.6324949");
            default:
                return null;
        }
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        int id = loader.getId();
        if (id == R.id.airports_loader) {
            if (data != null &&data.moveToFirst()) {
                List<Airport> airports = AirportsTable.listFromCursor(data);
                //do something here
            }
        }
        getLoaderManager().destroyLoader(id);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }
}

Apparently, there is nothing difficult. It is absolutely standard work with loader. In my opinion, loader provide the ideal abstraction layer. We load the necessary data, but without excess knowledge of that, how exactly they are loaded.

This model stable, rather convenient for use, but nevertheless has shortcomings:
1) Each new loader contains the logic for work with result. This shortcoming can be corrected, and partially we will make it in the following model and completely — in the last.
2) The second shortcoming is much more serious: all operations with database are run in the main flow applications, and it can lead to different negative effects, even to application stop at very large number of the saved data. And eventually, we use loader. Let's do everything asynchronously!

Loader + ContentProvider + synchronous requests


It is asked, why we executed request asynchronously with the help Retrofit-and when loader and so allow us to work in background? Let's correct it.

This model simplified, but the main difference is that asynchrony of request is reached at the expense of loader, and work already happens to base not in the main flow. Successors of base class have to return only to us object like Cursor. Now the base class can look as follows:
public abstract class BaseLoader extends AsyncTaskLoader<Cursor> {

    public BaseLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();
        forceLoad();
    }

    @Override
    public Cursor loadInBackground() {
        try {
            return apiCall();
        } catch (IOException e) {
            return null;
        }
    }

    protected abstract Cursor apiCall() throws IOException;
}

And then implementation of abstract method can look as follows:
@Override
protected Cursor apiCall() throws IOException {
    AirportsService service = ApiFactory.getAirportsService();
    Call<List<Airport>> call = service.airports(mGps);
    List<Airport> airports = call.execute().body();
    AirportsTable.save(getContext(), airports);
    return getContext().getContentResolver().query(AirportsTable.URI, null, null, null, null);
}

Work with loader in UI at us has not changed in any way.

Upon, this model is modification previous, it partially eliminates its defects. But in my opinion, it is all the same not enough of it. Here it is possible to select shortcomings again:
1) At each loader there is individual logic of saving of data.
2) Work only with the SQLite database is possible.

And at last, let's eliminate completely these defects and we will receive universal and almost ideal model!

Loader + any data storage + synchronous requests


Before consideration of specific models I said that for loader we have to use uniform data type. Except Cursor occurs nothing. So let's create such type! What has to be in it? Naturally, it should not be generic-type (differently we will not be able to use kollbeka of loader for different data types in one aktivita / fragment), but at the same time it has to be the container for object of any type. And here I see the only weak place in this model — we have to use the Object type and execute conversion unchecked. But nevertheless, it is not so essential minus. The final version of this type looks as follows:
public class Response {

    @Nullable private Object mAnswer;

    private RequestResult mRequestResult;

    public Response() {
        mRequestResult = RequestResult.ERROR;
    }

    @NonNull
    public RequestResult getRequestResult() {
        return mRequestResult;
    }

    public Response setRequestResult(RequestResult requestResult) {
        mRequestResult = requestResult;
        return this;
    }

    @Nullable
    public <T> T getTypedAnswer() {
        if (mAnswer == null) {
            return null;
        }
        //noinspection unchecked
        return (T) mAnswer;
    }

    public Response setAnswer(@Nullable Object answer) {
        mAnswer = answer;
        return this;
    }

    public void save(Context context) {
    }
}

This type can store result of execution of request. If we want to do something for specific request, it is necessary to be inherited from this class and to redefine / add the necessary methods. For example, so:
public class AirportsResponse extends Response {

    @Override
    public void save(Context context) {
        List<Airport> airports = getTypedAnswer();
        if (airports != null) {
            AirportsTable.save(context, airports);
        }
    }
}

It's cool! Now we will write base class for loader:
public abstract class BaseLoader extends AsyncTaskLoader<Response> {

    public BaseLoader(Context context) {
        super(context);
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();
        forceLoad();
    }

    @Override
    public Response loadInBackground() {
        try {
            Response response = apiCall();
            if (response.getRequestResult() == RequestResult.SUCCESS) {
                response.save(getContext());
                onSuccess();
            } else {
                onError();
            }
            return response;
        } catch (IOException e) {
            onError();
            return new Response();
        }
    }

    protected void onSuccess() {
    }

    protected void onError() {
    }

    protected abstract Response apiCall() throws IOException;
}

This class of loader is ultimate goal of this article and, in my opinion, the excellent, operable and expanded model. Want to pass with SQLite, for example, to Realm? Not problem. Let's consider it as the following example. Classes of loader will not change, only the model which you anyway would edit will change. It was not succeeded to execute request? Not problem, finish the apiCall method in the successor. Want to clear database at error? Redefine onError and work — this method is executed in background thread.

And any specific loader it is possible to provide as follows (besides, I will show only implementation of abstract method):
@Override
protected Response apiCall() throws IOException {
    AirportsService service = ApiFactory.getAirportsService();
    Call<List<Airport>> call = service.airports(mGps);
    List<Airport> airports = call.execute().body();
    return new AirportsResponse()
            .setRequestResult(RequestResult.SUCCESS)
            .setAnswer(airports);
}

Note
At unsuccessfully executed request Exception will be thrown out, and we will get to catch-branch of basic loader.

As a result we have received the following results:
1) Each loader depends only on the request (from parameters and result), but thus he does not know that it does with data retrieveds. That is it will change only at change of parameters of specific request.
2) The basic loader manages all logic of execution of requests and works with results.
3) Moreover, classes of model too have no concept how work with database and other is arranged. All this is taken out in separate classes / methods. I did not specify it obviously anywhere, but it is possible to look at it in example at Github — the link at the end of article.

Instead of the conclusion


Slightly above I promised to set one more example — transition with SQLite to Realm — and to be convinced that we really will not mention loader. Let's make it. Actually, code here absolutely slightly, after all work with base at us is now performed only in one method (I do not consider the changes connected with specifics of Realm, and they are, in particular, rules of naming of fields and work with Gson; it is possible to look at them at Github).

Let's connect Realm:
compile 'io.realm:realm-android:0.82.1'

Also we will change the save method in AirportsResponse:
public class AirportsResponse extends Response {

    @Override
    public void save(Context context) {
        List<Airport> airports = getTypedAnswer();
        if (airports != null) {
            AirportsHelper.save(Realm.getInstance(context), airports);
        }
    }
}

AirportsHelper
public class AirportsHelper {

    public static void save(@NonNull Realm realm, List<Airport> airports) {
        realm.beginTransaction();
        realm.clear(Airport.class);
        realm.copyToRealm(airports);
        realm.commitTransaction();
    }

    @NonNull
    public static List<Airport> getAirports(@NonNull Realm realm) {
        return realm.allObjects(Airport.class);
    }
}


That's all! We elementary, without mentioning classes which contain other logic, have changed way of data storage.

After all conclusion


I want to select one rather important point: we have not considered the questions connected with use of the cached data that is at Internet otstustviye. However strategy of use of the cached data in each application is individual, and I do not consider to impose any certain approach to the correct. And so article has stretched.

As a result we have considered the main questions of the organization of architecture of client-server applications, and I hope that this article has helped you to learn something new and that you will use any of the listed models in the projects. Besides, if you have ideas as it is possible to organize such architecture — write, I will be glad to discuss.

Thank you that have read up up to the end. Successful development!

P.S. The promised link to code to GitHub.

This article is a translation of the original post at habrahabr.ru/post/265405/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus