配置Retrofit網路框架及其使用

追風z發表於2020-10-21

**

1.配置service介面

網路請求需要哪些資訊?

**
一般網路請求,會需要如下這些資訊:

請求的網址
請求方式;是GET請求,還是POST請求
請求引數
引數傳遞方式;是通過表單方式傳遞,還是通過JSON方式傳遞
請求頭
如何配置?
將這些資訊寫到一個介面中。

建立Model

/**
 * 歌單詳情包裹物件
 * <p>
 * 只是用來測試
 */
public class SheetDetailWrapper {

    /**
     * 歌單詳情
     */
    private Sheet data;

    public Sheet getData() {
        return data;
    }

    public void setData(Sheet data) {
        this.data = data;
    }
}
/**
 * 歌單物件
 */
public class Sheet {
    /**
     * 歌單Id
     */
    private String id;//這個用字串型別,防止以後id變為字串了,不好搞
    /**
     * 歌單標題
     */
    private String title;
    /**
     * 歌單封面
     */
    private String banner;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getBanner() {
        return banner;
    }

    public void setBanner(String banner) {
        this.banner = banner;
    }
}

{
"data": {
	"id": 1,
	"title": "這世上所有的歌zheshishangtest",
	"banner": "assets/list1.jpg",
	"description": "這是因為iOS9引入了新特性App Transport Security (ATS),他要求App內網路請求必須使用HTTPS協議。解決方法是要麼改為HTTPS,要麼宣告可以使用HTTP,可以宣告部分使用HTTP,也可以所有;但需要說明的是如果APP內所有請求都是HTTP,那麼如果要上架App Store的時候基本都會被拒。",
	"clicks_count": 16773,
	"collections_count": 28,
	"comments_count": 172,
"user": {
	"id": 1,
	"nickname": "愛學啊dev666",
	"avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
	"gender": 0
},
"songs": [
	{
		"id": 11,
		"title": "忘不了的溫柔",
		"banner": "assets/yuanfengredehuo_andongyang.jpg",
		"uri": "assets/wangbiliaodewenrou_andongyang.mp3",
		"clicks_count": 0,
		"comments_count": 0,
		"created_at": "2019-09-17T05:52:50.000Z",
		"user": {
		"id": 1,
		"nickname": "愛學啊dev666",
		"avatar": "67133b479b364e8c9bfceb58015cd71f.jpg"
		},
		"singer": {
			"id": 43,
			"nickname": "安東陽",
			"avatar": "assets/andongyang.jpg"
		}
	},
	{
		"id": 10,
		"title": "傷心的站臺",
		"banner": "assets/yuanfengredehuo_andongyang.jpg",
		"uri": "assets/shangxingzhantai_andongyang.mp3",
		"clicks_count": 0,
		"comments_count": 0,
		"created_at": "2019-09-17T05:50:50.000Z",
		"user": {
		"id": 1,
		"nickname": "愛學啊dev666",
		"avatar": "67133b479b364e8c9bfceb58015cd71f.jpg"
		},
		"singer": {
			"id": 43,
			"nickname": "安東陽",
			"avatar": "assets/andongyang.jpg"
		}
		}
]
}

建立Service介面

我們希望將專案中,所有請求的資訊都放到Service介面中,名稱可以隨便寫,每個方法,就代表一個介面,定義是歌單詳情,只是我們這裡用這種命名方式。

/**
 * 網路介面配置
 * <p>
 * 之所以呼叫介面還能返回資料
 * 是因為Retrofit框架內部處理了
 * 這裡不講解原理
 * 在《詳解Retrofit》課程中講解
 */
public interface Service {

    /**
     * 歌單詳情
     *
     * @param id {id} 這樣表示id,表示的@Path("id")裡面的id,
     *           path裡面的id其實就是接收後面String id 的值
     *           <p>
     *           一般情況下,三個名稱都寫成一樣,比如3個都是id
     *           <p>
     *           //Retrofit如何知道我們傳入的是id呢,其實通過Retrofit註解@Path("id")知道
     *           (應該是相等於限定了id,其他的應該會報錯)
     *           <p>
     *           Observable<SheetDetailWrapper>:相等於把json資料轉換成這個SheetDetailWrapper型別的物件
     *           Observable:rxjava裡面的類
     */
    @GET("v1/sheets/{id}")
    Observable<SheetDetailWrapper> sheetDetail(@Path("id") String id);
}

如果需要更多介面,只需要在這裡新增就行了;這裡暫時只新增了一個介面,目的是後面封裝網路框架的時候會用到,其他的介面,用到了在新增。

2.如何使用Retrofit請求網路

 //測試網路請求

        OkHttpClient.Builder okhttpClientBuilder = new OkHttpClient.Builder();
        //構建者模式
        //初始化Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                //讓Retrofit使用okhttp
                .client(okhttpClientBuilder.build())
                //api地址
                .baseUrl(Constant.ENDPOINT)//這裡使用的是地址的公共字首
                //適配Rxjava(就是所返回的物件以Rxjava這種方式來工作(比如我們使用了Observable這種方式,介面Service檢視))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                //使用gson解析json
                //包括請求引數和響應
                // (比如使用Retrofit請求框架請求資料,傳送物件,也會轉換成json(使用gson轉換))
                .addConverterFactory(GsonConverterFactory.create())
                //建立Retrofit
                .build();

        //建立Service
        Service service = retrofit.create(Service.class);

        //請求歌單詳情
        service.sheetDetail("1")
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread())//在主執行緒觀察(操作UI在主執行緒)
                //介面方法裡面對應的物件:SheetDetailWrapper
                .subscribe(new Observer<SheetDetailWrapper>() {//訂閱回來的資料
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    /**
                     * 請求成功
                     *
                     * @param sheetDetailWrapper 解析回來的物件
                     */
                    @Override
                    public void onNext(SheetDetailWrapper sheetDetailWrapper) {
                        LogUtil.d(TAG, "request sheet detail success:" + sheetDetailWrapper.getData().getTitle());

                    }

                    /**
                     * 請求失敗
                     *
                     * @param e Error
                     */
                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
//                        LogUtil.d(TAG,"request sheet detail failed:" + e.getLocalizedMessage());
                    }

                    /**
                     * 請求結束
                     */
                    @Override
                    public void onComplete() {

                    }
                });

測試
執行專案,點選按鈕,就可以在日誌中檢視到歌單為1的JSON資料了。

2.2 網路請求錯誤 處理

程式碼演示:

在onError方法可以判斷,這樣太麻煩,後面會封裝

  //判斷錯誤型別
   if (e instanceof UnknownHostException) {
       ToastUtil.errorShortToast(R.string.error_network_unknown_host);
   } else if (e instanceof ConnectException) {
       ToastUtil.errorShortToast(R.string.error_network_connect);
   } else if (e instanceof SocketTimeoutException) {
       ToastUtil.errorShortToast(R.string.error_network_timeout);
   } else if (e instanceof HttpException) {
       HttpException exception = (HttpException) e;
       int code = exception.code();
       if (code == 401) {
           ToastUtil.errorShortToast(R.string.error_network_not_auth);
       } else if (code == 403) {
           ToastUtil.errorShortToast(R.string.error_network_not_permission);
       } else if (code == 404) {
           ToastUtil.errorShortToast(R.string.error_network_not_found);
       } else if (code >= 500) {
           ToastUtil.errorShortToast(R.string.error_network_server);
       } else {
           ToastUtil.errorShortToast(R.string.error_network_unknown);
       }
   } else {
       ToastUtil.errorShortToast(R.string.error_network_unknown);
   }

