1. 程式人生 > >用Retrofit+RxJava2封裝優雅的網路請求框架

用Retrofit+RxJava2封裝優雅的網路請求框架

最近難得賦閒在家,網上看到幾篇講Retrofit2的文章,發現自己以前Android專案中使用的封裝方式反而更加簡單易用,所以決定花點時間整理分享一下,讓做Android開發的小夥伴們可以更優雅的處理網路請求。

概述

首先,這是一篇講解如何封裝RetrofitRxJava2的文章,所以需要閱讀者對RetrofitRxJava2有一定的瞭解,不然不太容易看明白。現在OkHttp + Retrofit 基本成為Android開發中網路請求部分的標配,所以每個做Android開發的小夥伴都應該花點時間去學習一下。

官方網站:

Retrofit官網
A type-safe HTTP client for Android and Java.

OkHttp官網
An HTTP & HTTP/2 client for Android and Java applications.

RxJava2 Github 託管地址
RxJava – Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.

封裝背景

如果只是單獨使用retrofit 不做任何封裝的話會存在以下幾個問題。

  • 專案中會出現許多重複的網路處理程式碼。
  • 網路請求引數耦合嚴重,一旦修改網路請求的方式或者引數時,需要修改大量的程式碼,容易引入錯誤
  • 框架耦合

封裝方法

首先我們網路請求的方式以及後臺返回的資料格式都是預先設計好的,屬於介面協議,是穩定的。

例如:
請求:使用post 方法,以 request body形式向後臺發起http請求。
返回結果:為如下格式的 json

