手把手為你封裝一個MVP+RxJava+Retrofit2+Dagger2+BaseRecyclerView快速開發框架,兩週一個版本不是夢...

weixin_34365417發表於2017-06-12

距離我上次發表文章都有超過半年時間了,年前一直在複習,年後一段時間都在找工作,期間還去了一家公司三天,覺得不合適就溜了,感覺挺對不起那家公司的。最後等了一個多月(期間自己也有一段時間去了複習怎麼做網頁)才入職一家比較知名的國企,拿到自己想要的薪水,也是對上一年自己學習成果的回報吧,也實現了自己不想再待在外包公司小小的願望。現在回想起2016剛畢業真的覺得有點苦,白天在外包公司工作量成倍的增長,晚上還堅持看書學習做筆記,最後真正實現了逃亡的目標的時候自己也想放鬆一段日子,所以在2017年的上半年自己基本處於一種半頹廢的狀態,直到最近在新公司接到一個搭建新框架的任務才重新投入安卓的懷抱,2017年自己的計劃也正式的算是開始了。
好吧,不扯個人經歷這種無聊的話題了,迴歸正題,通過一段時間的歸納和總結網上很多我覺得很好的程式碼,就自己搭建了一套快速開發的框架,下面我就把自己的愚見和網上收集的資料分享一下。
如果不想聽我多囉嗦的話可以在GitHub上直接下載原始碼下載連結

1.首先,介紹一下BaseRecyclerView:

2185296-33569df5c1dd749e.png
BaseRecyclerView介面卡的使用.png

可以看到,封裝之後的Adapter,我們只需要重寫convert一個方法就可以實現該有的效果,比原來要重寫多個方法程式碼簡潔多了。
我們只用了

viewHolder.setText(R.id.tv_title,item.getTitle());

一句程式碼就完成了對View的初始化和運用,下面我們看看ViewHolder是怎麼寫的:

public class BaseViewHolder extends RecyclerView.ViewHolder {

    private SparseArray<View> mViews;
    public View mConvertView;
    public Context mContext;

    public BaseViewHolder(Context context,View itemView) {
        super(itemView);
        mViews = new SparseArray<>();
        mConvertView = itemView;
        mContext = context;
    }

    public View getConvertView()
    {
        return mConvertView;
    }

    public static BaseViewHolder createViewHolder(Context context, View itemView){
        BaseViewHolder holder = new BaseViewHolder(context, itemView);
        return holder;
    }

    public static BaseViewHolder createViewHolder(Context context, ViewGroup parent, int layoutId){
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        BaseViewHolder holder = new BaseViewHolder(context, itemView);
        return holder;
    }

    public BaseViewHolder setText(@IdRes int viewId, CharSequence value) {
        TextView view = getView(viewId);
        view.setText(value);
        return this;
    }

    public BaseViewHolder setBackgroundColor(@IdRes int viewId, @ColorInt int color) {
        View view = getView(viewId);
        view.setBackgroundColor(color);
        return this;
    }

    public BaseViewHolder setBackgroundRes(@IdRes int viewId, @DrawableRes int backgroundRes) {
        View view = getView(viewId);
        view.setBackgroundResource(backgroundRes);
        return this;
    }

    public BaseViewHolder setTextColor(@IdRes int viewId, @ColorInt int textColor) {
        TextView view = getView(viewId);
        view.setTextColor(textColor);
        return this;
    }

    public BaseViewHolder setImageDrawable(@IdRes int viewId, Drawable drawable) {
        ImageView view = getView(viewId);
        view.setImageDrawable(drawable);
        return this;
    }

    public BaseViewHolder setImageBitmap(@IdRes int viewId, Bitmap bitmap) {
        ImageView view = getView(viewId);
        view.setImageBitmap(bitmap);
        return this;
    }

    public BaseViewHolder setVisible(@IdRes int viewId, boolean visible) {
        View view = getView(viewId);
        view.setVisibility(visible ? View.VISIBLE : View.GONE);
        return this;
    }