測試錯誤(404和500)

因為我們上面程式碼判斷了,所以手機上報錯404和500會爆出相應的提示。
為了方便,我們電腦上先檢視下網路狀態,然後再 執行到手機app上檢視網路狀態


如果要404錯誤,只需要把http://dev-my-cloud-music-api-rails.ixuea.com/v1/sheets中的sheets改下,就變成404   (把URL地址更改為一個不存在的地址;就會提示“你訪問內容不存在!”。)
如果要500錯誤,只要使用者名稱不存在就會變成500錯誤(使用者名稱不存在就會報500錯誤)
比如:
http://dev-my-cloud-music-api-rails.ixuea.com/v1/users/-1?nickname=11111111這個地址就是500錯誤

開啟這個檢查介面,然後輸入網址就可以捕獲到網路狀態
在這裡插入圖片描述

在這裡插入圖片描述

測試無網路

我們這裡使用的是模擬器,所以關閉電腦WiFi就可以模擬;如果是真實手機,也可以關閉WiFi;測試會發現,關閉網路,會產生一個UnknownHostException異常,所以在這裡程式碼判斷就行了。

2.3 封裝

1. 封裝網路請求Api

可以把初始化okhttp,初始化retrofit,還要建立Service放到一個單獨的類中,然後把這個類,實現為單例,因為前面的這些初始化,只需要執行一次就行了。

Api 類


public class Api {
    /**
     * Api單例欄位
     */
    private static Api instance;
    /**
     * Service單例
     */
    private final Service service;

    /**
     * 返回當前物件的唯一例項
     * <p>
     * 單例設計模式
     * 由於移動端很少有高併發
     * 所以這個就是簡單判斷
     *
     * @return 本類單例
     */
    public static Api getInstance() {
        if (instance == null) {
            instance = new Api();
        }
        return instance;
    }


    /**
     * 私有構造方法
     */
    private Api() {
        OkHttpClient.Builder okhttpClientBuilder = new OkHttpClient.Builder();
        //構建者模式
        //初始化Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                //讓Retrofit使用okhttp
                .client(okhttpClientBuilder.build())
                //api地址
                .baseUrl(Constant.ENDPOINT)//這裡使用的是地址的公共字首
                //適配Rxjava(就是所返回的物件以Rxjava這種方式來工作(比如我們使用了Observable這種方式,介面Service檢視))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                //使用gson解析json
                //包括請求引數和響應
                // (比如使用Retrofit請求框架請求資料,傳送物件,也會轉換成json(使用gson轉換))
                .addConverterFactory(GsonConverterFactory.create())
                //建立Retrofit
                .build();

        //建立Service
        service = retrofit.create(Service.class);
    }

    /**
     * 歌單詳情
     *
     * @param id 傳入的第幾個歌曲Id
     * @return 返回Retrofit介面例項 裡面的方法返回的物件
     */
    public Observable<SheetDetailWrapper> sheetDetail(String id) {
        return service.sheetDetail(id)
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread());//在主執行緒觀察(操作UI在主執行緒)
    }

2. 使用

把之前的service去掉,然後用這個Api物件呼叫裡面的方法即可

  //請求歌單詳情
//        service.sheetDetail("1")
        Api.getInstance().sheetDetail("1")
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread())//在主執行緒觀察(操作UI在主執行緒)
                //介面方法裡面對應的物件:SheetDetailWrapper
                .subscribe(new Observer<SheetDetailWrapper>() {//訂閱回來的資料
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    /**
                     * 請求成功
                     *
                     * @param sheetDetailWrapper 解析回來的物件
                     */
                    @Override
                    public void onNext(SheetDetailWrapper sheetDetailWrapper) {
                        LogUtil.d(TAG, "request sheet detail success:" + sheetDetailWrapper.getData().getTitle());

                    }

                    /**
                     * 請求失敗
                     *
                     * @param e Error
                     */
                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
//                        LogUtil.d(TAG,"request sheet detail failed:" + e.getLocalizedMessage());

                        //判斷錯誤型別
                        if (e instanceof UnknownHostException) {
                            ToastUtil.errorShortToast(R.string.error_network_unknown_host);
                        } else if (e instanceof ConnectException) {
                            ToastUtil.errorShortToast(R.string.error_network_connect);
                        } else if (e instanceof SocketTimeoutException) {
                            ToastUtil.errorShortToast(R.string.error_network_timeout);
                        } else if (e instanceof HttpException) {
                            HttpException exception = (HttpException) e;
                            int code = exception.code();
                            if (code == 401) {
                                ToastUtil.errorShortToast(R.string.error_network_not_auth);
                            } else if (code == 403) {
                                ToastUtil.errorShortToast(R.string.error_network_not_permission);
                            } else if (code == 404) {
                                ToastUtil.errorShortToast(R.string.error_network_not_found);
                            } else if (code >= 500) {
                                ToastUtil.errorShortToast(R.string.error_network_server);
                            } else {
                                ToastUtil.errorShortToast(R.string.error_network_unknown);
                            }
                        } else {
                            ToastUtil.errorShortToast(R.string.error_network_unknown);
                        }
                    }

                    /**
                     * 請求結束
                     */
                    @Override
                    public void onComplete() {

                    }
                });

2.4 載入對話方塊

public class LoadingUtil {
    private static ProgressDialog progressDialog;

    /**
     * 使用一個載入對話方塊,使用預設提示文字
     *
     * @param activity Activity
     */
    public static void showLoading(Activity activity) {
        showLoading(activity, activity.getString(R.string.loading));
    }

    /**
     * 顯示一個載入對話方塊(可以輸入任何的message)
     *
     * @param activity Activity
     * @param message  Message
     */
    private static void showLoading(Activity activity, String message) {

        //判斷activity為空或者已經銷燬了
        if (activity == null || activity.isFinishing()) {
            return;
        }

        //判斷是否顯示了
        if (progressDialog != null) {
            //已經顯示了 不需要再次顯示
            //就不再顯示了
            return;
        }

        //建立一個進度對話方塊
        progressDialog = new ProgressDialog(activity);
        progressDialog.setTitle("提示");//提示標題
        progressDialog.setMessage(message);//提示資訊
        //點選外部彈窗不會自動隱藏
        progressDialog.setCancelable(false);
        progressDialog.show();
    }

    /**
     * 隱藏載入提示對話方塊
     */
    public static void hideLoading() {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.hide();
            progressDialog = null;
        }
    }
}

如何使用

     //測試載入提示框
        LoadingUtil.showLoading(getMainActivity());

        //3秒中隱藏載入提示框
        //因顯示後無法點選後面的按鈕(也就是當前頁面點選的3s後關閉,在另一個頁面關閉麻煩)
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                LoadingUtil.hideLoading();
            }
        }, 3000);

完成網路請求載入顯示

前面已經學習了RxJava的回撥方法,所以可以在onSubscribe方法顯示載入提示
在onNext和onError方法中隱藏載入提示

        //請求歌單詳情
