[suggestions] Android realizes the perfect packaging of Rxjava2+Retrofit

Learning is like sailing against the current.

I learned the basic usage of rxjava and Retrofit last year, but it has not been used in practical projects. This year, we started a new project and decisively introduced rxjava and Retrofit into the new project. This article will introduce the author's encapsulation of Retrofit in the project.
Let's take a look at how to use the encapsulated Retrofit.

RetrofitHelper.getApiService()
                .getMezi()
                .compose(this.<List<MeiZi>>bindToLifecycle())
                .compose(ProgressUtils.<List<MeiZi>>applyProgressBar(this))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<List<MeiZi>>() {
                    @Override
                    public void onSuccess(List<MeiZi> response) {
                        showToast("The request succeeded. The number of girls is" + response.size());
                    }
                });

Yes, it is such a simple chain call, which can display and load animation, and also adds the management of Retrofit life cycle.
Before starting, you need to add the dependency Library in the Gradle file in the module project

    compile "io.reactivex.rxjava2:rxjava:$rootProject.ext.rxjava2Version"
    compile "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofit2Version"
    compile "com.squareup.retrofit2:converter-scalars:$rootProject.ext.retrofit2Version"
    compile "com.squareup.retrofit2:converter-gson:$rootProject.ext.retrofit2Version"
    compile "com.squareup.retrofit2:adapter-rxjava2:$rootProject.ext.retrofit2Version"
    compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
    compile "com.trello.rxlifecycle2:rxlifecycle:$rootProject.ext.rxlifecycle"
    //compile "com.trello.rxlifecycle2:rxlifecycle-android:$rootProject.ext.rxlifecycle"
    compile "com.trello.rxlifecycle2:rxlifecycle-components:$rootProject.ext.rxlifecycle"

To facilitate the modification of the dependency library version, we add dependencies in the way of "io.reactivex.rxjava2:rxjava:$rootProject.ext.rxjava2Version". Therefore, the following contents need to be added to the build.gradle file of the project:

ext {
    supportLibVersion = '25.1.0'
    butterknifeVersion = '8.5.1'
    rxjava2Version = '2.0.8'
    retrofit2Version = '2.2.0'
    rxlifecycle='2.1.0'
    gsonVersion = '2.8.0'
}

This packaging will be analyzed in detail in the following sections:

  • The base class of server response data is BasicResponse
  • Build a tool class IdeaApi that initializes Retrofit
  • Get real response data through GsonConverterFactory
  • Encapsulates the DefaultObserver processing server response
  • Processing Loading
  • Manage the Retrofit lifecycle
  • How to use encapsulation
  • Summary

1, The base class of the server response data, BasicResponse.

It is assumed that the format of Json data returned by the server is as follows:

{
 "code": 200,
 "message": "success",
 "content": {
    ...
    }
}

Build our basic response according to the Json data format (the field content in the basic response needs to be determined according to the data returned by your server). The code is as follows:

public class BasicResponse<T> {
    private int code;
    private String message;
    private T content;
    ...Omit here get,set method.

2, Build a tool class IdeaApi that initializes Retrofit.

This class obtains the instance of ApiService through RetrofitUtils. The code is as follows:

public class IdeaApi {
    public static <T> T getApiService(Class<T> cls,String baseUrl) {
        Retrofit retrofit = RetrofitUtils .getRetrofitBuilder(baseUrl).build();
        return retrofit.create(cls);
    }
}

RetrofitUtils is used to build Retrofit.Builder and configure OkHttp in the following aspects:

