mvc/mvp/mvvm

一看就会的通用网络框架MVP+RxJava+Retrofit

2019-03-23  本文已影响188人  PenguinMan

前言

谈及Android当下最流行的网络框架,如果Retrofit认第二,恐怕没有一个人敢认第一。RESTful风格,丰富的注解规范,超强的解耦功能,支持RxJava,支持Json与xml多种反序列化工具等等。说道Retrofit就不得不说说他的一奶同胞okhttp,Retrofit可以理解为是Square公司对okhttp的接口封装,正所谓官方插件,最为好用,Retrofit确确实实的帮助我们实现了更优雅的网络编程。

RESTful风格接口

对RESTful风格的支持往往是Retrofit被提及最多的一个优点,那么RESTful是什么呢?RESTful是一种软件API架构风格,提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。简单的说,一套遵守RESTful风格的API可以接口做到看名知义,极大程度的提高了代码的可读性与维护性。
举例说明,比如我们有这么样一个用户系统:

序号 URL 功能 请求类型
1 https://yanghaoyi.openapi.com/api/v1/users 增加用户 POST
2 https://yanghaoyi.openapi.com/api/v1/users 删除用户 DELETE
3 https://yanghaoyi.openapi.com/api/v1/users 修改用户 PUT
4 https://yanghaoyi.openapi.com/api/v1/users 查找用户 GET

接口名用名词定义,不用动词,对数据的增、删、改、查操作通过请求类型来区分,这套风格的接口最大的好处就是前后端分离,前端拿到数据只负责展示和渲染,不对数据做任何处理。后端处理数据并以JSON格式传输出去,定义这样一套统一的接口,在Web,IOS,Android三端都可以用相同的接口。

Retrofit注解

Retrofit的丰富注解系统实现了客户端与服务端接口的无缝对接,对于基于Spring开发的Java后端,客户端甚至可以与服务端共用一套API文件。
Retrofit方法注解

序号 类型 功能
1 @POST POST请求
2 @DELETE DELETE请求
3 @PUT PUT请求
4 @PATCH PATCH请求

Retrofit参数注解

序号 类型 功能
1 @Headers 请求头
2 @Path 路径拼接
3 @Query 参数值
4 @Body 请求体
5 @FormUrlEncoded 表单

接入Retrofit

首先在所属模块的build.gradle中引入Retrofit与okhttp依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.squareup.retrofit2:retrofit:2.1.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.0.2'
    implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.1.2'
    implementation 'io.reactivex:rxjava:1.1.6'
    implementation 'io.reactivex:rxandroid:1.2.1'
}

初始化Retrofit

对项目而言,我们会在多个模块进行HTTP通信,我们不需要对每个请求都创建一个Client,HttpClient其实相当于一个小型浏览器,对应用而言,一个应用拥有一个Client就足够了,所以笔者也是建议对Retrofit的初始化放在一个单例类中,尤其建议使用Joshua Bloch在《Effective Java》中推荐的枚举单例。这种单例实现方法是一个足可以颠覆你单例模式复杂锁校验的一种单例写法,下面让我们来看一下如何初始化一个Client。

/**
 * @author YangHaoyi on 2019/3/4.
 * Email  : yanghaoyi@qq.com.
 * Description :请求Client
 * Change : YangHaoYi on 2019/3/4.
 * Version :V 1.1
 */
public enum  ApiClient {

    /** client单例 */
    INSTANCE;
    /** 请求域名 */
    private static final String BASE_URL = "https://yanghaoyi.openapi.com/";
    private AdvanceAPI api;

    /** 初始化 */
    public void init(){
        //构建Client
        OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
        //添加Token拦截器
        okHttpClient.addInterceptor(new TokenInterceptor());
        //添加Log拦截器
        okHttpClient.addInterceptor(new LoggingInterceptor());
        //添加Https验证
        okHttpClient.hostnameVerifier(new HostnameVerifier());
        //构建Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient.build())
                //添加RxJava适配
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                //添加Gson解析适配
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        api = retrofit.create(AdvanceAPI.class);
    }

    /** 获取API */
    public AdvanceAPI getApi() {
        return api;
    }
}

