RxJava2 + Retrofit2 完全指南 之 統(tǒng)一狀態(tài)碼/Exception處理

前言

直接上數(shù)據(jù)結(jié)構(gòu):

{
    "code": 200,
    "data": {
        "id": "1",
        "name": "name1",
        "stargazers_count": 1
    },
    "msg": "請求成功"
}

上面的數(shù)據(jù)結(jié)構(gòu)是一般比較簡單而常見的數(shù)據(jù)結(jié)構(gòu),在正確的情況下我們只關(guān)心data里面的數(shù)據(jù),錯誤的情況下我們關(guān)心codemsg提示,而區(qū)分這兩種情況又要不斷的寫大量的樣板代碼,這不是首選。所以就有了兩種方法:

  • 通過定義泛型T來處理
  • 自定義ResponseBodyConverter實現(xiàn)統(tǒng)一處理

首先講講泛型T的處理方式,基本如下:

public class BaseDataModel<T> {
    private int code;
    private T data;
    private String msg;

    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = 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;
    }
    public boolean isSuccessful() {
        return code == 200;
    }
}

使用也比較簡單,只需要將泛型替換為相應(yīng)的實體類就行。但是這也有一個不好的地方就是一旦項目中對接的數(shù)據(jù)結(jié)構(gòu)變得繁雜的時候,就需要不斷的頂部相應(yīng)的實體了,還是比較麻煩,后面會講到,所以我們還是使用自定義ResponseBodyConverter實現(xiàn)統(tǒng)一處理。

實現(xiàn)

分析

我在使用Retrofit的時候,一般是使用的是GsonConverterFactory,而GsonConverterFactory則是負責(zé)將我們的提交的數(shù)據(jù)進行序列化和將返回的數(shù)據(jù)進行反序列化的,所以只要我們分析完成其中的代碼,看明白他是怎么對數(shù)據(jù)進行反序列化的,那么我們就能做到統(tǒng)一的數(shù)據(jù)轉(zhuǎn)換了。

源碼目錄

GsonConverter源碼目錄

從上圖我們可以看到,其實只有三個類,分別是:GsonConverterFactory、GsonRequestBodyConverter和GsonResponseBodyConverter。

  • GsonConverterFactory 主入口,負責(zé)聯(lián)通GsonRequestBodyConverter和GsonResponseBodyConverter
  • GsonRequestBodyConverter 負責(zé)將注解為@Body的對象序列化為Json字符串。
  • GsonResponseBodyConverter 負責(zé)將返回的json字符串反序列化為對象。

其中GsonResponseBodyConverter的源碼內(nèi)容如下:

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
  private final Gson gson;
  private final TypeAdapter<T> adapter;

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

  @Override public T convert(ResponseBody value) throws IOException {
    // 這里就是對返回結(jié)果進行處理
    JsonReader jsonReader = gson.newJsonReader(value.charStream());
    try {
      T result = adapter.read(jsonReader);
      if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
        throw new JsonIOException("JSON document was not fully consumed.");
      }
      return result;
    } finally {
      value.close();
    }
  }
}

從上面的源碼可以看到,convert方法中其實就是通過Gson進行看一次序列化而已,而ResponseBody提供了string方法,直接將返回結(jié)果轉(zhuǎn)換為了字符串,那么我們就可以在中間加一道工序,將其中的數(shù)據(jù)進行相關(guān)的判斷處理,再進行返回。

編碼

首先將GsonConverter的源碼copy過來,然后改個名字,如下:

HandlerErrorGsonConvert

為了模擬不同的數(shù)據(jù)接口,硬編碼了以下三種數(shù)據(jù)結(jié)構(gòu)。

  • 數(shù)據(jù)結(jié)構(gòu)·1
{
    "code": 200,
    "message": "成功,但是沒有數(shù)據(jù)",
    "data": []
}
  • 數(shù)據(jù)結(jié)構(gòu)·2
{
    "code": -1,
    "message": "這里是接口返回的:錯誤的信息,拋出錯誤信息提示!",
    "data": []
}
  • 數(shù)據(jù)結(jié)構(gòu)·3
{
    "code": 401,
    "message": "這里是接口返回的:權(quán)限不足,請重新登錄!",
    "data": []
}

從上面三種數(shù)據(jù)結(jié)構(gòu)分析,我們需要在code200的情況下直接序列化data,其它情況下拋出codemsg,所以這里我們還需要定義一個Exception類來承載錯誤信息。

創(chuàng)建Exception類