  1. Set the log interceptor to intercept the json data returned by the server. Retrofit directly converts the request to json data into entity classes, but sometimes we need to view json data. Retrofit does not provide the function of directly obtaining json data. Therefore, we need to customize a log interceptor to intercept json data and enter it into the console.
  2. Set Http request header. Add a request header interceptor to OkHttp and configure the request header information. You can also add request header data to the interface. For example, the user name, password (or token) are uniformly added to the request header. Subsequently, the user name, password (or token) data will be carried in the request header of each interface to avoid adding it separately for each interface.
  3. Configure caching for OkHttp. Cache processing can also be implemented with the interceptor. It includes controlling the maximum life value of the cache and the expiration time of the cache.
  4. If https is used, we can also process certificate verification and server verification here.
  5. Add GsonConverterFactory for Retrofit. This is an important link, which will be explained in detail later.

RetrofitUtils code is as follows:

public class RetrofitUtils {
    public static OkHttpClient.Builder getOkHttpClientBuilder() {

        HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
            @Override
            public void log(String message) {
                try {
                    LogUtils.e("OKHttp-----", URLDecoder.decode(message, "utf-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    LogUtils.e("OKHttp-----", message);
                }
            }
        });
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        File cacheFile = new File(Utils.getContext().getCacheDir(), "cache");
        Cache cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb

        return new OkHttpClient.Builder()
                .readTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .connectTimeout(Constants.DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
                .addInterceptor(loggingInterceptor)
                .addInterceptor(new HttpHeaderInterceptor())
                .addNetworkInterceptor(new HttpCacheInterceptor())
               // . sslsocketfactory (sslcontextfactory. Getsslsocketfactoryfortwoway()) / / https authentication. If you want to use https and a user-defined certificate, you can remove these two lines of comments and configure the certificate yourself.
               // .hostnameVerifier(new SafeHostnameVerifier())
                .cache(cache);
    }

    public static Retrofit.Builder getRetrofitBuilder(String baseUrl) {
        Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().create();
        OkHttpClient okHttpClient = RetrofitUtils.getOkHttpClientBuilder().build();
        return new Retrofit.Builder()
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(baseUrl);
    }
}

3, Get real response data through GsonConverterFactory

In the first section, we build the server response data BasicResponse, which consists of three fields: code, message, and content. Where code is the error code returned by the server. We will agree with the server the code value when the request is successful in advance. For example, 200 indicates that the request is successful. However, various errors are inevitable in the process of requesting server data. For example, when the user logs in, the password is wrong and the request parameters are wrong. At this time, the server will return the corresponding error code according to the error condition. Generally speaking, we only care about the content data when the code is 200. When the code is not 200, we only need to give the corresponding Toast prompt. In fact, we only use the content data when the code is 200. Therefore, we can consider filtering out code and message and returning only the content in the callback with successful request.

In this case, we need to customize the GsonConverterFactory to implement it. We can directly copy three related classes of GsonConverterFactory from the source code of Retrofit for modification.

The final part is to modify the convert method in GsonResponseBodyConverter. In this method, get the server response data and judge whether the code is 200. If yes, get the content and return it. If not, you can throw the corresponding custom exception here. Then handle exceptions uniformly in the Observer. The code of GsonResponseBodyConverter is as follows:

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, Object> {

    private final TypeAdapter<T> adapter;

    GsonResponseBodyConverter(TypeAdapter<T> adapter) {
        this.adapter = adapter;
    }

    @Override
    public Object convert(ResponseBody value) throws IOException {
        try {
            BasicResponse response = (BasicResponse) adapter.fromJson(value.charStream());
            if (response.getCode()==200) {
            return response.getResults();
            } else {
                // Errors of a specific API are handled in the onError method of the corresponding DefaultObserver
                throw new ServerResponseException(response.getCode(), response.getMessage());
            }
        } finally {
            value.close();
        }
        return null;
    }
}

4, Build the DefaultObserver to process the server response.

In the previous section, we talked about some possible situations when requesting the server, such as password error and parameter error. The server returned the corresponding error code, and we threw the corresponding custom exception according to the error code. In addition, some exceptions may occur when we initiate a network request. For example, there is no network, the request times out, or the server returns data but there is a data parsing exception during parsing. We should also deal with such cases in a unified manner. Then we need to customize a DefaultObserver class to inherit Observer and override the corresponding methods.
The two most important methods in this class are onNext and onError.

1. When the server returns data successfully, it will call back to the onNext method. Therefore, we can define an abstract method onSuccess(T response) in DefaultObserver and override the onSuccess method when calling the network.

2. If any exception occurs in the process of requesting the server, it will be recalled to the onError method. Including the exceptions thrown by ourselves in the previous section, we will call back to onError. Therefore, our main play is to deal with onError. In onError, we can give the corresponding Toast prompt according to the exception information.

The code of DefaultObserver class is as follows:

public abstract class DefaultObserver<T> implements Observer<T> {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(T response) {
        onSuccess(response);
        onFinish();
    }