    //關於事件
    public BaseViewHolder setOnClickListener(@IdRes int viewId, View.OnClickListener listener) {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }



    public BaseViewHolder setOnTouchListener(@IdRes int viewId,View.OnTouchListener listener)
    {
        View view = getView(viewId);
        view.setOnTouchListener(listener);
        return this;
    }

    public BaseViewHolder setOnLongClickListener(@IdRes int viewId,View.OnLongClickListener listener)
    {
        View view = getView(viewId);
        view.setOnLongClickListener(listener);
        return this;
    }



    public <T extends View> T getView(int viewId){
        View view = mViews.get(viewId);
        if(view == null){
            view = itemView.findViewById(viewId);
            mViews.put(viewId,view);
        }
        return (T) view;
    }
}

關鍵的三步:

1.用SparseArray來儲存各個View;
2.用getView的方法去集體宣告View;
3.用系統給出的setText等方法達到你想要的效果。

簡單的封裝就能省略了開發中初始化ViewHolder的很多重複繁雜的程式碼~
在BaseAdapter,我也做了一些小小的封裝處理,把adapter的onCreateViewHolder(),onBindViewHolder(),getItemCount()三個必要寫的方法也減少為只需要重寫convert()一個方法。

2185296-e229983451bd9df9.png
BaseRecyclerView其他用法.png

可以看到,在adapter我同樣封裝了增加頭部、尾部和子項監聽事件,在下拉重新整理,上拉載入方面,我用了之前自己寫過的一個SwipeRecyclerView融合在一起,有興趣的朋友可以去看看SwipeRecyclerView原始碼下載連結
至於怎麼實現,我是參考了網上幾個對RecyclerView封裝的很好的框架進行修改的。以下是參考的連結:
BaseRecyclerViewHelperAdapter
為RecyclerView打造通用Adapter 讓RecyclerView更加好用

2.RxJava和Retrofit2的兩種網路連結方式:

因為各人都有自己的愛好,所以我聽取別人給我的建議,選擇去封裝了兩種不同連結方式。

(1)分引數上傳:

2185296-d36853b5c39141ca.png
分引數上傳.png

2185296-e6a1a9b4a1241074.png
分引數上傳-View程式碼.png

2185296-77e6e47915cf03bb.png
分引數上傳-Presenter程式碼.png

這種方式是RxJava和Retrofit2在MVP模式中最普遍的用法,它跟以前很多網路框架不一樣要先確定引數和JavaBean,所以也有習慣了以前模式的人吐槽這種方式,特別是Presenter的程式碼量太多,冗餘得程式碼也多,但是這種封裝卻是最能體現RxJava和Retrofit2特性的封裝,以後查詢介面可以一目瞭然地知道連結和所需要的引數。
這種連結方式比較常見,所以我也不做太多的解釋。
以下是參考連結:
GeekNews

(2)批量引數上傳:
2185296-817cb88b2ad0d05c.png
批量引數上傳-View層請求程式碼.png
2185296-3d5911f0bc00a5d2.png
批量引數上傳-Presenter層請求程式碼.png

很明顯,這種封裝Presenter層的程式碼量看上去會減少很多,同時,這樣的封裝跟以前很多網路框架的模式很像:

(1)用一個HashMap裝載所有的請求引數;
(2)url和引數直接在View層顯示;
(3)在請求的時候返回是一個callback。

下面我們看看程式碼:
一開始我還以為Retrofit不能批量引數上傳的,只能確定好每個引數,但是後來在網上才發現原來還有這種操作,所以就參照著幾個好的框架來實現了一次~

public interface BaseServerApi {

    @POST("{path}")
    Observable<ResponseBody> post(
            @Path(value = "path", encoded = true) String path,
            @QueryMap Map<String, Object> map);

