Implement Retrofit2+RxJava2 network request framework

created at 12-29-2021 views: 9

Supplement on January 18, 2021

Recently I encountered a problem that devices below 5.0 cannot be requested, but I have used Retrofit+Kotlin coroutine in the project, but the main reason is that Okhttp4 does not support devices below 5.0, here we need to do it Compatible, but my idea is to do compatibility through Gradle, and I haven't figured it out yet. You can look at this blog.

August 19, 2021

This blog has been written for a long time, and people are still liking it. I am quite happy. I personally feel that the code of ResponseTransformer may be a bit obscure. Some readers have also asked me about this change. If you refer to my code I think that piece of code can be optimized. After all, the code is written concisely and maintainable. The ResposeTransformer actually uses a transformation of Rxjava. The main reason is that I haven't used Rxjava for a long time. Suddenly I saw this piece of code and I was a bit confused, but the code can run through, and I also explained this transformation. I think people who understand Rxjava should be able to understand it. Thank you all for your support.

Because Retrofit is convenient to use and supports RxJava, so Retrofit has become a very popular web framework. Learning to encapsulate and use the Retrofit network request framework to practice hands-on practice is a very good example of improving one's own architecture level. And when the first component is successfully encapsulated, it will become handy if you encounter the task of encapsulating the component again.

1. The main logic of encapsulation

workflow

Encapsulate the network framework step by step according to this logic diagram.

1.1 Import dependencies

compile "io.reactivex.rxjava2:rxjava:2.1.0" // necessary rxjava2 dependency
     compile "io.reactivex.rxjava2:rxandroid:2.0.1" // Necessary to rely on rxandrroid, which is needed when cutting threads
     compile'com.squareup.retrofit2:retrofit:2.3.0' // necessary retrofit dependency
     compile'com.squareup.retrofit2:adapter-rxjava2:2.3.0' // Necessary dependency, must be used in combination with Rxjava, mentioned below
     compile'com.squareup.retrofit2:converter-gson:2.3.0' // Necessary dependency, used for parsing json characters
     compile'com.squareup.okhttp3:logging-interceptor:3.8.1' //non-essential dependency, log dependency, need to be added if you need to print OkHttpLog

1.2 Create a new Manger class to manage the required API

Here is a cookie-cutter approach, if the framework used globally is encapsulated, and it needs to have the same long life cycle as the entire software. Then there are two characteristics:

  • Initialize in Application.
  • Use singleton mode.
  • Initialize all the required parameters in a "management class".
/**
 * Created by Zaifeng on 2018/2/28.
 * API initialization class
 */
public class NetWorkManager {

    private static NetWorkManager mInstance;
    private static Retrofit retrofit;
    private static volatile Request request = null;

    public static NetWorkManager getInstance() {
        if (mInstance == null) {
            synchronized (NetWorkManager.class) {
                if (mInstance == null) {
                    mInstance = new NetWorkManager();
                }
            }
        }
        return mInstance;
    }

    /**
     * Initialize the necessary objects and parameters
     */
    public void init() {
        // Initialize okhttp
        OkHttpClient client = new OkHttpClient.Builder()
                .build();

        // Initialize Retrofit
        retrofit = new Retrofit.Builder()
                .client(client)
                .baseUrl(Request.HOST)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    public static Request getRequest() {
        if (request == null) {
            synchronized (Request.class) {
                request = retrofit.create(Request.class);
            }
        }
        return request;
    }
}

Here, a new NetWorkManager is created. OkHttp and Retrofit are initialized in the init method. The initialization of both is in the constructor mode.

Initialize OKHttp extension

The above code just adds the necessary attributes. You can also add more extensions to OkHttp and Retrofit. For example, if you need to add Log to OkHttp, you can write as follows.

HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(logging)
                    .build();

There are many uses of addInterceptor(), such as: encapsulating some public parameters and so on.

Two necessary configurations when initializing Retrofit:

  • The first configuration .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) This is used to determine whether your return value is Observable or Call.
// Use call
Call<String> login();

// Use Observable
Observable<String> login();

If it returns as Call, then this configuration can be omitted. If you use Observable, you must add this configuration. Otherwise, an error will be reported when requesting!

  • The second configuration: .addConverterFactory(GsonConverterFactory.create()) This configuration is to convert the json string returned by the server into an object. This can customize the Converter to deal with the different data returned by the server. How to customize it is not explained here (because the content is more).

Initialize Request