TokenInterceptor是一个对token鉴权机制的拦截器,它的作用是当我们的请求token过期时,能够拦截请求,进行一次token获取的同步请求,同步请求返回刷新token后,再重复进行之前鉴权失败的请求。
核心代码:

        synchronized (ApiClient.INSTANCE){
            //比较请求的token与本地存储的token   如果不一致还是直接重试
            String request_token=request.header(TOKEN);
            String access_token= TokenManager.getToken();
            if(request_token!=null&&access_token!=null&&!request_token.equals(access_token)){
                Log.d(TAG,"Request_retry:"+requestUrl);
                //等待的request重新拼装请求头
                Request newRequest=request.newBuilder().header("token", TokenManager.getToken()).build();
                return chain.proceed(newRequest);//重试request
            }
            if (null!=httpResult.getErr()&&httpResult.getErr().endsWith("auth fail")){
                Log.d(TAG,"RefreshToken");
                String token = getNewToken();
                Log.d(TAG,"RefreshToken_finish"+token);
                //保存token
                TokenManager.setToken(token);
                //重新拼装请求头
                Request newRequest=request.newBuilder().header("token", TokenManager.getToken()).build();
                //重试request
                return chain.proceed(newRequest);
            }
        }
        Log.d(TAG,"Request_end"+requestUrl);
        return originalResponse;
    }


    private String getNewToken() throws IOException {

        OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
        okHttpClient.addInterceptor(new LoggingInterceptor());

        Retrofit retrofit= new Retrofit.Builder()
                .baseUrl("https://yanghaoyi.openapi.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .client(okHttpClient.build())
                .build();
        retrofit2.Response<TokenInfo> tokenJson = retrofit.create(AdvanceAPI.class).refreshToken().execute();
        String headerToken = tokenJson.body().getData().getToken();
        return headerToken;
    }

LoggingInterceptor是对请求的Log打印拦截器,它的作用是对请求的url,请求体,请求参数,以及服务端的响应体进行打印,以方便调试。

        Log.w(TAG," ----------Start----------------");
        Log.w(TAG, "| "+request.toString());
        String method=request.method();
        if("POST".equals(method)){
            StringBuilder sb = new StringBuilder();
            if (request.body() instanceof FormBody) {
                FormBody body = (FormBody) request.body();
                for (int i = 0; i < body.size(); i++) {
                    sb.append(body.encodedName(i) + "=" + body.encodedValue(i) + ",");
                }
                sb.delete(sb.length() - 1, sb.length());
                Log.w(TAG, "| RequestParams:{"+sb.toString()+"}");
            }
        }
        Log.w(TAG, "| Response:" + content);
        Log.w(TAG," ----------End:"+duration+"毫秒----------");
        return response.newBuilder()
                .body(ResponseBody.create(mediaType, content))
                .build();

HostnameVerifier是对https请求的一个证书验证系统,它的作用是对https进行鉴权验证。

Retrofit接口示例

    //增加用户信息
    @POST("api/v1/user")
    Observable<User> uploadUser(@Body RequestBody body);
    //删除用户信息
    @DELETE("api/v1/user/userId={userId}")
    Observable<User> deletedUserById(@Path("userId") String userId);
    //修改用户信息
    @PUT("api/v1/user/userId={userId}")
    Observable<User> changeUserById(@Path("userId") String userId,@Body RequestBody body);
    //查询用户信息
    @GET("api/v1/user/userId={userId}")
    Observable<User> getUserById(@Path("userId") String userId);

Retrofit将Http请求抽象成Java接口,并在接口里面采用注解来配置网络请求参数。用动态代理将该接口的注解解析成一个Http请求,最后再执行 Http请求。

构建Retrofit请求

    /** 获取充电站列表 **/
    public void requestChargeStationList(final OnGetDataListener<List<StationListViewData>> listener, Point position, int pageNum){
        Observable<StationListNetData> response = api.getNearbyStation(configHeader(),configApiKey(),configParams(position,pageNum));
        response.observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(new Observer<StationListNetData>() {
                    @Override
                    public void onCompleted() {
                        //请求完成
                    }
                    @Override
                    public void onError(Throwable throwable) {
                        listener.fail(null,"");
                    }
                    @Override
                    public void onNext(StationListNetData netData) {
                        listener.success(convert.convertChargeStationList(netData));
                    }
                });
    }

可以看到,结合RxJava的线程调度,Retrofit的请求变得异常简洁,于此同时RxJava的map与zip等多种操作符,可以让多请求同步返回,多请求异步返回变得更加简洁易读,或许你觉得Retrofit也不过如此,让我们来看一下重构之前前人的代码。

  public <T extends RequestBaseBean> void net(final ChargingManager.ChargingCallback chargingCallback, final int type, final T t) {
        if (chargingCallback != null && t != null && t.getKeys() != null && t.getKeys().length != 0) {
            if (!this.isRequest(type, chargingCallback)) {
                this.doSelf(type);
                if (NetWorkUtils.isNetworkConnected(GlobalUtil.getContext())) {
                    switch(type) {
                    case 1:
                        this.doNet(type, chargingCallback, t);
                        break;
                    case 2:
                    case 3:
                    case 4:
                        String token = ManagerPreferences.CHARGE_TOKEN.get();
                        if (TextUtils.isEmpty(token)) {
                            RequestTokenBean bean = new RequestTokenBean();
                            bean.setKeys(new String[]{"client_id", "client_secret"});
                            this.net(new ChargingManager.ChargingCallback() {
                                public void isRequest(int typeToken) {
                                }

                                public void callback(int typeToken) {
                                    switch(typeToken) {
                                    case 1:
                                        switch(type) {
                                        case 2:
                                            ChargingManager.this.putState("nearly", false);
                                            break;
                                        case 3:
                                            ChargingManager.this.putState("route", false);
                                            break;
                                        case 4:
                                            ChargingManager.this.putState("info", false);
                                        }

                                        ChargingManager.this.net(chargingCallback, type, t);
                                        break;
                                    default:
                                        ChargingManager.this.err(type, chargingCallback);
                                    }

                                }
                            }, 1, bean);
                        } else {
                            this.doNet(type, chargingCallback, t);
                        }
                    }
                } else {
                    this.netError(type, chargingCallback);
                }

            }
        } else {
            this.errorSelf(type);
        }
    }
      OkHttpUtils.post().url(url).build().connTimeOut(1000 * 5).execute(new StringCallback() {
            @Override
            public void onError(Call call, Exception e, int id) {
                err(type, chargingCallback);
            }

            @Override
            public void onResponse(String response, int id) {
//                LogToFileUtil.lookToLogcatForError(log_tag, "response:" + response);
                try {
                    JSONObject object = new JSONObject(response);
                    int status = -1;//初始化是为了json解析异常时防止int类型默认取0,而0是json正常情况下的返回值
                    status = object.optInt("status");
                    switch (type) {
                        case TYPE_REQUEST_TOKEN: {
                            if (status == 0) {
                                JSONObject data = object.optJSONObject("data");
                                if (data != null) {
                                    String token = data.optString("token");
                                    if (token != null) {
//                                        SettingController.getInstance().setStringValue(TAG_CHARGE_TOKEN, token);
                                        ManagerPreferences.CHARGE_TOKEN.set(token);
                                        chargingCallback.callback(TOKEN_SUC);
                                        putState(KEY_TOKEN, false);
                                        return;
                                    }
                                }
                            }
                        }
                        break;
                        case TYPE_REQUEST_NEARLY: {
                            if (status == 0) {
                                poiJson(object, aroSResult);
                                suc(type, chargingCallback);
                                return;
                            } else if (status == 1) {
                                String err = object.optString("err") == null ? "" : object.optString("err");
                                if (err.equals("auth fail")) {
                                    //去判断一次token
                                    Boolean state = getTokenState.get(KEY_NEARLY);
                                    if (state == null || !state.booleanValue()) {
                                        resetGetTokenState(KEY_NEARLY);
                                        net(chargingCallback, type, t);
                                        return;
                                    }
                                }
                            }
                        }
                        break;
                        case TYPE_REQUEST_ROUTE: {
                            if (status == 0) {
                                poiJson(object, rouSResult);
                                suc(type, chargingCallback);
                                return;
                            } else if (status == 1) {
                                String err = object.optString("err") == null ? "" : object.optString("err");
                                if (err.equals("auth fail")) {
                                    //去判断一次token
                                    Boolean state = getTokenState.get(KEY_NEARLY);
                                    if (state == null || !state.booleanValue()) {
                                        resetGetTokenState(KEY_ROUTE);
                                        net(chargingCallback, type, t);
                                        return;
                                    }
                                }
                            }
                        }
                        break;
                        case TYPE_REQUEST_INFO: {
                            if (status == 0) {
                                infoJson(object);
                                suc(type, chargingCallback);
                                return;
                            } else if (status == 1) {
                                String err = object.optString("err") == null ? "" : object.optString("err");
                                if (err.equals("auth fail")) {
                                    //去判断一次token
                                    Boolean state = getTokenState.get(KEY_INFO);
                                    if (state == null || !state.booleanValue()) {
                                        resetGetTokenState(KEY_INFO);
                                        net(chargingCallback, type, t);
                                        return;
                                    }
                                }
                            }
                        }
                        break;
                    }
                    err(type, chargingCallback);
                } catch (JSONException e) {
                    err(type, chargingCallback);
                }
            }
        });


    }


现在你还觉得Retrofit不简洁么?

结合MVP模式

为了是软件能够更加符合迪米特设计原则,这里笔者强烈建议工程使用MVP模式,让数据与视图分离,只有这样使得服务端的接口变更对客户端不再是牵一发而动全身,笔者由于是后接手的项目,老项目很多地方都是采取Activity上帝模式的写法,导致一旦服务端API变更,客户端都要经历一场血淋淋的改动,Bug的产生也就在所难免,可谓苦不堪言。
首先让我们来明确两个概念:
①NetData
NetData就是服务端接口返回的数据,服务端定义的是什么,NetData就是什么,透传数据。
②ViewData
ViewData是客户端使用的数据,即视图数据,它可以是NetData的局部数据,也可以是对NetData符合View展示的一些数据封装。
客户端请求显示流程如图所示:


MVP请求.png

视图的展示使用ViewData,当Api接口发生变更,我们无需更改Presenter逻辑层与View视图层的代码,只要修改与数据相关的Model层代码即可,做到尽可能少的修改。

示例代码

GitHub完整示例代码

上一篇下一篇

猜你喜欢

热点阅读