{
    "body":
         {
            "name":"ben",
            "age":"28",
            "gender":"man"
         },
        "code
":"0", "status":"0" }

status 表示處理狀態 “0”:正確 “1”:錯誤;code 表示錯誤碼,當status 為“1”時,會產生相應的錯誤碼,當status為“0”時 code為“0”;body 表示返回的結果資料

構建返回結果對映型別

根據我們定義的返回資料格式,設計對映型別

public class ResponseResult
{
    private JsonElement body;
    private String code;
    private String status;

    public JsonElement getBody()
    {
        return body;
    }

    public void setBody(JsonElement body)
    {
        this.body = body;
    }

    public String getCode()
    {
        return code;
    }

    public void setCode(String code)
    {
        this.code = code;
    }

    public String getStatus()
    {
        return status;
    }

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

    @Override
    public String toString()
    {
        return "ResponseResult{" +
                "body='" + body + '\'' +
                ", code='" + code + '\'' +
                ", status='" + status + '\'' +
                '}';
    }
}

定義Retrofit請求介面

根據定義的請求方式,構建請求介面函式

public interface CommonQueueService
{
    @POST("")
    Observable<ResponseResult> postRxBody( @Url String interfaceName, @Body Map<String, Object> reqParamMap);
}

如果是以表單的形式發起請求可以參考如下程式碼

    @FormUrlEncoded
    @POST("queue")
    Observable<ResponseResult> postRxString(@FieldMap Map<String, String> reqParamMap);

獲取Retrofit例項並封裝請求介面

此幫助類以單例的形式向外暴露介面,共有兩個網路請求介面函式

1、用於處理返回資料較為簡單,不需要大量加工的情形

public void startServerRequest(Observer<String> subscriber, String interfaceName, Map<String, Object> reqParamMap, boolean isObserveMainThread){... }
引數1:外部呼叫傳入,在裡面獲得回撥結果,結果型別為String
引數2:介面相對url
引數3:request body
引數4:回撥結果是否在UI執行緒

2、用於返回資料型別較為複雜,需要大量加工的情形
public <T>void startServerRequest(Observer<T> subscriber,Function<String, T> mapper, String interfaceName, Map<String, Object> reqParamMap){...}
引數1:外部呼叫傳入,在裡面獲得回撥結果,結果型別為泛型
引數2:外部呼叫傳入,可以在裡面對後臺返回的資料做加工,加工成自己想要的樣子,此工作預設在子執行緒中執行
引數3:介面相對url
引數4:request body

下面是完整程式碼

public class HttpMethods
{
    private static final String TAG = HttpMethods.class.getSimpleName();
    private static final int DEFAULT_TIMEOUT = 20;
    private Retrofit retrofit;
    private Retrofit specialRetrofit;
    private OkHttpClient okHttpClient;

    private HttpMethods()//構造方法私有
    {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        builder.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        builder.addInterceptor(logging);
        okHttpClient = builder.build();
        retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create(new Gson()))  
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl(Api.SERVER_SITE)
                .build();
    }
    private static class SingletonHolder
    {
        private static final HttpMethods INSTANCE = new HttpMethods();
    }
    public static HttpMethods getInstance()    //獲取單例
    {
        return SingletonHolder.INSTANCE;
    }

    /**
     * 處理網路請求結果,返回的是後臺介面的body裡面的字串
     * @param subscriber
     * @param interfaceName
     * @param reqParamMap
     * @param isObserveMainThread
     */
    public void startServerRequest(Observer<String> subscriber, String interfaceName, Map<String, Object> reqParamMap, boolean isObserveMainThread)
    {
        CommonQueueService service = retrofit.create(CommonQueueService.class);
        //Logger.t(TAG).d(String.format("介面請求資料:%s  %s ",interfaceName, new Gson().toJson(reqParamMap)));
        Observable<String> observable = service.postRxBody(interfaceName,reqParamMap)
                .map(new ResponseResultMapper());
        toSubscribe(observable, subscriber, isObserveMainThread);
    }

    /**
     * 處理網路請求結果,將結果轉換成的型別交給使用者處理
     * 此方法的優秀之處在於將資料處理完全放在了工作執行緒,轉換成使用者的目標型別後才切換到UI執行緒
     * @param subscriber
     * @param mapper
     * @param interfaceName
     * @param reqParamMap
     * @param <T>
     */
    public <T>void startServerRequest(Observer<T> subscriber,Function<String, T> mapper, String interfaceName, Map<String, Object> reqParamMap)
    {
        CommonQueueService service = retrofit.create(CommonQueueService.class);
        //Logger.t(TAG).d(String.format("介面請求資料:%s  %s ",interfaceName, new Gson().toJson(reqParamMap)));
        Observable<T> observable = service.postRxBody(interfaceName,reqParamMap)
                .map(new ResponseResultMapper()).map(mapper);
        toSubscribe(observable, subscriber, true);
    }

    //觀察者啟動器
    private <T> void toSubscribe(Observable<T> o, Observer<T> s, boolean isMainThread)
    {
        Scheduler observeScheduler = Schedulers.io();
        if (isMainThread)
            observeScheduler = AndroidSchedulers.mainThread();
        o.subscribeOn(Schedulers.io()) //繫結在io
                .observeOn(observeScheduler) //返回 內容 在Android 主執行緒
                .subscribe(s);  //放入觀察者
    }

    /**
     * 用來統一處理Http的resultCode,並將HttpResult的Data部分剝離出來返回給subscriber
     */
    private class ResponseResultMapper implements Function<ResponseResult, String>
    {
        @Override
        public String apply(@NonNull ResponseResult httpResult) throws Exception
        {
            if (httpResult == null)
            {
                throw new NullPointerException("|返回結果為null|");
            }
            //Logger.t(TAG).d("伺服器返回結果" + AppController.getInstance().getGsonInstance().toJson(httpResult,ResponseResult.class));
            if ("1".equals(httpResult.getStatus()))
            {
                String bodyStr = httpResult.getBody();
                String codeStr = httpResult.getCode();
                throw new ApiException(codeStr == null ? "" : codeStr, bodyStr == null ? "" : bodyStr);
            }
            return TextUtils.isEmpty(httpResult.getBody().toString()) ? "{}" : httpResult.getBody().toString();
        }
    }
}

簡單對上面的程式碼做個解釋

首先在網路幫助類的私有建構函式中,構建了OkHttp的例項,我們也為OkHttp設定了Log攔截器,這樣就能列印log了。
然後構建了Retrofit的例項,將OkHttp作為其客戶端,我們通過.addConverterFactory(GsonConverterFactory.create(new Gson()))設定了json轉換器,以便於retrofit將json字串轉換為我們的目標 java object。通過.addCallAdapterFactory(RxJava2CallAdapterFactory.create())集成了RxJava2,如果不新增這個adapterretrofit 只能返回Call型別,通過新增這個adapter,我們就可以返回RxJava2的型別了,進而使用RxJava2了。

至此,我們成功構建了符合我們需求的Retrofit例項

異常類封裝

此類用於封裝異常

public class ApiException extends RuntimeException
{
    private String errorCode;
    private String errorBody;

    public ApiException(String detailMessage)
    {
        this(detailMessage,"{}");
    }

    public ApiException(String detailMessage, String errorBody)
    {
        super(detailMessage);
        this.errorBody = errorBody;
        this.errorCode=detailMessage;
    }

    public String getErrorCode()
    {
        return errorCode;
    }

    public String getErrBody()
    {
        return errorBody;
    }
}

用於處理回撥的Subscriber類

public class SilenceSubscriber<T> implements Observer<T>
{
    private final static String TAG = SilenceSubscriber.class.getSimpleName();