The Request is also initialized here. If you have a retrofit foundation and see the method retrofit.create, you should know that Request is the API wrapper class of the server that defines the request. It declares the required interface by means of annotations, which will be discussed here and below.

1.3 Agreement Response

When parsing the data returned by the server, it needs to return the Json format with the server, which is often encountered in development. Generally, the server will not return the Json format to the front-end casually, otherwise the front-end parsing will be very troublesome. (So the data structure here requires the front end and the server to agree on a fixed format) The fixed json format here is:

{ret:0,data:"",msg:""}

So the fixed `Response defined here is:

public class Response<T> {

     private int ret; // returned code
     private T data; // specific data results
     private String msg; // message can be used to return the description of the interface

     public int getCode() {
         return ret;
     }

     public void setCode(int code) {
         this.ret = code;
     }

     public T getData() {
         return data;
     }

     public void setData(T data) {
         this.data = data;
     }

     public String getMsg() {
         return msg;
     }

     public void setMsg(String msg) {
         this.msg = msg;
     }
}

1.4 Define Request

As mentioned in the previous NetWorkManager, this is the API interface that defines the request server.

public interface Request {

     // Fill in the server address that needs to be accessed
     public static String HOST = "https://www.xxx.com/app_v5/";

     @POST("?service=sser.getList")
     Observable<Response<List<javaBean>>> getList(@Query("id") String id);
}

A Post request is annotated with @Post here.

1.5 Define ResponseTransformer to handle data and exceptions

This is also a self-defined class, so it is possible not to use this custom class. However, for the convenience of use after packaging, packaging is still recommended. ResponseTransformer is actually an encapsulation of "transformation" in Rxjava. The situation when ResponseTransformer is not used.

model.getCarList("xxxxx")
                 .compose(schedulerProvider.applySchedulers())
                 .subscribe(new Consumer<Response<List<JavaBean>>>() {
                     @Override
                     public void accept(Response<List<JavaBean>> listResponse) throws Exception {
                         if(listResponse.getCode() == 200){
                             List<JavaBean> javaBeans = listResponse.getData();
                         }else{
                             // exception handling
                         }
                     }
                 }, new Consumer<Throwable>() {
                     @Override
                     public void accept(Throwable throwable) throws Exception {
                             // exception handling
                     }
                 });

When ResponseTransformer is used:

model.getCarList("xxxxx")
                 .compose(ResponseTransformer.handleResult())
                 .compose(schedulerProvider.applySchedulers())
                 .subscribe(new Consumer<List<JavaBean>>() {
                                @Override
                                public void accept(List<JavaBean> carBeans) throws Exception {
                                    // Process data directly to List<JavaBean> carBeans
                                }
                            }, new Consumer<Throwable>() {
                                @Override
                                public void accept(Throwable throwable) throws Exception {
                                    // handle exception
                                }
                            }

                 );

Without using ResponseTransformer, you need to determine the code of Response first, and then extract the data from the Response. The more tedious thing is to judge the code of code==200, and you need to write it every time you request an interface.

Use ResponseTransformer to further process the parsed Response data (encapsulate the code that judges code==200), directly extract the data processing that needs to be used, and process the Exception uniformly! This is the magical effect of "transformation" in RxJava, and it will be used in conjunction with compose to reduce duplication of code. Specific ResponseTransformer code.

public class ResponseTransformer {

    public static <T> ObservableTransformer<Response<T>, T> handleResult() {
        return upstream -> upstream
                .onErrorResumeNext(new ErrorResumeFunction<>())
                .flatMap(new ResponseFunction<>());
    }


    /**
     * Non-server exceptions, such as no local network request, Json data parsing error, etc.
     *
     * @param <T>
     */
    private static class ErrorResumeFunction<T> implements Function<Throwable, ObservableSource<? extends Response<T>>> {

        @Override
        public ObservableSource<? extends Response<T>> apply(Throwable throwable) throws Exception {
            return Observable.error(CustomException.handleException(throwable));
        }
    }

    /**
     * Analysis of the data returned by the service
     * The data returned by the normal server and the exceptions that the server may return
     *
     * @param <T>
     */
    private static class ResponseFunction<T> implements Function<Response<T>, ObservableSource<T>> {

        @Override
        public ObservableSource<T> apply(Response<T> tResponse) throws Exception {
            int code = tResponse.getCode();
            String message = tResponse.getMsg();
            if (code == 200) {
                return Observable.just(tResponse.getData());
            } else {
                return Observable.error(new ApiException(code, message));
            }
        }
    }
}

1.6 handle Exception

The Exception here is divided into two parts:

Exceptions generated locally, such as parsing errors, network link errors, and so on. Excption generated by the server, such as 404, 503, etc. Excption returned by the server.

Define the unified processing of ApiException:

public class ApiException extends Exception {
    private int code;
    private String displayMessage;

    public ApiException(int code, String displayMessage) {
        this.code = code;
        this.displayMessage = displayMessage;
    }

    public ApiException(int code, String message, String displayMessage) {
        super(message);
        this.code = code;
        this.displayMessage = displayMessage;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getDisplayMessage() {
        return displayMessage;
    }

    public void setDisplayMessage(String displayMessage) {
        this.displayMessage = displayMessage;
    }
}

The Exception generated locally is handled as follows:

public class CustomException {

    /**
     * unknown mistake
     */
    public static final int UNKNOWN = 1000;

    /**
     * Parsing error
     */
    public static final int PARSE_ERROR = 1001;

    /**
     * Network Error
     */
    public static final int NETWORK_ERROR = 1002;

    /**
     * Protocol error
     */
    public static final int HTTP_ERROR = 1003;

    public static ApiException handleException(Throwable e) {
        ApiException ex;
        if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {
            //Parsing error
            ex = new ApiException(PARSE_ERROR, e.getMessage());
            return ex;
        } else if (e instanceof ConnectException) {
            //Network Error
            ex = new ApiException(NETWORK_ERROR, e.getMessage());
            return ex;
        } else if (e instanceof UnknownHostException || e instanceof SocketTimeoutException) {
            //connection error
            ex = new ApiException(NETWORK_ERROR, e.getMessage());
            return ex;
        } else {
            //unknown mistake
            ex = new ApiException(UNKNOWN, e.getMessage());
            return ex;
        }
    }
}

How to throw these exceptions? Then look at the exception handling code in ResponseTransformer mentioned above. Both types of exceptions are emitted through Observable.error.

// Local exception handling
Observable.error(CustomException.handleException(throwable));

// Judge the code put back by the server
  int code = tResponse.getCode();
  String message = tResponse.getMsg();
  if (code == 200) {
      return Observable.just(tResponse.getData());
  } else {
      return Observable.error(new ApiException(code, message));
   }

1.7 thread switching

Having used Rxjava, you should be familiar with Rxjava thread switching. Here, the thread switching is separately encapsulated into a class and then combined with compose.

public class SchedulerProvider implements BaseSchedulerProvider {

    @Nullable
    private static SchedulerProvider INSTANCE;

    // Prevent direct instantiation.
    private SchedulerProvider() {
    }

    public static synchronized SchedulerProvider getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SchedulerProvider();
        }
        return INSTANCE;
    }

    @Override
    @NonNull
    public Scheduler computation() {
        return Schedulers.computation();
    }

    @Override
    @NonNull
    public Scheduler io() {
        return Schedulers.io();
    }

    @Override
    @NonNull
    public Scheduler ui() {
        return AndroidSchedulers.mainThread();
    }

    @NonNull
    @Override
    public <T> ObservableTransformer<T, T> applySchedulers() {
        return observable -> observable.subscribeOn(io())
                .observeOn(ui());
    }
}

2. Actual combat

After packaging, the next step is the actual use. However, in actual development, the logic may be implemented first and then encapsulated. Many things need to be encapsulated after the code is written.

2.1 initialization

Initialized in BaseApplication.

public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        NetWorkManager.getInstance().init();
    }
}

2.2. Build the MVP structure and use it in Presenter.

Disposable disposable = model.getCarList("xxxxxx")
                 .compose(ResponseTransformer.handleResult())
                 .compose(schedulerProvider.applySchedulers())
                 .subscribe(carBeans -> {
                     // Process data directly to List<JavaBean> carBeans
                     view.getDataSuccess();
                 }, throwable -> {
                     // handle exception
                     view.getDataFail();
                 });

         mDisposable.add(disposable);

Two callbacks, after the callback, the View layer can make corresponding UI changes. The use of compose is used here and there is no more explanation. Here compose combines thread switching encapsulation and transformation encapsulation to reduce redundant code.

The final directory structure:

structure

3. Points to note

  1. In many cases, effects need to be achieved before encapsulation can be carried out.
  2. When the framework is encapsulated, the logic layer is encapsulated. Do not write the business layer code in the logic layer.
created at:12-29-2021
edited at: 12-29-2021: