RxHttp 一條鏈傳送請求之強大的資料解析功能(二)

不怕天黑發表於2019-04-25

前言

在上一文中,我們對RxHttp做了一個整體的介紹,文章一經發表後,就收到了廣大讀者眾多不同的聲音,有對我的肯定,也有對RxHttp提出改進的建議,更有讀者直接指出了我的不足,為此,我收穫了很多,讓我對很多東西都有了新的認知,我想這就是很多人堅持寫作的原因,因為這裡,可以相互學習,相互交流以彌補自己的不足。所以我要感謝你們,是你們給我了動力讓我繼續寫作,我會堅持寫一些有營養的文章。

由於剛開始寫作,上一文有很多地方寫的不夠好,讓不少讀者走了冤枉路;收到讀者給我提出了改進的建議後,我加班加點將RxHttp 版本升級到了1.0.2,主要增加了設定baseUrl的功能,我想這是目前市面上最優雅的設定方法。 為此我對上一文做了很多修改,歡迎新老讀者打臉 RxHttp 一條鏈傳送請求,新一代Http請求神器(一)

簡介

資料解析器Parser在RxHttp擔任著一個很重要的角色,它的作用的將Http返回的資料,解析成我們想要的任意物件,可以用Json、DOM等任意資料解析方式。目前RxHttp提供了三個解析器,分別是SimpleParserListParserDownloadParser,如果這3個解析器不能滿足我們的業務開發,就可以自定義解析器,下面我詳細介紹。

首先我們先看看Parser的內部結構

public interface Parser<T> {

    /**
     * 資料解析
     * @param response Http執行結果
     * @return 解析後的物件型別
     * @throws IOException 網路異常、解析異常
     */
    T onParse(@NonNull Response response) throws IOException;

}
複製程式碼

可以看到,Parser就是一個介面類,並且裡面只有一個方法,輸入Http請求返回的Response物件,輸出我們傳入的泛型T,如果我們要自定義解析器,就必須要實現此介面。

在上一文中,我們對Parser做了簡單的介紹,我們來回顧一下。

SimpleParser

我們拿淘寶獲取IP的介面作為測試介面http://ip.taobao.com/service/getIpInfo.php?ip=63.223.108.42 對應的資料結構如下

public class Response {
    private int     code;
    private Address data;
    //省略set、get方法

    class Address {
        //為簡單起見,省略了部分欄位
        private String country; //國家
        private String region; //地區
        private String city; //城市
        //省略set、get方法
    }
}
複製程式碼

開始傳送請求

  RxHttp.get("http://ip.taobao.com/service/getIpInfo.php") //Get請求
        .add("ip", "63.223.108.42")//新增引數
        .addHeader("accept", "*/*") //新增請求頭
        .addHeader("connection", "Keep-Alive")
        .addHeader("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)")
        .fromSimpleParser(Response.class)  //這裡返回Observable<Response> 物件
        .as(RxLife.asOnMain(this))  //感知生命週期,並在主執行緒回撥
        .subscribe(response -> {
            //成功回撥
        }, throwable -> {
            //失敗回撥
        });
複製程式碼

上面程式碼中使用了fromSimpleParser操作符,並傳入Response.class,此是在觀察者就能只能拿到Response物件,那麼它是如何實現的呢?看名字應該也能猜到,它內部就是用了SimpleParser解析器,我們點進去看看

  public <T> Observable<T> fromSimpleParser(Class<T> type) {
    return from(SimpleParser.get(type));
  }
複製程式碼

果然它使用了SimpleParser解析器,那我們就來看看SimpleParser的原始碼

在這裡插入圖片描述
我們具體看onParser方法,可以看到。

  • 首先通過將Http請求返回的Response(注意,此Response類是OkHttp內部的類,並不上我們上面定義的類)物件,拿到Http的請求結果,為String物件
  • 然後就拿到我們傳入的泛型型別判斷是否是String型別,如果是,則直接將結果返回,否則就通過Json將結果解析成我們傳入的泛型物件
  • 最後對泛型物件做判斷,如果為空,就代表解析失敗,我們丟擲異常(這裡的異常會被RxJava的onError觀察者接收),否則返回泛型物件

到這,我想你應該知道SimpleParser解析器的作用類,它就是將Http請求返回的結果直接解析成我們想要的任意物件。

自問:你說SimpleParser能將資料解析成任意物件,而fromSimpleParser(Class<T> type)操作符傳入的是一個Class<T>型別,而對於List物件,只能傳入List.classList裡面的泛型我怎麼傳入呢?又該如何實現呢? 自答:如果想得到一個list<T>物件,通過fromSimpleParser操作符確實沒辦法實現,但是同時SimpleParser卻能實現,我們可以直接new 出一個SimpleParser物件,並且傳入一個List<T>即可,我們假設要獲取學生的集合,如下:

  RxHttp.get("/service/...")
        .from(new SimpleParser<List<Student>>() {})
        .as(RxLife.asOnMain(this))  //感知生命週期,並在主執行緒回撥
        .subscribe(students -> { //這裡students 即為List<Student>物件
            //成功回撥
        }, throwable -> {
            //失敗回撥
        });
複製程式碼

可以看到,我們直接使用from操作符,並傳入我們new出來的SimpleParser物件,最後在觀察者就能拿到List<Student>物件。 到這,有讀者會有疑問,我們new出來的SimpleParser物件,為啥要使用匿名內部類呢?不使用不行嗎?可以肯定的回答不行。如果new SimpleParser<List<Student>>()這樣書寫,編譯器會報錯,為什麼呢?眼尖的你可能發現了,SimpleParser無參的構造方法是protected關鍵字修飾的,那為啥要用protected關鍵字修飾呢?因為不用protected關鍵字修飾,SimpleParser內部就拿不到泛型的具體型別,如果你再要問為什麼,那你就需要了解一些泛型了,這個跟Gson庫裡面的TypeToken類是同一個道理,可以檢視我的另一片文章Android、Java泛型掃盲

上面SimpleParser我們是通過匿名內部類new出來的,然後我們知道,內部類都會持有外部類的引用,如果外部類是一個Activity,就有可能會有記憶體洩漏的危險(如果使用了RxLife就不會有這種危險),而且,這種寫法本人也不是很喜歡。為此,有沒有什麼辦法來避免此類問題呢?

有,那就是通過ListParser解析器

ListParser

ListParser的作用是,將Http返回的結果,用Json解析成List<T>物件,原始碼如下:

在這裡插入圖片描述
程式碼跟SimpleParser差不多,不在詳細講解。不同的是這裡使用了ParameterizedTypeImpl類來處理泛型,這個類的用法及原理,也檢視我的另一片文章Android、Java泛型掃盲

我們直接看看通過ListParser如何拿到List<T>物件,如下

  RxHttp.get("/service/...")
        .fromListParser(Student.class)
        .as(RxLife.asOnMain(this))  //感知生命週期,並在主執行緒回撥
        .subscribe(students -> {    //這裡students 即為List<Student>物件
            //成功回撥
        }, throwable -> {
            //失敗回撥
        });
複製程式碼

可以看到,直接使用fromListParser操作符,傳入Student.class即可,它內部就是通過ListParser.get(Student.class)獲取的ListParser物件。

接下來我們看看RxHttp提供的最後一個解析器DownloadParser

DownloadParser

DownloadParser的作用是將Http返回的輸入流寫到檔案中,即檔案下載

在這裡插入圖片描述
這個好理解,就不仔細講解了,有一點要的說的,此解析器是支援斷點下載,我們來看看如何實現斷點下載,並且帶進度回撥