//        service.sheetDetail("1")
        Api.getInstance().sheetDetail("1")
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread())//在主執行緒觀察(操作UI在主執行緒)
                //介面方法裡面對應的物件:SheetDetailWrapper
                .subscribe(new Observer<SheetDetailWrapper>() {//訂閱回來的資料
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "onSubscribe: ");

                        //顯示載入對話方塊
                        LoadingUtil.showLoading(getMainActivity());
                    }

                    /**
                     * 請求成功
                     *
                     * @param sheetDetailWrapper 解析回來的物件
                     */
                    @Override
                    public void onNext(SheetDetailWrapper sheetDetailWrapper) {
                        LogUtil.d(TAG, "onNext:" + sheetDetailWrapper.getData().getTitle());

                        LoadingUtil.hideLoading();//隱藏載入提示框

                    }

                    /**
                     * 請求失敗
                     *
                     * @param e Error
                     */
                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError: ");
                        LoadingUtil.hideLoading();//隱藏載入提示框
                    }
                 }

測試

確認請求網路能顯示,並隱藏。

2.5 請求資料

2.5.1 json資料分析

{
    "data": [
        {
            "id": 1,
            "title": "這世上所有的歌zheshishangtest",
            "banner": "assets/list1.jpg",
            "clicks_count": 16795,
            "collections_count": 28,
            "comments_count": 172,
            "user": {
                "id": 1,
                "nickname": "愛學啊dev666",
                "avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
                "gender": 0
            }
        },
        {
            "id": 2,
            "title": "我向來做事十拿九不穩 不信可以試試woxianglaitest",
            "banner": "assets/list2.jpg",
            "clicks_count": 2099,
            "collections_count": 11,
            "comments_count": 2,
            "user": {
                "id": 1,
                "nickname": "愛學啊dev666",
                "avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
                "gender": 0
            }
        },
        {
            "id": 3,
            "title": "網路離別歌曲最後還是離開了wangllibietest",
            "banner": "assets/list3.jpg",
            "clicks_count": 4433,
            "collections_count": 10,
            "comments_count": 0,
            "user": {
                "id": 1,
                "nickname": "愛學啊dev666",
                "avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
                "gender": 0
            }
        },
        {
            "id": 9,
            "title": "傷心的人怎可願意聽慢歌",
            "banner": "assets/yuanfengredehuo_andongyang.jpg",
            "clicks_count": 620,
            "collections_count": 7,
            "comments_count": 4,
            "user": {
                "id": 1,
                "nickname": "愛學啊dev666",
                "avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
                "gender": 0
            }
        },
        {
            "id": 10,
            "title": "你開始懂得了歌詞",
            "banner": "assets/shengburusi.jpg",
            "clicks_count": 772,
            "collections_count": 9,
            "comments_count": 0,
            "user": {
                "id": 1,
                "nickname": "愛學啊dev666",
                "avatar": "67133b479b364e8c9bfceb58015cd71f.jpg",
                "gender": 0
            }
        },
        {
            "id": 92,
            "title": "2020",
            "banner": null,
            "clicks_count": 15,
            "collections_count": 0,
            "comments_count": 0,
            "user": {
                "id": 622,
                "nickname": "凌鍇",
                "avatar": null,
                "gender": 0
            }
        },
        {
            "id": 91,
            "title": "最好聽的歌單",
            "banner": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3489003925,2338759571&fm=26&gp=0.jpg",
            "clicks_count": 120,
            "collections_count": 1,
            "comments_count": 0,
            "user": {
                "id": 516,
                "nickname": "樂天gg",
                "avatar": "7f71228986a340a8830cb778d2c9037b.jpg",
                "gender": 10
            }
        },
        {
            "id": 90,
            "title": "2020年度金曲",
            "banner": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3489003925,2338759571&fm=26&gp=0.jpg",
            "clicks_count": 41,
            "collections_count": 1,
            "comments_count": 0,
            "user": {
                "id": 516,
                "nickname": "樂天gg",
                "avatar": "7f71228986a340a8830cb778d2c9037b.jpg",
                "gender": 10
            }
        },
        {
            "id": 89,
            "title": "最好聽的粵語歌",
            "banner": "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3489003925,2338759571&fm=26&gp=0.jpg",
            "clicks_count": 29,
            "collections_count": 1,
            "comments_count": 0,
            "user": {
                "id": 516,
                "nickname": "樂天gg",
                "avatar": "7f71228986a340a8830cb778d2c9037b.jpg",
                "gender": 10
            }
        },
        {
            "id": 88,
            "title": "sfsfdx",
            "banner": null,
            "clicks_count": 20,
            "collections_count": 0,
            "comments_count": 0,
            "user": {
                "id": 118,
                "nickname": "阿健",
                "avatar": "95a189d10eb94da4b795585af183c774.jpg",
                "gender": 10
            }
        }
    ]
}

分析:

可以當成一個大物件(也就是外圍是一個類),然後物件裡面的成員變數又是一個物件的話,按照物件的思維去解析;否則直接在大物件的類裡面直接新增一個成員變數即可。

我們先摺疊json資料
在這裡插入圖片描述
我們可以建立一個類

public class SheetListWrapper {
  
}

依次張開
在這裡插入圖片描述
這個data是個陣列(當做集合處理),這個大物件裡面的data成員變數是一個集合。

那集合裡面的item又是一個物件,所以我們又得定義一個item物件Sheet(這個之前定義的)

//public class Sheet extends BaseModel {
public class Sheet extends BaseMultiItemEntity {
    //id可以刪除了,因為已經定義到父類了
//    /**
//     * 歌單Id
//     */
//    private String id;//這個用字串型別,防止以後id變為字串了,不好搞
    /**
     * 歌單標題
     */
    private String title;
    /**
     * 歌單封面
     */
    private String banner;

    /**
     * 描述
     */
    private String description;
}

我們依次展開
可以看到
在這裡插入圖片描述

所以最總的

這裡記得把set get方法加上

public class SheetListWrapper {
    /**
     * 歌單列表
     */
    private List<Sheet> data;

    public List<Sheet> getData() {
        return data;
    }

    public void setData(List<Sheet> data) {
        this.data = data;
    }
}

請求單個item資料的model 方法同上。
這裡順便附上程式碼

/**
 * 歌單詳情包裹物件
 * <p>
 * 只是用來測試
 */
public class SheetDetailWrapper {

    /**
     * 歌單詳情
     */
    private Sheet data;

    //這裡返回的是單個歌單Sheet,//SheetListWrapper那邊返回的是多個歌單,也就是list,
    public Sheet getData() {
        return data;
    }

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

2.5.2 請求資料並簡單使用

1.建立模型資料,前面已經分析了

/**
 * 歌單詳情包裹物件
 * <p>
 * 只是用來測試
 */
 //這裡類這裡還沒有用到,先寫上  這個是請求單個歌單詳情時候用到的model類
public class SheetDetailWrapper { 

    /**
     * 歌單詳情
     */
    private Sheet data;

    //這裡返回的是單個歌單Sheet,//SheetListWrapper那邊返回的是多個歌單,也就是list,
    public Sheet getData() {
        return data;
    }