    @Override
    public void onComplete()
    {

    }

    @Override
    public void onSubscribe(Disposable d)
    {

    }

    @Override
    public void onError(Throwable e)
    {
        try
        {
            if (e instanceof SocketTimeoutException)
            {
                Logger.t(TAG).d("SocketTimeoutException 網路中斷,請檢查您的網路狀態>" + e.getMessage());
                onHandledNetError(e);
                e.printStackTrace();
            }
            else if (e instanceof SocketException)
            {
                Logger.t(TAG).d("SocketException 網路中斷,請檢查您的網路狀態>" + e.getMessage());
                onHandledNetError(e);
                e.printStackTrace();
            }
            else if (e instanceof ApiException)
            {
                String errCode=((ApiException)e).getErrorCode();
                Logger.t(TAG).d("錯誤碼為》"+errCode);
                if (ErrorCodeTable.handleSpecificError(errCode))
                    return;
                onHandledError((ApiException) e);
            }
            else
            {
                e.printStackTrace();
                onHandledNetError(e);
                Logger.t(TAG).d("網路請求發生了沒有處理異常 網路中斷,請檢查您的網路狀態>" + e.getMessage());
            }
        } catch (Exception e1)
        {
            e1.printStackTrace();
        }
    }

    //要處理特殊的錯誤碼,重寫這個函式,需要展示toast的呼叫super,不需要就不呼叫--wb
    //1:如果要特殊處理“GAME_OVER”,而且不希望彈出toast,那麼就重寫此函式,且不呼叫super。
    //2:如果不需要特殊處理“GAME_OVER”,只是想彈出“遊戲結束”的toast,那麼把“GAME_OVER”放入碼錶裡面解析成對應的文案就好了。不要重寫此函式!
    //3: 如果要將後臺返回的提示直接顯示成toast,不做任何處理,不要重寫此函式。
    public void onHandledError(ApiException apiE)
    {
        Logger.t(TAG).d("父類onHandledError呼叫》"+apiE.getErrorCode()+" "+apiE.getErrBody());
        String errMsg= ErrorCodeTable.parseErrorCode(apiE.getErrorCode());//碼錶裡只存放不需要特殊處理只需要顯示toast的錯誤碼。
        if (!TextUtils.isEmpty(errMsg))
            ToastUtils.showShort(errMsg);
    }

    public void  onHandledNetError(Throwable throwable)
    {
        Logger.t(TAG).d("onHandledNetError》"+ (throwable==null?"null":throwable.getMessage()));
    }

    @Override
    public void onNext(T response)
    {
        Logger.t(TAG).d("onNext》"+response);
    }
}

如何使用

第一種返回簡單資料的情形:

例如我們要呼叫登入這個介面,首先構建請求的引數map,然後傳入介面名稱,選擇是否要將結果切換到UI執行緒。

    public void login(final String username, String password)
    {
        Map<String, Object> reqMap = RequestHeader.getCommonPartOfParam();
        reqMap.put(RequestHeader.U_NAME, username);
        reqMap.put(RequestHeader.U_PASSWORD, password);
        HttpMethods.getInstance().startServerRequest(new SilenceSubscriber<String>()
        {
            @Override
            public void onNext(String response)
            {
                super.onNext(response);
                //此處獲得了後臺返回的body裡面的資料
            }
        }, Api.SYSTEMC_FEEDBACK, reqMap, true);
    }

第二種:返回複雜資料,需要對資料加工的情形:

例如我們呼叫獲取使用者詳情這個介面,構建請求map,傳入介面引數,傳入一個對返回資料做加工的Function,然後在subscriber裡面的onNext中獲得最終需要的資料。

    public void getUser(final String tUserId)
    {
        Map<String, Object> reqMap = RequestHeader.getCommonPartOfParam();
        reqMap.put(RequestHeader.USER_ID, tUserId);
        HttpMethods.getInstance().startServerRequest(new SilenceSubscriber<UserBean>()
        {
            @Override
            public void onNext(UserBean user)
            {
                super.onNext(user);
                //在UI執行緒中獲取到了最終處理過的資料
            }
        }, new Function<String, UserBean>()
        {
            @Override
            public UserBean apply(String response) throws Exception
            {
                return new Gson().fromJson(response, UserBean.class);
            }
        },Api.USERC_QUERYUSERINFO, reqMap);
    }

上面處理了請求成功的情形,請求失敗也只是使用了預設處理,如果使用者需要特殊處理某些失敗情形,可以重寫SilenceSubscriber中相應的方法。

總結:

本文介紹的方法在我們的專案中執行的很好,希望可以給苦逼的從事Android開發的程式設計師一些啟發,那樣我就很欣慰了。