//斷點下載,帶進度
public void breakpointDownloadAndProgress() {
    String destPath = getExternalCacheDir() + "/" + "Miaobo.apk";
    File file = new File(destPath);
    long length = file.length();
    RxHttp.get("http://update.9158.com/miaolive/Miaolive.apk")
            //如果檔案存在,則新增 RANGE 頭資訊 ,以支援斷點下載
            .addHeader("RANGE", "bytes=" + length + "-", length > 0)
            .downloadProgress(destPath)
            .map(progress -> {
                if (length > 0) {//增加上次已經下載好的位元組數
                    progress.addCurrentSize(length);
                    progress.addTotalSize(length);
                    progress.updateProgress();
                }
                return progress;
            })
            .observeOn(AndroidSchedulers.mainThread()) //主執行緒回撥
            .doOnNext(progress -> {
                //下載進度回撥,0-100,僅在進度有更新時才會回撥
                int currentProgress = progress.getProgress(); //當前進度 0-100
                long currentSize = progress.getCurrentSize(); //當前已下載的位元組大小
                long totalSize = progress.getTotalSize();     //要下載的總位元組大小
            })
            .filter(Progress::isCompleted)//過濾事件,下載完成,才繼續往下走
            .map(Progress::getResult) //到這,說明下載完成,拿到Http返回結果並繼續往下走
            .as(RxLife.asOnMain(this)) //加入感知生命週期的觀察者
            .subscribe(s -> { //s為String型別
                //下載成功,處理相關邏輯
            }, throwable -> {
                //下載失敗,處理相關邏輯
            });
}
複製程式碼

跟帶進度回撥的下載程式碼差不多,上面也有註釋,就不在講解了。

自定義解析器

在上面的介紹的3個解析中,SimpleParser可以說是萬能的,任何資料結構,只要你建好對應的Bean類,都能夠正確解析,就是要我們去建n個Bean類,甚至這些Bean類,可能很多都是可以抽象化的。例如,大部分Http返回的資料結構都可以抽象成下面的Bean類

public class Data<T> {
    private int    code;
    private String msg;
    private T      data;
    //這裡省略get、set方法
}
複製程式碼

假設,Data裡面的T是一個學生物件,我們要拿到此學生資訊,就可以這麼做

  RxHttp.get(http://www.......) //這裡get,代表Get請求
        .from(new SimpleParser<Data<Student>>() {}) //這裡泛型傳入Data<Student>
        .observeOn(AndroidSchedulers.mainThread()) //主執行緒回撥
        .map(Data::getData) //通過map操作符獲取Data裡面的data欄位
        .as(RxLife.asOnMain(this))
        .subscribe(student -> {
            //這裡的student,即Data裡面的data欄位內容
        }, throwable -> {
            //Http請求出現異常
        });
複製程式碼

以上程式碼有3個缺點

  • 還是通過SimpleParse匿名內部類實現的,前面說過,這種方式有可能造成記憶體洩漏,而且寫法上不是很優雅
  • 下游首先拿到的是一個Data<Student>物件,隨後使用map操作符從Data<Student>拿到Student物件傳給下游觀察者
  • 沒法統一對Data裡面的code欄位做驗證

DataParser

那麼有什麼優雅的辦法解決呢?答案就是自定義解析器。我們來定一個DataParser解析器,如下:

在這裡插入圖片描述
程式碼跟SimpleParser類差不多,好處如下

  • DataParser自動為我們做了一層過濾,我們可以直接拿到T物件,而不再使用map操作符了
  • 內部可以對code欄位做統一判斷,根據不同的code,丟擲不同的異常,做到統一的錯誤處理機制(這裡丟擲的異常會被下游的onError觀察者接收)
  • 當codo正確時,就代表了資料正確,下游的onNext觀察者就能收到事件
  • 避免了使用匿名內部類

此時,我們就可以如下實現:

  RxHttp.get("http://www...") //這裡get,代表Get請求
        .fromDataParser(Student.class)  //此方法是通過註解生成的
        .as(RxLife.asOnMain(this))
        .subscribe(student -> {
            //這裡的student,即Data裡面的data欄位內容
        }, throwable -> {
            //Http請求出現異常
            String msg = throwable.getMessage(); //Data裡面的msg欄位或者其他異常資訊
            String code = throwable.getLocalizedMessage(); //Data裡面的code欄位,如果有傳入的話
        });
複製程式碼

注:我們在定義DataParser時,使用了註解@Parser(name = "DataParser"),故在RxHttp類裡有fromDataParser方法,註解使用請檢視RxHttp 擴充套件篇之註解處理器 Generated API(七)

然後,如果Data裡面T是一個List<T>又該怎麼辦呢?我們也許可以這樣:

  RxHttp.get("http://www...") //這裡get,代表Get請求
        .from(new DataParser<List<Student>>() {})  
        .as(RxLife.asOnMain(this))
        .subscribe(students -> {
            //這裡的students,為List<Student>物件
        }, throwable -> {
            //Http請求出現異常
            String msg = throwable.getMessage(); //Data裡面的msg欄位或者其他異常資訊
            String code = throwable.getLocalizedMessage(); //Data裡面的code欄位,如果有傳入的話
        });
複製程式碼

又是通過匿名內部類實現的,心累,有沒有更優雅的方式?有,還是自定義解析器,我們來定義一個DataListParser解析器

DataListParser

在這裡插入圖片描述
程式碼都差不多,就不在講解了,直接看怎麼用:

  RxHttp.get("http://www...") //這裡get,代表Get請求
        .fromDataListParser(Student.class)  //此方法是通過註解生成的
        .as(RxLife.asOnMain(this))
        .subscribe(students -> {
            //這裡的students,為List<Student>物件
        }, throwable -> {
            //Http請求出現異常
            String msg = throwable.getMessage(); //Data裡面的msg欄位或者其他異常資訊
            String code = throwable.getLocalizedMessage(); //Data裡面的code欄位,如果有傳入的話
        });
複製程式碼

注: fromDataListParser方法也是通過註解生成的 我們最後來看一個問題

{
    "code": 0,
    "msg": "",
    "data": {
        "totalPage": 0,
        "list": []
    }
}
複製程式碼

這種資料,我們又該如何解析呢?首先,我們再定一個Bean類叫PageList,如下:

public class PageList<T> {
    private int     totalPage;
    private List<T> list;
    //省略get/set方法
}
複製程式碼

此Bean類,對於的是data欄位的資料結構,機智的你肯定馬上想到了用DataParser如何實現,如下:

  RxHttp.get("http://www...") //這裡get,代表Get請求
        .from(new DataParser<PageList<Student>>(){})
        .as(RxLife.asOnMain(this))
        .subscribe(pageList -> {
            //這裡的pageList,即為PageList<Student>型別
        }, throwable -> {
            //Http請求出現異常
            String msg = throwable.getMessage(); //Data裡面的msg欄位或者其他異常資訊
            String code = throwable.getLocalizedMessage(); //Data裡面的code欄位,如果有傳入的話
        });
    }
複製程式碼

好吧,又是匿名內部類,還是乖乖自定義解析器吧。我們定義一個DataPageListParser解析器,如下:

DataPageListParser

在這裡插入圖片描述
繼續看看怎麼用

  RxHttp.get("http://www...") //這裡get,代表Get請求
        .fromDataPageListParser(Student.class)  //此方法是通過註解生成的
        .as(RxLife.asOnMain(this))
        .subscribe(pageList -> {
            //這裡的pageList,即為PageList<Student>型別
        }, throwable -> {
            //Http請求出現異常
            String msg = throwable.getMessage(); //Data裡面的msg欄位或者其他異常資訊
            String code = throwable.getLocalizedMessage(); //Data裡面的code欄位,如果有傳入的話
        });
複製程式碼

注: fromDataPageListParser方法依然是通過註解生成的。

到這,仔細觀察你會發現,我們定一個三個解析器DataParser、DataListParser及DataPageListParser,程式碼其實都差不多,用法也差不多,無非就輸出的不一樣。

小結

本篇文章,給大家介紹了RxHttp自定的三個解析器SimpleParserListParserDownloadParser他們的用法及內部實現,後面又對常見的資料結構帶領大家自定義了3個解析器,分別是DataParser、DataListParser及DataPageListParser,相信有了這個6個解析器,就能應對大多數場景了。如果還有場景不能實現,看完本篇文章自定義解析對你來說也是非常容易的事情了。

自問: 為啥不將自定義的三個解析器DataParser、DataListParser及DataPageListParser封裝進RxHttp? 自答: 因為這3個解析器都涉及到了具體的業務需求,每個開發者的業務邏輯都可能不一樣,故不能封裝進RxHttp庫裡。

最後,本文如果有寫的不對的地方,請廣大讀者指出。 如果覺得我寫的不錯,記得給我點贊RxHttp

更過詳情請檢視RxHttp系列其它文章

RxHttp 一條鏈傳送請求之強大的Param類(三)

RxHttp 一條鏈傳送請求之註解處理器 Generated API(四)

轉載請註明出處,謝謝?

相關文章