    public void setData(Sheet data) {
        this.data = data;
    }
}
/**
 * 歌單列表模型
 * <p>
 * 只是用來測試
 * <p>
 * 地址:http://dev-my-cloud-music-api-rails.ixuea.com/v1/sheets
 * 後面沒有數字,有數字的是具體某個歌單
 */
public class SheetListWrapper {
    /**
     * 歌單列表
     */
    private List<Sheet> data;

    public List<Sheet> getData() {
        return data;
    }

    public void setData(List<Sheet> data) {
        this.data = data;
    }
}

item裡面的model類

//public class Sheet extends BaseModel {
public class Sheet extends BaseMultiItemEntity {
    //id可以刪除了,因為已經定義到父類了
//    /**
//     * 歌單Id
//     */
//    private String id;//這個用字串型別,防止以後id變為字串了,不好搞
    /**
     * 歌單標題
     */
    private String title;
    /**
     * 歌單封面
     */
    private String banner;

    /**
     * 描述
     */
    private String description;
}

2.介面配置

/**
 * 網路介面配置
 * <p>
 * 之所以呼叫介面還能返回資料
 * 是因為Retrofit框架內部處理了
 * 這裡不講解原理
 * 在《詳解Retrofit》課程中講解
 */
public interface Service {

    /**
     * 歌單列表
     */
    @GET("v1/sheets")
    Observable<SheetListWrapper> sheets();
}

Api類

public class Api {
      
     ....
     
    /**
     * 歌單列表
     *
     * @return 返回Observable<SheetListWrapper>
     */
    public Observable<SheetListWrapper> sheets() {
        return service.sheets()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

3.使用

        //請求歌單列表
        Api.getInstance()
                .sheets()
                .subscribe(new Observer<SheetListWrapper>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(SheetListWrapper sheetListWrapper) {
                        LogUtil.d(TAG, "onNext:" + sheetListWrapper.getData().size());
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });
   }

2.5.3 封裝請求資料Model

前面我們也說了,專案中所有的網路,請求最外層都有一層包裝,真實的資料在data裡面,如果是詳情,data就是一個物件,如果是列表,data就是一個陣列;前面的解析歌單詳情的時候,還要給歌單詳情外面建立一個包裝類,那如果其他物件也按照這種方式實現的話,第一個是要建立很多的包裝類,同時外面的包裝類是重複的,所以說,可以建立一個通用的包裝類,通過泛型的方式指定裡面的內容

1.如何實現封裝

前面看到每個網路請求,最外層都有可能有,message,status兩個欄位,他們是必要的時候才有,還有一個data欄位,只是不同的介面,型別不一樣;所以我們可以建立一個BaseResponse。

建立BaseResponse

/**
 * 通用網路請求響應模型
 * <p>
 * 前面看到每個網路請求,最外層都有可能有,message,status兩個欄位,他們是必要的時候才有,
 * 還有一個data欄位,只是不同的介面,型別不一樣;所以我們可以建立一個BaseResponse。
 */
public class BaseResponse {

    /**
     * 狀態碼
     * <p>
     * 只有發生了錯誤才會有
     * <p>
     * 如果用int型別的話,全域性變數會預設初始化,這個值就預設為0了
     * 而我們不想為0,讓發生了錯誤的時候才會值(不想有值,出錯了才有值)
     * 所以我們這裡定義為引用型別Integer,就會預設初始化為null
     */
    private Integer status;

    /**
     * 出錯的提示資訊
     * <p>
     * 發生了錯誤不一定有
     */
    private String message;

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

建立詳情物件
建立一個DetailResponse,它用來解析詳情這類網路請求。

/**
 * 詳情網路請求解析類
 * <p>
 * 繼承BaseResponse
 * 定義了一個泛型T
 */
public class DetailResponse<T> extends BaseResponse {
    /**
     * 真實資料
     * 他的型別就是泛型
     */
    private T data;

    public T getData() {
        return data;
    }

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

前面建立了BaseResponse,也實現了詳情網路請求的包裝,那對應列表請求來說,其實也只有data不一樣,所以可以建立一個ListResponse。

/**
 * 解析列表網路請求
 */
public class ListResponse<T> extends BaseResponse {

    /**
     * 定義一個列表
     * <p>
     * 裡面的物件使用了泛型
     */
    private List<T> data;//名字要和伺服器端的一樣

    public List<T> getData() {
        return data;
    }

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

2.使用封裝後請求

2.1單個物件詳情使用

統一將裡面的model類改成 DetailResponse

 * <p>
 * 之所以呼叫介面還能返回資料
 * 是因為Retrofit框架內部處理了
 * 這裡不講解原理
 * 在《詳解Retrofit》課程中講解
 */
public interface Service {