Exception類如下,主要是對一些和后端約定的狀態(tài)碼進行轉(zhuǎn)義和包裹。

public class NetErrorException extends IOException {
    private Throwable exception;
    private int mErrorType = NO_CONNECT_ERROR;
    private String mErrorMessage;
    /*無連接異常*/
    public static final int NoConnectError = 1;
    /**
     * 數(shù)據(jù)解析異常
     */
    public static final int PARSE_ERROR = 0;
    /**
     * 無連接異常
     */
    public static final int NO_CONNECT_ERROR = 1;
    /*網(wǎng)絡(luò)連接超時*/
    public static final int SocketTimeoutError = 6;
    /**
     * 無法連接到服務(wù)
     */
    public static final int ConnectExceptionError = 7;
    /**
     * 服務(wù)器錯誤
     */
    public static final int HttpException = 8;
    /**
     * 登陸失效
     */
    public static final int LOGIN_OUT = 401;
    /**
     * 其他
     */
    public static final int OTHER = -99;
    /**
     * 沒有網(wǎng)絡(luò)
     */
    public static final int UNOKE = -1;
    /**
     * 無法找到
     */
    public static final int NOT_FOUND = 404;
    /*其他*/

    public NetErrorException(Throwable exception, int mErrorType) {
        this.exception = exception;
        this.mErrorType = mErrorType;
    }

    public NetErrorException(String message, Throwable cause) {
        super(message, cause);
    }

    public NetErrorException(String message, int mErrorType) {
        super(message);
        this.mErrorType = mErrorType;
        this.mErrorMessage = message;
    }

    @Override
    public String getMessage() {
        if (!TextUtils.isEmpty(mErrorMessage)) {
            return mErrorMessage;
        }
        switch (mErrorType) {
            case PARSE_ERROR:
                return "數(shù)據(jù)解析異常";
            case NO_CONNECT_ERROR:
                return "無連接異常";
            case OTHER:
                return mErrorMessage;
            case UNOKE:
                return "當(dāng)前無網(wǎng)絡(luò)連接";
            case ConnectExceptionError:
                return "無法連接到服務(wù)器,請檢查網(wǎng)絡(luò)連接后再試!";
            case HttpException:
                try {
                    if (exception.getMessage().equals("HTTP 500 Internal Server Error")) {
                        return "服務(wù)器發(fā)生錯誤!";
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (exception.getMessage().contains("Not Found"))
                    return "無法連接到服務(wù)器,請檢查網(wǎng)絡(luò)連接后再試!";
                return "服務(wù)器發(fā)生錯誤";
        }


        try {
            return exception.getMessage();
        } catch (Exception e) {
            return "未知錯誤";
        }

    }
    /**
     * 獲取錯誤類型
     */
    public int getErrorType() {
        return mErrorType;
    }
}

統(tǒng)一訂閱處理

由于這里使用的是RxJava,需要自定義一個Subscriber來對Convert拋出的Exception進行捕獲,也需要對其它Exception進行捕獲和包裹,防止發(fā)生錯誤后直接崩潰,代碼不多,如下:

public abstract class ApiSubscriber<T> extends ResourceSubscriber<T> {

    @Override
    public void onError(Throwable e) {
        NetErrorException error = null;
        if (e != null) {
            // 對不是自定義拋出的錯誤進行解析
            if (!(e instanceof NetErrorException)) {
                if (e instanceof UnknownHostException) {
                    error = new NetErrorException(e, NetErrorException.NoConnectError);
                } else if (e instanceof JSONException || e instanceof JsonParseException) {
                    error = new NetErrorException(e, NetErrorException.PARSE_ERROR);
                } else if (e instanceof SocketTimeoutException) {
                    error = new NetErrorException(e, NetErrorException.SocketTimeoutError);
                } else if (e instanceof ConnectException) {
                    error = new NetErrorException(e, NetErrorException.ConnectExceptionError);
                } else {
                    error = new NetErrorException(e, NetErrorException.OTHER);
                }
            } else {
                error = new NetErrorException(e.getMessage(), NetErrorException.OTHER);
            }
        }

        // 回調(diào)抽象方法
        onFail(error);

    }

    /**
     * 回調(diào)錯誤
     */
    protected abstract void onFail(NetErrorException error);
    
}

修改HandlerErrorGsonResponseBodyConverter

這一步其實沒什么難度,只是在convert方法中提前將ResponseBody.string()取出來,通過最簡單的JSONObject來判斷code,為200則返回data,否則拋出自定義錯誤。詳細代碼如下:

final class HandlerErrorGsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final TypeAdapter<T> adapter;

    /**模擬的假數(shù)據(jù)*/
    private final List<String> mockResult;

    private final Random random;

    HandlerErrorGsonResponseBodyConverter(TypeAdapter<T> adapter) {
        this.random = new Random();
        this.adapter = adapter;
        mockResult = new ArrayList<>();
        mockResult.add("{\"code\":200,\"message\":\"成功,但是沒有數(shù)據(jù)\",\"data\":[]}");
        mockResult.add("{\"code\":-1,\"message\":\"這里是接口返回的:錯誤的信息,拋出錯誤信息提示!\",\"data\":[]}");
        mockResult.add("{\"code\":401,\"message\":\"這里是接口返回的:權(quán)限不足,請重新登錄!\",\"data\":[]}");
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        // 這里就是對返回結(jié)果進行處理
        String jsonString = value.string();
        try {
            // 這里為了模擬不同的網(wǎng)絡(luò)請求,所以采用了本地字符串的格式然后進行隨機選擇判斷結(jié)果。
            int resultIndex = random.nextInt(mockResult.size() + 1);
            if (resultIndex == mockResult.size()) {
                return adapter.fromJson(jsonString);

            } else {
                // 這里模擬不同的數(shù)據(jù)結(jié)構(gòu)
                jsonString = mockResult.get(resultIndex);

                Log.e("TAG", "這里進行了返回結(jié)果的判斷");
                // ------------------ JsonObject 只做了初略的判斷,具體情況自定
                JSONObject object = new JSONObject(jsonString);
                int code = object.getInt("code");
                if (code != 200) {
                    throw new NetErrorException(object.getString("message"), code);
                }
                return adapter.fromJson(object.getString("data"));

            }

        } catch (JSONException e) {
            e.printStackTrace();
            throw new NetErrorException("數(shù)據(jù)解析異常", NetErrorException.PARSE_ERROR);
        } finally {
            value.close();
        }
    }
}

如何調(diào)用

主要是有以下兩個地方:

  1. 創(chuàng)建Retrofit的時候?qū)?code>addConverterFactory換成自定義的HandlerErrorGsonConverterFactory.create()
  2. RxJava的subscribeWith換成自定義的ApiSubscriber<T>。

部分代碼如下:

......
retrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(HandlerErrorGsonConverterFactory.create()) // 這里使用的是用自己自定義的轉(zhuǎn)換器
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

service = retrofit.create(GitHubService.class);
......

mResultTv.setText("創(chuàng)建請求................\n");
disposable = service.listRxJava2FlowableRepos("aohanyao", "owner")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeWith(new ApiSubscriber<List<Repo>>() {
        @Override
        public void onNext(List<Repo> repos) {
            mResultTv.append("請求成功,repoCount:" + repos.size() + ":\n");
            for (Repo repo : repos) {
                mResultTv.append("repoName:" + repo.getName() + "    star:" + repo.getStargazers_count() + "\n");
            }
        }

        @Override
        protected void onFail(NetErrorException error) {
            mResultTv.append("請求失敗" + error.getMessage() + "................\n");
        }

        @Override
        public void onComplete() {
            mResultTv.append("請求成功................\n");
        }
    });

演示

HandlerResponseError

結(jié)束

這里只寫了對返回狀態(tài)碼和錯誤的統(tǒng)一處理,并未對不同的數(shù)據(jù)結(jié)構(gòu)進行處理,下一篇將對不同的數(shù)據(jù)結(jié)構(gòu)進行統(tǒng)一處理。

源碼在這里

微信公眾號

AndroidRookie
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,416評論 4 61
  • “陽光花城”長大的我,對于賞雪那是萬分激動的事情。怕冷,但是不怕雪天的冷。 早上起來拉開窗簾,遠處的群山白茫茫一片...
    南英子閱讀 2,315評論 36 64
  • 聽風(fēng)泉下露,醉雨夢中菊; 初雪寒釣客,南雁遠來書。 蘭陵花杏酒,老綠一枝疏; 四時有佳意,何妨笑問愚。
    劉海峰閱讀 360評論 1 2
  • 美美的小姑娘,今天是第一天正式上課,因為要回趟老家,所以我去輔導(dǎo)班接的她,^_^,第一句話就是:媽媽,你再別來接我...
    六月的那些日子閱讀 243評論 0 0

友情鏈接更多精彩內(nèi)容