preface

Writing in the front

**Flyabbit is a quick app built by Retrofit+Rxjava2+ Dagger2 +Mvp+Material Design. UI is one of them. More importantly, I will start from project zero through a series of articles Technology selection -> package -> component-based design -> Simple use of local Maven (Nexus)-> Automatic packaging (Jenkins continuous integration)-> unit testing -> online Bug quick fix (hot fix), for your reference part of the reference, shortcomings also hope to point out **

Lot of the project: github.com/chengzichen…

Purpose:

Why write [Starting from scratchIn this series, because of the rapid technological update, some of the previous coding and design are not applicable to the current situation of the company, so I built a more suitable framework for the current company from scratch. But the more important point is to get familiar with a new framework and simple principles quickly in a team. As the saying goes, it is better to teach people and fish than to teach people and fish. I am here to record this process and hope to give you some help

  • [x] the road to AndroidApp development from scratch – network request encapsulation
  • [] the road of AndroidApp development from scratch – the road of MVP design
  • [] the road of AndroidApp development from scratch – the road of Base extraction
  • [] the road to AndroidApp development from scratch – componentization and continuous integration
  • [] the road to AndroidApp development from scratch – unit testing

Network request

Why encapsulate your own network request framework

Although there are many mature network request frameworks in the world

For example, HttpClent was removed by Google in Android 6.0, as well as HttpUrlConnection, the oldest and oldest, we abandoned Android-Async-HTTP, and XUtil, Google 2013 I/O from Volley to now Retrofit +okHttp,okGo,NoHttp, etc. (thanks to the open source contributors who have brought fresh blood into the Android ecosystem), but your own project can be a simple packaged HttpClient, HttpUrlConnection, okhttp also can be out of the box android – async – HTTP, XUtil, Volley, Retrofit, okGo, NoHttp, etc., I want to say is suitable is the best of the project, here give you some reference

Project selection Request Framework: Retrofit

Reason:

  1. The fastest processing, using Gson,Okhttp by default
  2. RESTful design style commonly used by project backend staff
  3. You can generate callAdapters from factories, Converter, you can use different request adapters, such as RxJava, Java8, Guava.
  4. You can use different deserialization tools (Converter) such as JSON, Protobuff, XML, Moshi, etc

    Note:

    RESTful: refers to a software architecture style. A design style rather than a standard provides a combination of design principles and constraintsCopy the code

To use the Retrofi framework, it is best to use it in conjunction with RxJava, otherwise it has no advantage over regular networking frameworks

Note:

RxJava: is a library that implements asynchronous operationsCopy the code

So a combination of Retrofit+Rxjava +Okhttp+Gson is used to encapsulate everything from request to async to parse

Encapsulation results

  1. Retrofit+Rxjava+ OKHTTP basic usage method
  2. Unified processing of request data formats
  3. Cookie management,Token refresh a variety of solutions
  4. Multi-baseurl processing
  5. Customize the request header and define the request body
  6. Unified processing of returned data
  7. Retry encapsulation after a failure
  8. RxLifecycle manages the life cycle to prevent leaks
  9. Converter processing, multiple parsers (encryption, decryption)
  10. File upload
  11. Chrysanthemum control on request

Packaging details

  • Import dependence

    Add a reference to app/build.gradle


  `/*rx-android-java*/
    compile 'io.reactivex.rxjava2:rxandroid:+'
    compile 'io.reactivex.rxjava2:rxjava:+'
    compile 'com.tbruyelle.rxpermissions2:rxpermissions:+'
    compile 'com.trello:rxlifecycle-components:+'
    /*rotrofit*/
    compile 'com.squareup.retrofit2:retrofit:+'
    compile 'com.squareup.retrofit2:converter-gson:+'
    compile 'com.squareup.retrofit2:adapter-rxjava2+'
    compile 'com.google.code.gson:gson:+'




Copy the code

For future extension and replacement, the network framework encapsulates the network request as aHelper class and implements the IDataHelper interface

The contents include methods to be exposed.

public interface IDataHelper { void init(Context context); <S> S getApi(Class<S> serviceClass); <S> S createApi(Class<S> serviceClass); <S> S getApi(Class<S> serviceClass, OkHttpClient client); <S> S createApi(Class<S> serviceClass, OkHttpClient client); OkHttpClient getClient(); . }Copy the code

If Dagger2 dependency injection is used in your project, then you can change the IDataHelper implementation class and inject it into each module without modifying other code. The diagram below:

Or use the simple factory design pattern

public class DataHelperProvider { private static IDataHelper dataHelper; public static IDataHelper getHttpHelper(Context context) { if (dataHelper == null) { synchronized (DataHelperProvider.class) { if (dataHelper == null) { dataHelper = new HttpHelper(context); } } } return dataHelper; }... }Copy the code

You only need to change the implementation of IDataHelper

HttpHelper is divided into:

  • Request related:

  • Okhttp basic Settings:

  • General Settings: Connection timeout,cacheInterceptor cache handling

  • Personalization: ClearableCookieJar management, optimized anti-duplication, HTTPS, custom request headers, define request body (encryption, decryption,)

  • Related development support

Okhttp basic Settings

Public OkHttpClient getOkHttpClient() {ClearableCookieJar cookieJar =// SetCookieCache(), new SharedPrefsCookiePersistor(context)); File cacheFile = new File(context.getExternalCacheDir(), "KairuCache"); Cache Cache = new Cache(cacheFile, 1024 * 1024 * 40); CacheInterceptor CacheInterceptor = new CacheInterceptor(context); TokenInterceptor = new TokenInterceptor(); TokenInterceptor = new TokenInterceptor(); Okhttpclient.builder Builder = new okHttpClient.builder ().cache(cache).addInterceptor(cacheInterceptor) / / for API cache Settings. AddNetworkInterceptor (cacheInterceptor) / / for API cache Settings. RetryOnConnectionFailure (true) / / whether to request connection failure .connecttimeout (15, timeunit.seconds)// connection timeout. WriteTimeout (3, timeunit.seconds)// writeTimeout timeout. ReadTimeout (3, Timeunit.seconds)// read timetime.addInterceptor (tokenInterceptor)// Token request header Settings. CookieJar (cookieJar); If (apputil.isDebug ()) {// Enable HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); builder.addInterceptor(loggingInterceptor); } // okHttpClient = builder.build(); return okHttpClient; }Copy the code

== Note == : – retryOnConnectionFailure(true) Method the failure reconnection mechanism here is: as long as after a failed network request will have been heavy even cannot meet even when The Times and long product demand, found in practical project and not to use and flexible, so here I still gave up this method, (subsequent perhaps at this point) for my own use Rxjava redesigned a reconnection times and provided a long when reconnection Methods. (Here again depends on the actual requirements)

  • If the background uses standard Cookie security authentication and authentication tokens are placed in cookies, try using ClearableCookieJar and PersistentCookieJar
Com. Making. Franmontiel: PersistentCookieJar: v1.0.0Copy the code

But didn’t hear the but? In fact, there are various ways of server security authentication, and there are also various ways of Token submission and storage. There are no tokens, some are directly written to death, some are put in the Head, and some are when the request parameters. In these cases, after the Token expires, The EU needs to refresh and obtain the Token according to the actual situation Most of the techniques will also be introduced

Retrofit related Settings:

public Retrofit getRetrofit(String host) { if (gson == null) gson = new GsonBuilder().create(); If (okHttpClient == null) okHttpClient = getOkHttpClient(); Retrofit = new retrofit.builder ().baseurl (host)// baseUrl pathway.client (okHttpClient)// Add the okHttpClient client .addConverterFactory(new StringConverterFactory())// Add String format chemical AddConverterFactory (GsonConverterFactory. Create (gson)) / / add gson format chemical plant .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); return retrofit; }Copy the code

== Description == : Two data parsing methods are provided here: parsing as String type and using Gson to parse into the corresponding Bean in order to correspond to the actual needs of development. Here, we can also extend Adapter further and not go into depth here

  • Multi-baseurl handling: There are many scenarios in development where different server addresses are used
    public <S> S createApi(Class<S> serviceClass, OkHttpClient client) {
        String baseURL = "";
        try {
            Field field1 = serviceClass.getField("baseURL");
            baseURL = (String) field1.get(serviceClass);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.getMessage();
            e.printStackTrace();
        }
        if(retrofit!=null&&retrofit.baseUrl().host()==baseURL){
            return  retrofit.create(serviceClass);
        }else{

        return getRetrofit(baseURL).create(serviceClass);
        }
    }Copy the code

== Description == : This uses the reflection principle to get the BaseUrl written in each ApiService, if not, the same BaseUrl will be used for the request

  • Unified processing request data format:

    If the format of the data returned by the backend server is very formal, you can try to extract some common parts, such as: Code (where Code refers to business Code) and ErrorMessage(ErrorMessage), and encapsulate the rest into a base class.

    • You don’t have to write duplicate code
    • Business code can be processed uniformly, such as :200 success, directly processed in the base class once, and such as Token expiration mentioned later can be completed using this operation.
public class ApiResponse<T> { public T data; public String code; public String message; . get() set() }Copy the code

Here’s a simple example:

{code:200 ,
    message: success,
    loginInfo :{
        ...
    }}Copy the code

I’m just going to write the ApiResponse, the actual data LoginInfoBean is going to be retrieved and used in T data, and then the Code is going to be filtered one level up. You can pull the generic out of a base class if you need to, to prevent background data from mutating

Rxjava encapsulation

  • Thread scheduling: encapsulation is used here to Rxjava2.0 briefly, before involve multithreading switch will be thinking of Handler, AsyncTask, ThreadPool and so on different occasions, of course some good network library also provides the relevant design (Okgo, etc.)
/** public static <T extends ApiResponse> rxSchedulerHelper<T, T> rxSchedulerHelper(int count,long delay) {//compose return new FlowableTransformer<T, T>() { @Override public Flowable<T> apply(Flowable<T> upstream) { return upstream.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); }}; }Copy the code
  • BaseSubscriber (Subscriber wrapper)

BaseSubscriber inherits ResourceSubscriber from Rxjava2.0, which supports back pressure,

BaseSubscriber handles things like business status code unification, exception handling,Checkout, and control chrysanthemums.

public class BaseSubscriber<T extends ApiResponse> extends ResourceSubscriber<T> implements ProgressCancelListener { private static final String TAG = "BaseSubscriber"; private SubscriberListener mSubscriberOnNextListener; private ProgressDialogHandler mHandler; Context aContext; /** * a dialog will automatically pop up and vanish. If you want to do this, you can do it yourself, or you can control it by {@link SubscriberListener#isShowLoading() mSubscriberOnNextListener * @param aContext */ public BaseSubscriber(SubscriberListener mSubscriberOnNextListener, Context aContext) { this.mSubscriberOnNextListener = mSubscriberOnNextListener; mHandler = new ProgressDialogHandler(aContext, this, false); this.aContext = aContext; } / use the constructor does not LoadingDialog * * * * * @ param mSubscriberOnNextListener * / public BaseSubscriber (SubscriberListener mSubscriberOnNextListener) { this.mSubscriberOnNextListener = mSubscriberOnNextListener; } @Override protected void onStart() { super.onStart(); if (! NetworkUtil. IsNetAvailable (AppContext. The get ())) {ToastUtil. ShortShow (AppContext. The get (), "network error. Please check your network"); if (isDisposed()) this.dispose(); return; } if (mSubscriberOnNextListener ! = null && mSubscriberOnNextListener.isShowLoading()) showProgressDialog(); onBegin(); } /** * public void onBegin() {log. I (TAG, "onBegin"); if (mSubscriberOnNextListener ! = null) { mSubscriberOnNextListener.onBegin(); } } private void showProgressDialog() { if (mHandler ! = null) { mHandler.obtainMessage(ProgressDialogHandler.SHOW_PROGRESS_DIALOG).sendToTarget(); } } private void dismissProgressDialog() { if (mHandler ! = null) { mHandler.obtainMessage(ProgressDialogHandler.DISMISS_PROGRESS_DIALOG).sendToTarget(); mHandler = null; }} @override public void onError(Throwable e) {log. I (TAG); "onError:" + e.toString()); if (mSubscriberOnNextListener ! = null) { mSubscriberOnNextListener.onError(e); } onComplete(); } @override public void onComplete() {log. I (TAG, "onCompleted"); if (mSubscriberOnNextListener ! = null && mSubscriberOnNextListener.isShowLoading()) dismissProgressDialog(); if (mSubscriberOnNextListener ! = null) { mSubscriberOnNextListener.onCompleted(); } if (! this.isDisposed()) { this.dispose(); }} /** * Return the result of onNext method to the Activity or Fragment itself, you can encapsulate the generic type of @param Response when creating Subscriber */ @override public according to the actual situation void onNext(T response) { Log.i(TAG, "onNext"); if (mSubscriberOnNextListener ! = null) {if (Constants) SUCCEED) equals (response. Code)) {/ / success mSubscriberOnNextListener. OnSuccess (response. Data); } else if (Constants.STATUS_RE_LOGIN.equals(response.code) || Constants. Status_no_login. equals(Response.code))// Not logged in or logged out {// Determine whether you need to log in again mSubscriberOnNextListener.checkReLogin(response.code, response.message); } else {/ / business or server anomaly mSubscriberOnNextListener. OnFail (. The response. The code, the response message); Disposed() {Override public void onCancelProgress() {if (isDisposed()) this.dispose(); if (mHandler ! = null) mHandler = null; }}Copy the code
  • Unified exception resolution: Service exception processing, Service exception resolution Processing (Rxjava)

Although some of the most common network requests are pretty much the same here, there are some minor problems when using Rxjava. Here are some tips

public abstract class KrSubscriberListener<T> extends SubscriberListener<T> { public void onFail(String errorCode, String errorMsg) {// todo} @override public void onError(Throwable e) {// The exception can be classified into network exceptions, service exceptions, etc. NetError error = null; if (e ! = null) { if (! (e instanceof NetError)) { if (e instanceof UnknownHostException) { error = new NetError(e, NetError.NoConnectError); } else if (e instanceof JSONException || e instanceof JsonParseException || e instanceof JsonSyntaxException) { error = new NetError(e, NetError.ParseError); } else if (e instanceof SocketException || e instanceof SocketTimeoutException) { error = new NetError(e, NetError.SocketError); } else { error = new NetError(e, NetError.OtherError); } } else { error = (NetError) e; } onFail(error.getType(), error.getMessage()); } } @Override public void checkReLogin(String errorCode, String errorMsg) { //todo } }Copy the code
  • Token refresh problem. Procedure

There are many ways to authenticate and refresh tokens

1. The first option

The Authenticator interface is provided by okHTTP, but if you look at the source code of OKHTTP, you will find that the Authenticator interface is used only when the HTTP status code is 401. If the server design is standard, you can try the following methods.

Implement the Authenticator interface

== Note that the 401 refers to the HTTP status code

public class TokenAuthenticator implements Authenticator { @Override public Request authenticate(Proxy proxy, Response Response) throws IOException {// Fetch the local refreshToken. If the local refreshToken expires, you need to log in again. String refreshToken = "XXXXXXXXXXXXXXXXXXX"; HttpHelper = new HttpHelper(application); // Get the new token through a specific interface. ApiService service = httpHelper.getApi(ApiService.class); Call<String> call = service.refreshToken(refreshToken); String newToken = call.execute().body(); return response.request().newBuilder() .header("token", newToken) .build(); } @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException { return null; }}Copy the code

Then add it to OkHttpClient so that OkHttpClient can refresh the Token automatically. After refreshing the Token, OkHTTP will initiate the request again

OkHttpClient client = new OkHttpClient();
client.setAuthenticator(new TokenAuthenticator());Copy the code

2. The second option

However, if the server cannot return the 401**HTTP status code **, then the above solution does not solve the problem

Sometimes, the Token expiration is classified as a service exception. Therefore, the server returns the HTTP status Code of 200 success but the service Code of 401, for example:Copy the code
{"data":{// return nothing}, "Message ":"success" "code":401Copy the code

With okHTTP interceptors, okHTTP has provided interceptor functionality since 2.2.0

public class TokenInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(request); Request.Builder requestBuilder = original.newBuilder(); String access_token = (String) SPHelper.get(AppContext.get(),"access_token",""); if(! TextUtils.isEmpty(access_token)){ requestBuilder.addHeader("Authentication", access_token); } if (isTokenExpired(response)) {if (isTokenExpired(response)) {if (isTokenExpired(response)) {if (isTokenExpired(response)) { requestBuilder.header("Authentication", access_token); } Request request = requestBuilder.build(); return chain.proceed(request); } /** * Response, Check whether Token is invalid * * @param Response * @return */ private Boolean isTokenExpired(response Response) {if (response. Code () == 401) { return true; } return false; } /** * Obtain the latest Token through a synchronous request ** @return */ private String getNewToken() throws IOException {// Obtain a new Token through a specific interface. Here we use the synchronous retroFIT request String refreshToken = "XXXXXXXXXXXXXXXXXXX"; HttpHelper httpHelper= new HttpHelper(application); ApiService service = httpHelper.getApi(ApiService.class); Call<String> call = service.refreshToken(refreshToken); String newToken = call.execute().body(); Sphelper.put (appContext.get (),"access_token",newToken)// Save local return newToken; }}Copy the code

3. Of course, there are other solutions, as long as you can request the server to refresh the Token again after the unified request comes back. The key is to seize this opportunity, such as modifying the GsonConverterFactory during Gson parsing, determining the business Code and refreshing the Token, or using Rx Java flatMap implementations are all the same original. The difference between these methods is that the timing point can be: okHTTP interceptor, or Gson parsing, or Gson parsing with Rxjava flatMap is the same

  • Corresponding encrypted data

public class StringConverterFactory implements Converter

Public static <T extends ApiResponse> FlowableTransformer<T, T> rxSchedulerHelper() {//compose compose simple thread return rxSchedulerHelper(3,5); } /** ** Unified thread processing and failed reconnection * @param count Number of failed reconnections * @param delay Delay time * @param <T> Returns data data actual data * @return Returns data data actual data */ public static <T extends ApiResponse> FlowableTransformer<T, T> rxSchedulerHelper(int count,long delay) {//compose return new FlowableTransformer<T, T>() { @Override public Flowable<T> apply(Flowable<T> upstream) { return upstream.subscribeOn(Schedulers.io()) .flatMap(new Function<T, Flowable<T>>() {@override public Flowable<T> apply(T T) throws Exception {//TODO something TODO with the result return Flowable.  } }) .retryWhen(new RetryWhenHandler(count, delay)) .observeOn(AndroidSchedulers.mainThread()); }}; } public class RetryWhenHandler implements Function<Flowable<? extends Throwable>, Flowable<? >> { private int mCount = 3; private long mDelay = 3; //s private int counter = 0; public RetryWhenHandler() { } public RetryWhenHandler(int count) { this.mCount = count; } public RetryWhenHandler(int count, long delay) { this(count); this.mCount = count; this.mDelay = delay; } @Override public Flowable<? > apply(Flowable<? extends Throwable> flowable) throws Exception { return flowable .flatMap(new Function<Throwable, Flowable<? >>() { @Override public Flowable<? > apply(Throwable throwable) throws Exception { if (counter < mCount && (throwable instanceof UnknownHostException || throwable instanceof SocketException || throwable instanceof HttpException )) { counter++; return Flowable.timer(mDelay, TimeUnit.SECONDS); } else if ((counter < mCount && throwable instanceof NullPointerException && throwable.getMessage() ! = null) { counter++; return Flowable.timer(0, TimeUnit.SECONDS); } return Flowable.error(throwable); }}); }}Copy the code

== description == : Here is the use of retryWhen() method in Rxjava to achieve, when it is found that the exception thrown is a network exception, it will request again, so as to control the reconnection times and duration (the default is reconnection three times, and each reconnection event is 3s, here can be formulated according to the specific situation), if you still have no understanding of Rxja Va children’s boots get up to speed.

  • Multi-file upload

How to upload multiple files if the server provides a file system

There are two ways – use List

/** * Encapsulates the file path array as {@link List< multipartBody. Part>} * @param key corresponds to the value of name in the request body. In the current interface provided by the server, all image files use the same name value, @param imageType File type */ public static List< multiPartBody. Part> files2Parts(String key, String[] filePaths, MediaType imageType) { List<MultipartBody.Part> parts = new ArrayList<>(filePaths.length); for (int i = 0; i <filePaths.length ; i++) { File file = new File(filePaths[i]); Create (imageType, File); // Create (imageType, File); MultipartBody.Part Part = MultiPartBody.part. CreateFormData (key+ I, file.getName(), requestBody); // Add to the collection parts.add(part); } return parts; } /** * encapsulates a File as a RequestBody and then as a Part. <br> * The difference is to use multiPartBody. Builder to build MultipartBody * @param Key * @param filePaths * @param imageType */ public  static MultipartBody filesToMultipartBody(String key, String[] filePaths, MediaType imageType) { MultipartBody.Builder builder = new MultipartBody.Builder(); for (int i = 0; i <filePaths.length ; i++) { File file = new File(filePaths[i]); RequestBody requestBody = RequestBody.create(imageType, file); builder.addFormDataPart(key+i, file.getName(), requestBody); } builder.setType(MultipartBody.FORM); return builder.build(); }Copy the code

Then use Retrofit to define the Api

public interface FileUploadApi { String baseURL= Constants.MOCK_FILE_UPLOAD; /** * Note 1: {@code @post} must be annotated as POST request <br> * Note: To use the {@code@multipart} annotation method, you must annotate its parameters with {@code@part}/<br> * {@code@partmap} <br> * This interface divides text data and file data into two parameters. <br> * {@link multipartbody. Part} can also be merged into a {@code @part} parameter * @param params used to encapsulate text data * @param */ @multipart @post () Flowable<ApiResponse<UploadBackBean>> requestUploadWork(@Url String url,@PartMap Map<String, RequestBody> params, @Part() List<MultipartBody.Part> parts); /** * Note 1: {@code @post} must be annotated as POST request <br> * Note 2: With the {@code@body} annotation parameter, {@code @multipart} <br> * Merge all {@link multipartbody. Part} into one {@link MultipartBody} */ @post () Flowable<ApiResponse<UploadBackBean>> requestUploadWork(@Url String url,@Body MultipartBody body); }Copy the code

Final use;

public void requestUploadWork(String[] files) { List<MultipartBody.Part> parts = UploadUtil.files2Parts("file", files, MediaType.parse("multipart/form-data")); return mHttpHelper.getApi(FileUploadApi.class).requestUploadWork(Constants.BASE_URL+"file/upload/img",parts).compose(RxUtil.<Ap iResponse<String>>rxSchedulerHelper(0, 5)) .subscribe(new BaseSubscriber<ApiResponse<String>>(new KrSubscriberListener<String>() { @Override public void onSuccess(String response) { //todo } @Override public void onFail(String errorCode, String errorMsg) { super.onFail(errorCode, errorMsg); //todo } })); } public void requestUploadWork(String[] files) { MultipartBody parts = UploadUtil.filesToMultipartBody("file", files, MediaType.parse("multipart/form-data")); return mHttpHelper.getApi(FileUploadApi.class).requestUploadWork(Constants.BASE_URL+"file/upload/img",parts).requestUploadWork( Constants.BASE_URL+"file/upload/img",parts).compose(RxUtil.<ApiResponse<String>>rxSchedulerHelper(0, 5)) .subscribe(new BaseSubscriber<ApiResponse<String>>(new KrSubscriberListener<String>() { @Override public void onSuccess(String response) { //todo } @Override public void onFail(String errorCode, String errorMsg) { super.onFail(errorCode, errorMsg); //todo } }));; }Copy the code

This enables multiple file uploads

The last

Some of the previous general Settings are also extended

Flyabbit github.com/chengzichen…

This project will be updated as this article is updated

About me

Finally thanks: code home, as well as some big guy’s help

Reference: github.com/square/okht…