    /**
     * 歌單詳情
     *
     * @param id {id} 這樣表示id,表示的@Path("id")裡面的id,
     *           path裡面的id其實就是接收後面String id 的值
     *           <p>
     *           一般情況下,三個名稱都寫成一樣,比如3個都是id
     *           <p>
     *           //Retrofit如何知道我們傳入的是id呢,其實通過Retrofit註解@Path("id")知道
     *           (應該是相等於限定了id,其他的應該會報錯)
     *           <p>
     *           Observable<SheetDetailWrapper>:相等於把json資料轉換成這個SheetDetailWrapper型別的物件
     *           Observable:rxjava裡面的類
     */
    @GET("v1/sheets/{id}")
//    Observable<SheetDetailWrapper> sheetDetail(@Path("id") String id);
    Observable<DetailResponse<Sheet>> sheetDetail(@Path("id") String id);

}
    /**
     * 歌單詳情
     *
     * @param id 傳入的第幾個歌曲Id
     * @return 返回Retrofit介面例項 裡面的方法返回的物件
     */
    public Observable<DetailResponse<Sheet>> sheetDetail(String id) {
        return service.sheetDetail(id)
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread());//在主執行緒觀察(操作UI在主執行緒)
    }
 //請求DetailResponse歌單詳情(封裝介面後使用)
        Api.getInstance().sheetDetail("1")
                .subscribe(new Observer<DetailResponse<Sheet>>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(DetailResponse<Sheet> sheetDetailResponse) {
                        LogUtil.d(TAG, "onNext:" + sheetDetailResponse.getData().getTitle());
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

2.2 列表使用

類似的,使用也把Model類改成 ListResponse 即可。

最後還要測試能正常的執行。

2.5.4 封裝網路請求回撥

如果大家不知道從和下手,還是按照我們前面的方法,從使用的位置,也就是我們期望封裝完成後的效果開始。

請求網路的時候先通過Api的getInstance方法,獲取到Api類,然後呼叫相應的方法,他返回的是Observable物件,然後重寫onSucceeded只關注成功,通過onFailed關注失敗。

1.實現通用Observer回撥

可以看到現在使用Observer的時候,都需要實現全部方法,這樣每次使用的時候比較麻煩,所以可以借鑑Java中的設計,就是給介面建立一個預設實現類,這個類只是簡單的實現這些方法。

/**
 * 通用實現Observer裡面的方法
 * <p>
 * 目的是避免要實現所有方法
 */
public class ObserverAdapter<T> implements Observer<T> {

    /**
     * 開始訂閱了執行(可以簡單理解為開始執行前)
     *
     * @param d Disposable物件
     */
    @Override
    public void onSubscribe(Disposable d) {

    }

    /**
     * 下一個Observer(當前Observer執行完成了)
     *
     * @param t 具體的物件或者集合
     */
    @Override
    public void onNext(T t) {

    }

    /**
     * 發生了錯誤(執行失敗了)
     *
     * @param e Throwable物件
     */
    @Override
    public void onError(Throwable e) {

    }

    /**
     * 回撥了onNext方法後呼叫
     */
    @Override
    public void onComplete() {

    }
}
 //使用ObserverAdapter
 Api.getInstance().sheetDetail("1")
         .subscribe(new ObserverAdapter<DetailResponse<Sheet>>() {
             @Override
             public void onNext(DetailResponse<Sheet> sheetDetailResponse) {
                 super.onNext(sheetDetailResponse);
                 LogUtil.d(TAG, "onNext:" + sheetDetailResponse.getData().getTitle());
             }
         });

可以看到只需要重寫需要的方法就行了。

最後確保能正確的執行

2.實現HttpObserver回撥

後面我們要實現自動網路錯誤處理,而這部分邏輯放到ObserverAdapter不太好,所以我們建立HttpObserver。
/**
 * 網路請求Observer
 * <p>
 * 有些錯誤不方便放在ObserverAdapter處理,所以定義了HttpObserver類這個是繼承ObserverAdapter<T>的
 * <p>
 * 本類有成功與失敗方法
 */
public abstract class HttpObserver<T> extends ObserverAdapter<T> {


    private static final String TAG = "HttpObserver";

    /**
     * 請求成功
     *
     * @param data 資料(物件或者集合)
     *             Succeeded:success  後面的2個s改成ed
     *             改成抽象類,讓子類實現
     */
    public abstract void onSucceeded(T data);

    /**
     * 請求失敗
     *
     * @param data 資料(物件或者集合) 比如傳入進來的:DetailResponse<Sheet>模型物件類
     * @param e    Throwable
     * @return boolean
     */
    public boolean onFailed(T data, Throwable e) {
        return false;
    }

    @Override
    public void onNext(T t) {
        super.onNext(t);
        LogUtil.d(TAG, "onNext:" + t);

        //TODO 處理錯誤

        //請求正常
        onSucceeded(t);
    }

    @Override
    public void onError(Throwable e) {
        super.onError(e);
        LogUtil.d(TAG, "onError:" + e.getLocalizedMessage());

        //TODO 處理錯誤
    }

}

使用:

//使用HttpObserver
Api.getInstance().sheetDetail("1")
        .subscribe(new HttpObserver<DetailResponse<Sheet>>() {
            @Override
            public void onSucceeded(DetailResponse<Sheet> data) {
                LogUtil.d(TAG, "onSucceeded:" + data.getData().getTitle());
            }
        });

確保能執行成功

2.2.加強(新增錯誤處理)

HttpObserver類

package com.ixuea.courses.mymusicold.listener;

import android.text.TextUtils;

import com.ixuea.courses.mymusicold.R;
import com.ixuea.courses.mymusicold.domain.response.BaseResponse;
import com.ixuea.courses.mymusicold.util.LogUtil;
import com.ixuea.courses.mymusicold.util.ToastUtil;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

import retrofit2.HttpException;

/**
 * 網路請求Observer
 * <p>
 * 有些錯誤不方便放在ObserverAdapter處理,所以定義了HttpObserver類這個是繼承ObserverAdapter<T>的
 * <p>
 * 本類有成功與失敗方法
 */
public abstract class HttpObserver<T> extends ObserverAdapter<T> {


    private static final String TAG = "HttpObserver";

    /**
     * 請求成功
     *
     * @param data 資料(物件或者集合)
     *             Succeeded:success  後面的2個s改成ed
     *             改成抽象類,讓子類實現
     */
    public abstract void onSucceeded(T data);

    /**
     * 請求失敗
     *
     * @param data 資料(物件或者集合)
     * @param e    Throwable
     * @return boolean
     */
    public boolean onFailed(T data, Throwable e) {
        return false;
    }

    /**
     * 如果要404錯誤,只需要把http://dev-my-cloud-music-api-rails.ixuea.com/v1/sheets中的sheets改下,就變成404
     * 如果要500錯誤,只要使用者名稱不存在就會變成500錯誤
     * 比如:
     * http://dev-my-cloud-music-api-rails.ixuea.com/v1/users/-1?nickname=11111111這個地址就是500錯誤
     * <p>
     * 總結:發生錯誤都會發生在onError中
     *
     * @param t 具體的物件或者集合
     */

    @Override
    public void onNext(T t) {
        super.onNext(t);
        LogUtil.d(TAG, "onNext:" + t);
        /**
         * 已經請求成功,但是登入失敗了
         * 但是如果使用者名稱 密碼錯誤會返回false
         *
         * 可以理解為200~299之間的值就會返回到這裡來
         * 這裡面的錯誤,可以先看看,到時候遇到再說
         */
        if (isSucceeded(t)) {
            //請求正常
            onSucceeded(t);
        } else {
            //請求出錯了(就是登入失敗了)
            requestErrorHandler(t, null);
        }

    }

    @Override
    public void onError(Throwable e) {
        super.onError(e);
        LogUtil.d(TAG, "onError:" + e.getLocalizedMessage());

        requestErrorHandler(null, e);//第一個引數為null,出錯了,沒有資料物件傳遞到這個方法裡面來
    }

    /**
     * 判斷網路請求是否成功
     *
     * @param t T
     * @return 是否成功
     */
    private boolean isSucceeded(T t) {
        //比如返回的code=200,(比如使用者名稱 密碼錯誤)
        if (t instanceof BaseResponse) {
            //判斷具體的業務請求是否成功
            BaseResponse baseResponse = (BaseResponse) t;

            //沒有狀態碼錶示成功
            //這是我們和服務端的一個規定(一般情況下status等於0才是成功,我們這裡是null才成功)
            //一般==null,則return true;否則return false
            return baseResponse.getStatus() == null;
        }
        return false;
    }

    /**
     * 處理錯誤網路請求
     *
     * @param data  T 資料模型物件
     * @param error Throwable錯誤物件
     */
    private void requestErrorHandler(T data, Throwable error) {
        if (error != null) {
            //判斷錯誤型別
            if (error instanceof UnknownHostException) {
                ToastUtil.errorShortToast(R.string.error_network_unknown_host);
            } else if (error instanceof ConnectException) {
                ToastUtil.errorShortToast(R.string.error_network_connect);
            } else if (error instanceof SocketTimeoutException) {
                ToastUtil.errorShortToast(R.string.error_network_timeout);
            } else if (error instanceof HttpException) {
                HttpException exception = (HttpException) error;
                int code = exception.code();
                if (code == 401) {
                    ToastUtil.errorShortToast(R.string.error_network_not_auth);
                } else if (code == 403) {
                    ToastUtil.errorShortToast(R.string.error_network_not_permission);
                } else if (code == 404) {
                    ToastUtil.errorShortToast(R.string.error_network_not_found);
                } else if (code >= 500) {
                    ToastUtil.errorShortToast(R.string.error_network_server);
                } else {
                    ToastUtil.errorShortToast(R.string.error_network_unknown);
                }
            } else {
                ToastUtil.errorShortToast(R.string.error_network_unknown);
            }
        } else {//error為null(這個時候是走onNext-->else-->requestErrorHandler)
            //(登入失敗的這種錯誤)
            if (data instanceof BaseResponse) {
                //判斷具體的業務請求是否成功
                BaseResponse response = (BaseResponse) data;
                if (TextUtils.isEmpty(response.getMessage())) {
                    //沒有錯誤提示資訊(message可能沒有錯誤提示資訊) (未知錯誤,請稍後再試!)
                    ToastUtil.errorShortToast(R.string.error_network_unknown);
                } else {//message不為空
                    ToastUtil.errorShortToast(response.getMessage());
                }
            }
        }


    }

}

2.2.1使用(程式碼實現404和500錯誤,看看是否會彈出錯誤提示)
public interface Service {
    /**
     * 歌單詳情
     *
     * @param id {id} 這樣表示id,表示的@Path("id")裡面的id,
     *           path裡面的id其實就是接收後面String id 的值
     *           <p>
     *           一般情況下,三個名稱都寫成一樣,比如3個都是id
     *           <p>
     *           //Retrofit如何知道我們傳入的是id呢,其實通過Retrofit註解@Path("id")知道
     *           (應該是相等於限定了id,其他的應該會報錯)
     *           <p>
     *           Observable<SheetDetailWrapper>:相等於把json資料轉換成這個SheetDetailWrapper型別的物件
     *           Observable:rxjava裡面的類
     */
//    @GET("v1/sheets11111111/{id}")//404
    @GET("v1/sheets/{id}")
//    Observable<SheetDetailWrapper> sheetDetail(@Path("id") String id);
    Observable<DetailResponse<Sheet>> sheetDetail(@Path("id") String id);

//模擬500(也就是)
    /**
     * 使用者詳情
     * //後面的查詢引數會自動新增到後面的
     * http://dev-my-cloud-music-api-rails.ixuea.com/v1/users/-1?nickname=11111111
     * 比如這裡嗎的 問號?和引數nickname=11111111會新增到後面
     * 因為這裡有個引數 @QueryMap Map<String,String> data
     */
    @GET("v1/users/{id}")
    Observable<DetailResponse<User>> userDetail(@Path("id") String id, @QueryMap Map<String, String> data);

}

上面Service介面中用到的User模型類,目前還沒有內容,因為我們這裡是測試錯誤。‘’

/**
 * 使用者詳情
 */
public class User {
}

API類中的

    /**
     * 歌單詳情
     *
     * @param id 傳入的第幾個歌曲Id
     * @return 返回Retrofit介面例項 裡面的方法返回的物件
     */
    public Observable<DetailResponse<Sheet>> sheetDetail(String id) {
        return service.sheetDetail(id)
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread());//在主執行緒觀察(操作UI在主執行緒)
    }

    /**
     * 歌單詳情
     *
     * @param id 傳入的第幾個歌曲Id
     * @return 返回Retrofit介面例項 裡面的方法返回的物件
     */
    public Observable<DetailResponse<User>> userDetail(String id, String nickname) {

        //新增查詢引數
        HashMap<String, String> data = new HashMap<>();

        if (StringUtils.isNotBlank(nickname)) {
            //如果暱稱不為空才新增
            // nickname=11111111  鍵Constant.NICKNAME對應nickname;  值nickname對應11111111
            data.put(Constant.NICKNAME, nickname);
        }

        return service.userDetail(id, data)
                .subscribeOn(Schedulers.io())//在子執行緒執行
                .observeOn(AndroidSchedulers.mainThread());//在主執行緒觀察(操作UI在主執行緒)
    }

在登入按鈕 點選事件裡面 測試

//        //使用HttpObserver 和 404
//        Api.getInstance().sheetDetail("1")
//                .subscribe(new HttpObserver<DetailResponse<Sheet>>() {
//                    @Override
//                    public void onSucceeded(DetailResponse<Sheet> data) {
//                        LogUtil.d(TAG, "onSucceeded:" + data.getData().getTitle());
//                    }
//                });

        //模擬500錯誤 使用者名稱錯誤
        Api.getInstance().userDetail("-1", "1111111111")
                .subscribe(new HttpObserver<DetailResponse<User>>() {
                    @Override
                    public void onSucceeded(DetailResponse<User> data) {
                        LogUtil.d(TAG, "onSucceeded:" + data.getData());
                    }
                });

最後確保能正確的執行。

2.3.手動處理錯誤(HttpObserver新增程式碼)

有些時候,我們可能希望自定義錯誤處理,而現在預設是,出錯了就在父類處理了。

如何實現?

可以使用onFailed方法的返回值來實現,可以這樣,如果返回true表示子類處理錯誤,如果返回false父類處理錯誤。

 //模擬500錯誤 使用者名稱錯誤
 Api.getInstance().userDetail("-1", "1111111111")
         .subscribe(new HttpObserver<DetailResponse<User>>() {
             @Override
             public void onSucceeded(DetailResponse<User> data) {
                 LogUtil.d(TAG, "onSucceeded:" + data.getData());
             }

             @Override
             public boolean onFailed(DetailResponse<User> data, Throwable e) {
                 LogUtil.d(TAG, "onFailed:" + e);

//                        return super.onFailed(data, e);//呼叫父類,內部處理錯誤

                 //return true 表示:手動處理錯誤
                 return true;//外部處理,(就是說內部的那個提示沒有彈出來)
             }
         });

HttpObserver 類

/**
 * 網路請求Observer
 * <p>
 * 有些錯誤不方便放在ObserverAdapter處理,所以定義了HttpObserver類這個是繼承ObserverAdapter<T>的
 * <p>
 * 本類有成功與失敗方法
 */
public abstract class HttpObserver<T> extends ObserverAdapter<T> {


    private static final String TAG = "HttpObserver";

    /**
     * 請求成功
     *
     * @param data 資料(物件或者集合)
     *             Succeeded:success  後面的2個s改成ed
     *             改成抽象類,讓子類實現
     */
    public abstract void onSucceeded(T data);

    /**
     * 請求失敗
     *
     * @param data 資料(物件或者集合)
     * @param e    Throwable
     * @return boolean false :表示父類(本類)要處理錯誤(內部處理);true:表示子類要處理錯誤(外部處理)
     */
    public boolean onFailed(T data, Throwable e) {
        return false;
    }

    /**
     * 如果要404錯誤,只需要把http://dev-my-cloud-music-api-rails.ixuea.com/v1/sheets中的sheets改下,就變成404
     * 如果要500錯誤,只要使用者名稱不存在就會變成500錯誤
     * 比如:
     * http://dev-my-cloud-music-api-rails.ixuea.com/v1/users/-1?nickname=11111111這個地址就是500錯誤
     * <p>
     * 總結:發生錯誤都會發生在onError中
     *
     *
     *   已經請求成功,但是登入失敗了
     *   但是如果使用者名稱 密碼錯誤會返回false
     *
     *   可以理解為200~299之間的值就會返回到這裡來
     *   這裡面的錯誤,可以先看看,到時候遇到再說
     * @param t 具體的物件或者集合
     */


    @Override
    public void onNext(T t) {
        super.onNext(t);
        LogUtil.d(TAG, "onNext:" + t);

        if (isSucceeded(t)) {
            //請求正常
            onSucceeded(t);
        } else {
            //請求出錯了(就是登入失敗了)
            requestErrorHandler(t, null);
        }

    }

    @Override
    public void onError(Throwable e) {
        super.onError(e);
        LogUtil.d(TAG, "onError:" + e.getLocalizedMessage());

        requestErrorHandler(null, e);//第一個引數為null,出錯了,沒有資料物件傳遞到這個方法裡面來
    }

    /**
     * 判斷網路請求是否成功
     *
     * @param t T
     * @return 是否成功
     */
    private boolean isSucceeded(T t) {
        //比如返回的code=200,(比如使用者名稱 密碼錯誤)
        if (t instanceof BaseResponse) {
            //判斷具體的業務請求是否成功
            BaseResponse baseResponse = (BaseResponse) t;

            //沒有狀態碼錶示成功
            //這是我們和服務端的一個規定(一般情況下status等於0才是成功,我們這裡是null才成功)
            //一般==null,則return true;否則return false
            return baseResponse.getStatus() == null;
        }
        return false;
    }

    /**
     * 處理錯誤網路請求
     *
     * @param data  T 資料模型物件
     * @param error Throwable錯誤物件
     */
    private void requestErrorHandler(T data, Throwable error) {
        if (onFailed(data, error)) {//fasle就會走else,父類(可以說本類)處理錯誤;true:就是外部處理錯誤
            //回撥了請求失敗方法
            //並且該方法返回了true

            //返回true就表示外部手動處理錯誤
            //那我們框架內部就不用做任何事情了
        } else {
            if (error != null) {
                //判斷錯誤型別
                if (error instanceof UnknownHostException) {
                    ToastUtil.errorShortToast(R.string.error_network_unknown_host);
                } else if (error instanceof ConnectException) {
                    ToastUtil.errorShortToast(R.string.error_network_connect);
                } else if (error instanceof SocketTimeoutException) {
                    ToastUtil.errorShortToast(R.string.error_network_timeout);
                } else if (error instanceof HttpException) {
                    HttpException exception = (HttpException) error;
                    int code = exception.code();
                    if (code == 401) {
                        ToastUtil.errorShortToast(R.string.error_network_not_auth);
                    } else if (code == 403) {
                        ToastUtil.errorShortToast(R.string.error_network_not_permission);
                    } else if (code == 404) {
                        ToastUtil.errorShortToast(R.string.error_network_not_found);
                    } else if (code >= 500) {
                        ToastUtil.errorShortToast(R.string.error_network_server);
                    } else {
                        ToastUtil.errorShortToast(R.string.error_network_unknown);
                    }
                } else {
                    ToastUtil.errorShortToast(R.string.error_network_unknown);
                }
            } else {//error為null(這個時候是走onNext-->else-->requestErrorHandler)
                //(登入失敗的這種錯誤)
                if (data instanceof BaseResponse) {
                    //判斷具體的業務請求是否成功
                    BaseResponse response = (BaseResponse) data;
                    if (TextUtils.isEmpty(response.getMessage())) {
                        //沒有錯誤提示資訊(message可能沒有錯誤提示資訊) (未知錯誤,請稍後再試!)
                        ToastUtil.errorShortToast(R.string.error_network_unknown);
                    } else {//message不為空
                        ToastUtil.errorShortToast(response.getMessage());
                    }
                }
            }
        }
        
    }

}

3.重構錯誤到工具類(HttpUtil)

HttpUtil 類


/**
 * 網路請求相關輔助方法
 */
public class HttpUtil {
    /**
     * 網路請求錯誤處理
     *
     * @param data  Object
     * @param error Throwable
     *              這個static後面的<T>是必須要的,否則引數那裡會找不到這個泛型T
     *              也可以不用泛型T,直接用Object
     */
//    public static <T> void handlerRequest(T data, Throwable error) {
    public static void handlerRequest(Object data, Throwable error) {
        if (error != null) {
            //判斷錯誤型別
            if (error instanceof UnknownHostException) {
                ToastUtil.errorShortToast(R.string.error_network_unknown_host);
            } else if (error instanceof ConnectException) {
                ToastUtil.errorShortToast(R.string.error_network_connect);
            } else if (error instanceof SocketTimeoutException) {
                ToastUtil.errorShortToast(R.string.error_network_timeout);
            } else if (error instanceof HttpException) {
                HttpException exception = (HttpException) error;
                int code = exception.code();
                if (code == 401) {
                    ToastUtil.errorShortToast(R.string.error_network_not_auth);
                } else if (code == 403) {
                    ToastUtil.errorShortToast(R.string.error_network_not_permission);
                } else if (code == 404) {
                    ToastUtil.errorShortToast(R.string.error_network_not_found);
                } else if (code >= 500) {
                    ToastUtil.errorShortToast(R.string.error_network_server);
                } else {
                    ToastUtil.errorShortToast(R.string.error_network_unknown);
                }
            } else {
                ToastUtil.errorShortToast(R.string.error_network_unknown);
            }
        } else {//error為null(這個時候是走onNext-->else-->requestErrorHandler)
            //(登入失敗的這種錯誤)
            if (data instanceof BaseResponse) {
                //判斷具體的業務請求是否成功
                BaseResponse response = (BaseResponse) data;
                if (TextUtils.isEmpty(response.getMessage())) {
                    //沒有錯誤提示資訊(message可能沒有錯誤提示資訊) (未知錯誤,請稍後再試!)
                    ToastUtil.errorShortToast(R.string.error_network_unknown);
                } else {//message不為空
                    ToastUtil.errorShortToast(response.getMessage());
                }
            }
        }
    }
}

在原來的HTTPObserver上 改:

    @Override
    public void onNext(T t) {
        super.onNext(t);
        LogUtil.d(TAG, "onNext:" + t);

        if (isSucceeded(t)) {
            //請求正常
            onSucceeded(t);
        } else {
            //請求出錯了(就是登入失敗了)
            handlerRequest(t, null);
        }

    }

    @Override
    public void onError(Throwable e) {
        super.onError(e);
        LogUtil.d(TAG, "onError:" + e.getLocalizedMessage());

        handlerRequest(null, e);//第一個引數為null,出錯了,沒有資料物件傳遞到這個方法裡面來
    }


	 /**
     * 處理錯誤網路請求
     *
     * @param data  T 資料模型物件
     * @param error Throwable錯誤物件
     */
    private void handlerRequest(T data, Throwable error) {
        if (onFailed(data, error)) {//fasle就會走else,父類(可以說本類)處理錯誤;true:就是外部處理錯誤
            //回撥了請求失敗方法
            //並且該方法返回了true

            //返回true就表示外部手動處理錯誤
            //那我們框架內部就不用做任何事情了
        } else {
            //呼叫工具處理錯誤(這個是父類,內部處理錯誤)
            HttpUtil.handlerRequest(data, error);

        }
        
    }

子類這裡我們也呼叫它的工具類HttpUtil來處理錯誤。
注意:這樣的話,處理錯誤的邏輯是和父類一樣的(因為都封裝到HTTPUtil這個工具類裡面了),但是這裡我們還是返回true,呼叫下HTTPUtil類方法。

        //模擬500錯誤 使用者名稱錯誤
        Api.getInstance().userDetail("-1", "1111111111")
                .subscribe(new HttpObserver<DetailResponse<User>>() {
                    @Override
                    public void onSucceeded(DetailResponse<User> data) {
                        LogUtil.d(TAG, "onSucceeded:" + data.getData());
                    }

                    @Override
                    public boolean onFailed(DetailResponse<User> data, Throwable e) {
                        LogUtil.d(TAG, "onFailed:" + e);

//                        return super.onFailed(data, e);//呼叫父類,內部處理錯誤

                        //return true 表示:手動處理錯誤

                        //呼叫工具類處理錯誤(這個時候會有提示彈出來)
                        HttpUtil.handlerRequest(data, e);

                        return true;//外部處理,(就是說內部的那個提示沒有彈出來)
                    }
                });

4.網路請求載入對話方塊顯示和隱藏(網路請求時顯示,成功和失敗都隱藏)

在HttpObserver的構造方法中新增一個引數控制是否顯示對話方塊

HttpObserver類

package com.ixuea.courses.mymusicold.listener;

import com.ixuea.courses.mymusicold.activity.BaseCommonActivity;
import com.ixuea.courses.mymusicold.domain.response.BaseResponse;
import com.ixuea.courses.mymusicold.util.HttpUtil;
import com.ixuea.courses.mymusicold.util.LoadingUtil;
import com.ixuea.courses.mymusicold.util.LogUtil;

import io.reactivex.disposables.Disposable;

/**
 * 網路請求Observer
 * <p>
 * 有些錯誤不方便放在ObserverAdapter處理,所以定義了HttpObserver類這個是繼承ObserverAdapter<T>的
 * <p>
 * 本類有成功與失敗方法
 */
public abstract class HttpObserver<T> extends ObserverAdapter<T> {


    private static final String TAG = "HttpObserver";
    //final 修飾的成員變數,必須要初始化一次,而這裡有個空構造方法,有可能沒有初始化成員變數,
    // 所以final修飾的成員變數在編譯的時候可能沒有初始化,故報錯
//    private final BaseCommonActivity activity;
//    private final boolean isShowLoading;
    private BaseCommonActivity activity;//公共Activity
    private boolean isShowLoading;//是否顯示載入對話方塊

    /**
     * 無參構造方法  新增這個的主要目的是:父類中有個有參構造方法(這時父類沒有無參構造方法,子類在new無參構造的時候就會報錯,所以這裡要新增無參構造方法)
     */
    public HttpObserver() {

    }

    /**
     * 有參構造方法
     *
     * @param activity      BaseCommonActivity
     * @param isShowLoading 是否顯示載入提示框
     */
    public HttpObserver(BaseCommonActivity activity, boolean isShowLoading) {
        this.activity = activity;
        this.isShowLoading = isShowLoading;
    }


    /**
     * 請求成功
     *
     * @param data 資料(物件或者集合)
     *             Succeeded:success  後面的2個s改成ed
     *             改成抽象類,讓子類實現
     */
    public abstract void onSucceeded(T data);

    /**
     * 請求失敗
     *
     * @param data 資料(物件或者集合)
     * @param e    Throwable
     * @return boolean false :表示父類(本類)要處理錯誤(內部處理);true:表示子類要處理錯誤(外部處理)
     */
    public boolean onFailed(T data, Throwable e) {
        return false;
    }

    @Override
    public void onSubscribe(Disposable d) {
        super.onSubscribe(d);
        if (isShowLoading) {
            //顯示載入對話方塊
            LoadingUtil.showLoading(activity);
        }
    }

    /**
     * 如果要404錯誤,只需要把http://dev-my-cloud-music-api-rails.ixuea.com/v1/sheets中的sheets改下,就變成404
     * 如果要500錯誤,只要使用者名稱不存在就會變成500錯誤
     * 比如:
     * http://dev-my-cloud-music-api-rails.ixuea.com/v1/users/-1?nickname=11111111這個地址就是500錯誤
     * <p>
     * 總結:發生錯誤都會發生在onError中
     *
     *
     *   已經請求成功,但是登入失敗了
     *   但是如果使用者名稱 密碼錯誤會返回false
     *
     *   可以理解為200~299之間的值就會返回到這裡來
     *   這裡面的錯誤,可以先看看,到時候遇到再說
     * @param t 具體的物件或者集合
     */


    @Override
    public void onNext(T t) {
        super.onNext(t);
        LogUtil.d(TAG, "onNext:" + t);

        //檢查是否需要隱藏載入提示框(其他地方如onError中用到,提取到一個方法中)
        checkHideLoading();

        if (isSucceeded(t)) {
            //請求正常
            onSucceeded(t);
        } else {
            //請求出錯了(就是登入失敗了)
            handlerRequest(t, null);
        }

    }


    @Override
    public void onError(Throwable e) {
        super.onError(e);
        LogUtil.d(TAG, "onError:" + e.getLocalizedMessage());
        //檢查是否需要隱藏載入提示框
        checkHideLoading();

        handlerRequest(null, e);//第一個引數為null,出錯了,沒有資料物件傳遞到這個方法裡面來
    }

    /**
     * 判斷網路請求是否成功
     *
     * @param t T
     * @return 是否成功
     */
    private boolean isSucceeded(T t) {
        //比如返回的code=200,(比如使用者名稱 密碼錯誤)
        if (t instanceof BaseResponse) {
            //判斷具體的業務請求是否成功
            BaseResponse baseResponse = (BaseResponse) t;

            //沒有狀態碼錶示成功
            //這是我們和服務端的一個規定(一般情況下status等於0才是成功,我們這裡是null才成功)
            //一般==null,則return true;否則return false
            return baseResponse.getStatus() == null;
        }
        return false;
    }

    /**
     * 處理錯誤網路請求
     *
     * @param data  T 資料模型物件
     * @param error Throwable錯誤物件
     */
    private void handlerRequest(T data, Throwable error) {
        if (onFailed(data, error)) {//fasle就會走else,父類(可以說本類)處理錯誤;true:就是外部處理錯誤
            //回撥了請求失敗方法
            //並且該方法返回了true

            //返回true就表示外部手動處理錯誤
            //那我們框架內部就不用做任何事情了
        } else {
            //呼叫工具處理錯誤(這個是父類,內部處理錯誤)
            HttpUtil.handlerRequest(data, error);

        }

    }

    /**
     * 檢查是否需要隱藏載入提示框
     */
    private void checkHideLoading() {
        if (isShowLoading) {
            LoadingUtil.hideLoading();
        }
    }

}

使用

 //測試自動顯示載入對話方塊
Api.getInstance().sheetDetail("1")
        .subscribe(new HttpObserver<DetailResponse<Sheet>>(getMainActivity(), true) {
            //false表示不顯示
//                .subscribe(new HttpObserver<DetailResponse<Sheet>>(getMainActivity(),false) {
            //無參構造方法(沒有引數,也是不顯示提示載入對話方塊)
//                .subscribe(new HttpObserver<DetailResponse<Sheet>>(getMainActivity(),false) {
            @Override
            public void onSucceeded(DetailResponse<Sheet> data) {
                LogUtil.d(TAG, "onSucceeded:" + data.getData().getTitle());
            }
        });

相關文章