    @Override
    public void onError(Throwable e) {
        LogUtils.e("Retrofit", e.getMessage());
        if (e instanceof HttpException) {     //   HTTP error
            onException(ExceptionReason.BAD_NETWORK);
        } else if (e instanceof ConnectException
                || e instanceof UnknownHostException) {   //   Connection error
            onException(ExceptionReason.CONNECT_ERROR);
        } else if (e instanceof InterruptedIOException) {   //  connection timed out
            onException(ExceptionReason.CONNECT_TIMEOUT);
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException) {   //  Parsing error
            onException(ExceptionReason.PARSE_ERROR);
        }else if(e instanceof ServerResponseException){
            onFail(e.getMessage());
        } else {
            onException(ExceptionReason.UNKNOWN_ERROR);
        }
        onFinish();
    }

    @Override
    public void onComplete() {
    }

    /**
     * Request succeeded
     *
     * @param response Data returned by the server
     */
    abstract public void onSuccess(T response);

    /**
     * The server returned data, but the response code was not 200
     *
     */
    public void onFail(String message) {
        ToastUtils.show(message);
    }

    public void onFinish(){}

    /**
     * Request exception
     *
     * @param reason
     */
    public void onException(ExceptionReason reason) {
        switch (reason) {
            case CONNECT_ERROR:
                ToastUtils.show(R.string.connect_error, Toast.LENGTH_SHORT);
                break;

            case CONNECT_TIMEOUT:
                ToastUtils.show(R.string.connect_timeout, Toast.LENGTH_SHORT);
                break;

            case BAD_NETWORK:
                ToastUtils.show(R.string.bad_network, Toast.LENGTH_SHORT);
                break;

            case PARSE_ERROR:
                ToastUtils.show(R.string.parse_error, Toast.LENGTH_SHORT);
                break;

            case UNKNOWN_ERROR:
            default:
                ToastUtils.show(R.string.unknown_error, Toast.LENGTH_SHORT);
                break;
        }
    }

    /**
     * Request network failure reason
     */
    public enum ExceptionReason {
        /**
         * Failed to parse data
         */
        PARSE_ERROR,
        /**
         * Network problems
         */
        BAD_NETWORK,
        /**
         * Connection error
         */
        CONNECT_ERROR,
        /**
         * connection timed out
         */
        CONNECT_TIMEOUT,
        /**
         * unknown error
         */
        UNKNOWN_ERROR,
    }
}

5, Processing Loading

For Loading, we can do a very elegant processing through the RxJava compose operator. First, define a ProgressUtils tool class, and then make a transformation through RxJava's ObservableTransformer to handle Loading. To display Loading, just add. Compose (ProgressUtils. < T > applyprogressbar (this)).
ProgressUtils code is as follows:

public class ProgressUtils {
    public static <T> ObservableTransformer<T, T> applyProgressBar(
            @NonNull final Activity activity, String msg) {
        final WeakReference<Activity> activityWeakReference = new WeakReference<>(activity);
        final DialogUtils dialogUtils = new DialogUtils();
        dialogUtils.showProgress(activityWeakReference.get());
        return new ObservableTransformer<T, T>() {
            @Override
            public ObservableSource<T> apply(Observable<T> upstream) {
                return upstream.doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(Disposable disposable) throws Exception {

                    }
                }).doOnTerminate(new Action() {
                    @Override
                    public void run() throws Exception {
                        Activity context;
                        if ((context = activityWeakReference.get()) != null
                                && !context.isFinishing()) {
                            dialogUtils.dismissProgress();
                        }
                    }
                }).doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(Disposable disposable) throws Exception {
                        /*Activity context;
                        if ((context = activityWeakReference.get()) != null
                                && !context.isFinishing()) {
                            dialogUtils.dismissProgress();
                        }*/
                    }
                });
            }
        };
    }

    public static <T> ObservableTransformer<T, T> applyProgressBar(
            @NonNull final Activity activity) {
        return applyProgressBar(activity, "");
    }
}

So far, the secondary encapsulation of RxJava and Retrofit has been basically completed. But we can't ignore a very important point, that is, the life cycle of network requests. We will explain it in detail in the next section.

6, Manage the Retrofit lifecycle

When the activity is destroyed, the network request should also be terminated. Otherwise, it may cause memory leakage. It will seriously affect the performance of the App! Therefore, the management of Retrofit life cycle is also an important content. Here we use RxLifecycle To manage the lifecycle of Retrofit. Its application process is as follows:

1. Add dependencies in gradel as follows:

compile 'com.trello.rxlifecycle2:rxlifecycle:2.1.0'
compile 'com.trello.rxlifecycle2:rxlifecycle-components:2.1.0'

2. Let our BaseActivity inherit RxAppCompatActivity.
The specific codes are as follows:

public abstract class BaseActivity extends RxAppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        init(savedInstanceState);
    }
    protected void showToast(String msg) {
        ToastUtils.show(msg);
    }

    protected abstract @LayoutRes int getLayoutId();

    protected abstract void init(Bundle savedInstanceState);
}

Similarly, the BaseFragment of our project inherits RxFragment (note that RxFragment under the inherited V4 package is used), as follows:

public abstract class BaseFragment extends RxFragment {

    public View rootView;
    public LayoutInflater inflater;

    @Nullable
    @Override
    public final View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        this.inflater = inflater;
        if (rootView == null) {
            rootView = inflater.inflate(this.getLayoutId(), container, false);
            init(savedInstanceState);
        }
        ViewGroup parent = (ViewGroup) rootView.getParent();
        if (parent != null) {
            parent.removeView(rootView);
        }
        return rootView;
    }

    protected abstract int getLayoutId();

    protected abstract void init(Bundle savedInstanceState);

    protected void showToast(String msg) {
        ToastUtils.show(msg);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
    }
}

3. Manage the Retrofit lifecycle using the compose operator:

myObservable
            .compose(bindToLifecycle())
            .subscribe();

perhaps

myObservable
    .compose(RxLifecycle.bindUntilEvent(lifecycle, ActivityEvent.DESTROY))
    .subscribe();

For detailed usage of RxLifecycle, please refer to RxLifecycle official website

7, How to use encapsulation

The previous sections explained how to re encapsulate RxJava. The encapsulated code can be placed in the Library module of our project. So how should we use it in the app module after encapsulation?

1. Define an interface to store the API of our project

public interface IdeaApiService {
    /**
     * The generic T of this interface server response data BasicResponse should be list < Meizi >
     * Basic response < list < Meizi > >
     * @return BasicResponse<List<MeiZi>>
     */
    @Headers("Cache-Control: public, max-age=10")//Set the cache time to 100s
    @GET("welfare/10/1")
    Observable<List<MeiZi>> getMezi();

    /**
     * The login interface is false and cannot return data
     * @return
     */
    @POST("login.do")
    Observable<LoginResponse> login(@Body LoginRequest request);

    /**
     * The refresh token interface is false and cannot return data
     * @return
     */
    @POST("refresh_token.do")
    Observable<RefreshTokenResponseBean> refreshToken(@Body RefreshTokenRequest request);

    @Multipart
    @POST("upload/uploadFile.do")
    Observable<BasicResponse> uploadFiles(@Part List<MultipartBody.Part> partList);
}

2. Define a RetrofitHelper class to obtain the instance of IdeaApiService through IdeaApi.

public class RetrofitHelper {
    private static IdeaApiService mIdeaApiService;

    public static IdeaApiService getApiService(){
        return mIdeaApiService;
    }
    static {
       mIdeaApiService= IdeaApi.getApiService(IdeaApiService.class, Constants.API_SERVER_URL);
    }
}

3. Initiate a network request in Activity or Fragment

    /**
     * Get request
     * @param view
     */
    public void getData(View view) {
        RetrofitHelper.getApiService()
                .getMezi()
                .compose(this.<List<MeiZi>>bindToLifecycle())
                .compose(ProgressUtils.<List<MeiZi>>applyProgressBar(this))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DefaultObserver<List<MeiZi>>() {
                    @Override
                    public void onSuccess(List<MeiZi> response) {
                        showToast("The request succeeded. The number of girls is" + response.size());
                    }
                });
    }

8, Summary

This article mainly explains the secondary encapsulation of Rxjava and Retrofit. The above content is also the author's reference to various materials, which has been modified and optimized for a long time. However, in view of my limited ability, I can't avoid some wrongdoing. Please forgive me. In addition, there may be many ungraceful aspects in the official account of Guo Shen, such as the processing of response data and the processing of Loading. After the submission was pushed, I received many suggestions from small partners. Therefore, the author also referred to your opinions and optimized them. Thank you here. Finally, if you have any questions, please leave a comment in the article.

Tags: Android Retrofit rxjava

Posted on Mon, 29 Nov 2021 22:44:19 -0500 by shedokan