    @GET("{path}")
    Observable<ResponseBody> get(
            @Path(value = "path", encoded = true) String path,
            @QueryMap Map<String, Object> map);
}

Retrofit2的相關配置和Get、Post上傳方法

public class RetrofitHelper {

    private static final int CONNECT_TIME_OUT = 20;
    private static final int READ_TIME_OUT = 20;
    private static final int WRITE_TIME_OUT = 20;
    private static final int MAX_CACHE_SIZE = 1024 * 1024 * 50;
    private static final String HTTP_CACHE_DIR = Environment.getExternalStorageDirectory() + "/HTTP_LIBRARY_CACHE";

    private static Retrofit retrofit;
    private static BaseServerApi baseServerApi;

    private static OkHttpClient mOkHttpClient = null;

    @Inject
    //初始化Retrofit
    public RetrofitHelper() {
        initOkHttp();
        retrofit = new Retrofit.Builder()
                .baseUrl(AppURL.BaseURL)
                .client(mOkHttpClient)
                .addConverterFactory(GsonConverterFactory.create()) //自定義的gsonConverter 在裡面處理了登入失效處理
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
        baseServerApi = retrofit.create(BaseServerApi.class);

    }

    //get方法
    public <T> T Get(String url, Map<String, Object> maps, BaseSubscriber<T> subscriber){

        return (T) baseServerApi.get(url,maps)
                .compose(RxUtil.<ResponseBody>rxSchedulerHelper())
                .compose(RxUtil.<T>handleResult())
                .subscribe(subscriber);
    }

    //post方法
    public <T> T Post(String url, Map<String, Object> maps, BaseSubscriber<T> subscriber){

        return (T) baseServerApi.post(url,maps)
                .compose(RxUtil.<ResponseBody>rxSchedulerHelper())
                .compose(RxUtil.<T>handleResult())
                .subscribe(subscriber);
    }

    //配置okHttp的資料
    public static void initOkHttp() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (Logger.DEBUG) {
            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder.addInterceptor(loggingInterceptor);
        }

        File cacheFile = new File(HTTP_CACHE_DIR);
        Cache cache = new Cache(cacheFile, MAX_CACHE_SIZE);
        Interceptor cacheInterceptor = new CacheInterceptor();
        //設定快取
        builder.addNetworkInterceptor(cacheInterceptor);
        builder.addInterceptor(cacheInterceptor);
        builder.cache(cache);
        //設定超時
        builder.connectTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS);
        builder.readTimeout(READ_TIME_OUT, TimeUnit.SECONDS);
        builder.writeTimeout(WRITE_TIME_OUT, TimeUnit.SECONDS);

        //錯誤重連
        builder.retryOnConnectionFailure(true);
        mOkHttpClient = builder.build();
    }
}

按照一位以前班裡的大神說:
這種方式來說,可讀性可能比較差點,但是如果以後你想換一種網路框架,只需要把callback稍微改一下就好了而不需要像第一種那樣整個專案都要傷筋動骨,但是這樣做卻把RxJava的鏈式結構的特性給抹殺掉了。。。
其實兩種連結方式各有各的優點,看你喜歡哪一種,或者按照個人習慣去選擇,兩種方式我都放在框架裡面了。
以下是參考連結:
Android 巧妙封裝,基於Retrofit+RxJava網路框架“Leopard”---完整淺析
Novate:Retrofit2.0和RxJava的又一次完美改進加強!

其實,很多時候我們請求資料回來都需要有第一層封裝去處理,譬如說超時登入等等,所以接下來我們也來封裝一下:
第二種網路連結方式的處理:

2185296-84d9ff3d9e6b4669.png
第二種網路連結方式的第一層資料封裝.png
public static <T> Observable.Transformer<ResponseBody, T> handleResult() {
        return new Observable.Transformer<ResponseBody, T>() {
            @Override
            public Observable<T> call(Observable<ResponseBody> tObservable) {
                return tObservable.flatMap(
                        new Func1<ResponseBody, Observable<T>>() {
                            @Override
                            public Observable<T> call(ResponseBody responseBody) {
                                try {
                                    String jstr = new String(responseBody.bytes());
                                    Type type = new TypeToken<WXHttpResponse>() {
                                    }.getType();
                                    Log.e("responseBody",jstr);
                                    //需要手動解析Json,WXHttpResponse相當於BaseBean,自己可以修改
                                    WXHttpResponse wxHttpResponse = new Gson().fromJson(jstr,type);
                                    //驗證成功返回碼
                                    if (wxHttpResponse.getCode() == 200) {
                                        Log.e("wxHttpResponse",wxHttpResponse.getNewslist().toString());
                                        return (Observable<T>) Observable.just(wxHttpResponse.getNewslist());
                                    } else {
                                        // 處理被踢出登入情況
                                        return Observable.error(new ReloginException("伺服器返回error"));
                                    }
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                return Observable.error(new ReloginException("伺服器返回error"));
                            }
                        }
                );
            }
        };
    }
public abstract class BaseSubscriber<T> extends Subscriber<T> {

    private Task<T> resultTask;
    private String msg;

    public BaseSubscriber(){
        Type type = getSuperClassGenricType(getClass(),0);
        this.resultTask = new Task(type);
    }

    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();

        if(throwable instanceof ReloginException){
            // 踢出登入
        }else if (throwable instanceof UnknownHostException) {
            msg = "沒有網路...";
        } else if (throwable instanceof SocketTimeoutException) {
            // 超時
            msg = "超時...";
        }else{
            msg = "請求失敗,請稍後重試...";
        }
        onErrorT(msg);
    }

    @Override
    public void onNext(T t) {
        if(t != null) {
            Gson gson = new Gson();
            String result = gson.toJson(t);
            resultTask.setJson(result);
            onNextT(resultTask.getResult());
        }
    }

    public abstract void onNextT(T t);

    public abstract void onErrorT(String msg);

    // 獲取超類型別
    private Type getSuperclassTypeParameter(Class<?> clazz, int index)
    {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            return Object.class;
        }

        return (Class) params[index];
    }
  
    public static Type getSuperClassGenricType(final Class clazz, final int index) {

        //返回表示此 Class 所表示的實體(類、介面、基本型別或 void)的直接超類的 Type。
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }
        //返回表示此型別實際型別引數的 Type 物件的陣列。
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            return Object.class;
        }
        if (!(params[index] instanceof Type)) {
            return Object.class;
        }

        return params[index];
    }

}

第一種連線方式跟第二種在這裡是有區別的,第二種批量引數上傳對比起第一種相對比較複雜,是因為這裡沒有用到retrofit2自動解析json的功能,要自己手動去解析,所以這裡就只列出第二種連線方式給大家看看。
以下是參考連結:
RxJava簡潔封裝之道

3.Dagger2

對於Dagger2,我只能說自己的理解能力有限,一直都是靠文章和大神的幫助才慢慢理解,所以這裡就不發表自己的愚見了,直接上幾篇寫的比較好的文章給大家參考吧~
Android:dagger2讓你愛不釋手-基礎依賴注入框架篇
Dagger2從入門到放棄再到恍然大悟
Dagger2 入門,以初學者角度

4.相關工具類

2185296-c64868b027079ce5.png
相關工具類.png

應有盡有的相關工具類滿足你的一切要求~
由衷感謝我的好友兼我的大神jaminchanks對我安卓學習提供的巨大支援,我有很多安卓資料都是從他那裡學習的。他比較低調,但是他知道的比我多太多了,這是他的簡書賬號
接下來可能我還會更新圖片框架的封裝和其他的一些改善。
如果有哪位朋友喜歡這個框架的,可以下載原始碼,一起交流一下哪裡可以改進的,共同進步~
原始碼下載連結

相關文章