基于谷歌最新AAC架構(gòu),MVVM設(shè)計模式的一套快速開發(fā)庫,整合Okhttp+RxJava+Retrofit+Glide等主流模塊捺典,滿足日常開發(fā)需求从祝。使用該框架可以快速開發(fā)一個高質(zhì)量、易維護(hù)的An...

目前擎浴,android流行的MVC贮预、MVP模式的開發(fā)框架很多链嘀,然而一款基于MVVM模式開發(fā)框架卻很少。MVVMHabit是以谷歌DataBinding+LiveData+ViewModel框架為基礎(chǔ),整合Okhttp+RxJava+Retrofit+Glide等流行模塊售葡,加上各種原生控件自定義的BindingAdapter,讓事件與數(shù)據(jù)源完美綁定的一款容易上癮的實用性MVVM快速開發(fā)框架齿坷。從此告別findViewById(),告別setText()悬嗓,告別setOnClickListener()...

框架流程

image.png

框架特點(diǎn)

  • 快速開發(fā)

    只需要寫項目的業(yè)務(wù)邏輯,不用再去關(guān)心網(wǎng)絡(luò)請求慰照、權(quán)限申請噩斟、View的生命周期等問題,擼起袖子就是干毛俏。

  • 維護(hù)方便

    MVVM開發(fā)模式,低耦合击纬,邏輯分明芋肠。Model層負(fù)責(zé)將請求的數(shù)據(jù)交給ViewModel原在;ViewModel層負(fù)責(zé)將請求到的數(shù)據(jù)做業(yè)務(wù)邏輯處理,最后交給View層去展示,與View一一對應(yīng);View層只負(fù)責(zé)界面繪制刷新伐蒋,不處理業(yè)務(wù)邏輯宏多,非常適合分配獨(dú)立模塊開發(fā)荣月。

  • 流行框架

    retrofit+okhttp+rxJava負(fù)責(zé)網(wǎng)絡(luò)請求;gson負(fù)責(zé)解析json數(shù)據(jù);glide負(fù)責(zé)加載圖片挑格;rxlifecycle負(fù)責(zé)管理view的生命周期到旦;與網(wǎng)絡(luò)請求共存亡煤率;rxbinding結(jié)合databinding擴(kuò)展UI事件鹃彻;rxpermissions負(fù)責(zé)Android 6.0權(quán)限申請尝盼;material-dialogs一個漂亮的渐苏、流暢的哗戈、可定制的material design風(fēng)格的對話框。

  • 數(shù)據(jù)綁定

    滿足google目前控件支持的databinding雙向綁定,并擴(kuò)展原控件一些不支持的數(shù)據(jù)綁定巷嚣。例如將圖片的url路徑綁定到ImageView控件中,在BindingAdapter方法里面則使用Glide加載圖片盒刚;View的OnClick事件在BindingAdapter中方法使用RxView防重復(fù)點(diǎn)擊堵腹,再把事件回調(diào)到ViewModel層,實現(xiàn)xml與ViewModel之間數(shù)據(jù)和事件的綁定(框架里面部分?jǐn)U展控件和回調(diào)命令使用的是@kelin原創(chuàng)的)。

  • 基類封裝

    專門針對MVVM模式打造的BaseActivity、BaseFragment、BaseViewModel扎拣,在View層中不再需要定義ViewDataBinding和ViewModel,直接在BaseActivity袭异、BaseFragment上限定泛型即可使用九巡。普通界面只需要編寫Fragment溯饵,然后使用ContainerActivity盛裝(代理)锨用,這樣就不需要每個界面都在AndroidManifest中注冊一遍增拥。

  • 全局操作

    1. 全局的Activity堆棧式管理,在程序任何地方可以打開棵帽、結(jié)束指定的Activity逗概,一鍵退出應(yīng)用程序忘衍。
    2. LoggingInterceptor全局?jǐn)r截網(wǎng)絡(luò)請求日志,打印Request和Response铅搓,格式化json星掰、xml數(shù)據(jù)顯示,方便與后臺調(diào)試接口怀偷。
    3. 全局Cookie播玖,支持SharedPreferences和內(nèi)存兩種管理模式蜀踏。
    4. 通用的網(wǎng)絡(luò)請求異常監(jiān)聽,根據(jù)不同的狀態(tài)碼或異常設(shè)置相應(yīng)的message木西。
    5. 全局的異常捕獲八千,程序發(fā)生異常時不會崩潰燎猛,可跳入異常界面重啟應(yīng)用重绷。
    6. 全局事件回調(diào),提供RxBus愤钾、Messenger兩種回調(diào)方式候醒。
    7. 全局任意位置一行代碼實現(xiàn)文件下載進(jìn)度監(jiān)聽(暫不支持多文件進(jìn)度監(jiān)聽)倒淫。
    8. 全局點(diǎn)擊事件防抖動處理敌土,防止點(diǎn)擊過快。

1兴枯、準(zhǔn)備工作

網(wǎng)上的很多有關(guān)MVVM的資料财剖,在此就不再闡述什么是MVVM了,不清楚的朋友可以先去了解一下。todo-mvvm-live

1.1该默、啟用databinding

在主工程app的build.gradle的android {}中加入:

dataBinding {
    enabled true
}

1.2栓袖、依賴Library

從遠(yuǎn)程依賴:

在根目錄的build.gradle中加入

allprojects {
    repositories {
        ...
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

在主項目app的build.gradle中依賴

dependencies {
    ...
    implementation 'com.github.goldze:MVVMHabit:3.1.4'
}

下載例子程序裹刮,在主項目app的build.gradle中依賴?yán)映绦蛑械?strong>mvvmhabit:

dependencies {  
    ...
    implementation project(':mvvmhabit')
}

1.3捧弃、配置config.gradle

如果不是遠(yuǎn)程依賴,而是下載的例子程序嘴办,那么還需要將例子程序中的config.gradle放入你的主項目根目錄中涧郊,然后在根目錄build.gradle的第一行加入:

apply from: "config.gradle"

注意: config.gradle中的

android = [] 是你的開發(fā)相關(guān)版本配置眼五,可自行修改

support = [] 是你的support相關(guān)配置,可自行修改

dependencies = [] 是依賴第三方庫的配置批旺,可以加新庫朱沃,但不要去修改原有第三方庫的版本號茅诱,不然可能會編譯不過

1.4瑟俭、配置AndroidManifest

添加權(quán)限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

配置Application:

繼承mvvmhabit中的BaseApplication,或者調(diào)用

BaseApplication.setApplication(this);

來初始化你的Application

可以在你的自己AppApplication中配置

//是否開啟日志打印
KLog.init(true);
//配置全局異常崩潰操作
CaocConfig.Builder.create()
    .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,開啟沉浸式
    .enabled(true) //是否啟動全局異常捕獲
    .showErrorDetails(true) //是否顯示錯誤詳細(xì)信息
    .showRestartButton(true) //是否顯示重啟按鈕
    .trackActivities(true) //是否跟蹤Activity
    .minTimeBetweenCrashesMs(2000) //崩潰的間隔時間(毫秒)
    .errorDrawable(R.mipmap.ic_launcher) //錯誤圖標(biāo)
    .restartActivity(LoginActivity.class) //重新啟動后的activity
    //.errorActivity(YourCustomErrorActivity.class) //崩潰后的錯誤activity
    //.eventListener(new YourCustomEventListener()) //崩潰后的錯誤監(jiān)聽
    .apply();

2坯门、快速上手

2.1逗扒、第一個Activity

以大家都熟悉的登錄操作為例:三個文件LoginActivty.java矩肩、LoginViewModel.java现恼、activity_login.xml

2.1.1、關(guān)聯(lián)ViewModel

在activity_login.xml中關(guān)聯(lián)LoginViewModel黍檩。

<layout>
    <data>
        <variable
            type="com.goldze.mvvmhabit.ui.login.LoginViewModel"
            name="viewModel"
        />
    </data>
    .....

</layout>

variable - type:類的全路徑
variable - name:變量名

2.1.2叉袍、繼承BaseActivity

LoginActivity繼承BaseActivity

public class LoginActivity extends BaseActivity<ActivityLoginBinding, LoginViewModel> {
    //ActivityLoginBinding類是databinding框架自定生成的,對activity_login.xml
    @Override
    public int initContentView(Bundle savedInstanceState) {
        return R.layout.activity_login;
    }

    @Override
    public int initVariableId() {
        return BR.viewModel;
    }

    @Override
    public LoginViewModel initViewModel() {
        //View持有ViewModel的引用,如果沒有特殊業(yè)務(wù)處理刽酱,這個方法可以不重寫
        return ViewModelProviders.of(this).get(LoginViewModel.class);
    }
}

保存activity_login.xml后databinding會生成一個ActivityLoginBinding類喳逛。(如果沒有生成,試著點(diǎn)擊Build->Clean Project)

BaseActivity是一個抽象類棵里,有兩個泛型參數(shù)润文,一個是ViewDataBinding,另一個是BaseViewModel转唉,上面的ActivityLoginBinding則是繼承的ViewDataBinding作為第一個泛型約束,LoginViewModel繼承BaseViewModel作為第二個泛型約束稳捆。

重寫B(tài)aseActivity的二個抽象方法

initContentView() 返回界面layout的id
initVariableId() 返回變量的id赠法,對應(yīng)activity_login中name="viewModel",就像一個控件的id乔夯,可以使用R.id.xxx砖织,這里的BR跟R文件一樣,由系統(tǒng)生成末荐,使用BR.xxx找到這個ViewModel的id侧纯。

選擇性重寫initViewModel()方法,返回ViewModel對象

@Override
public LoginViewModel initViewModel() {
    //View持有ViewModel的引用甲脏,如果沒有特殊業(yè)務(wù)處理眶熬,這個方法可以不重寫
    return ViewModelProviders.of(this).get(LoginViewModel.class);
}

注意: 不重寫initViewModel(),默認(rèn)會創(chuàng)建LoginActivity中第二個泛型約束的LoginViewModel块请,如果沒有指定第二個泛型娜氏,則會創(chuàng)建BaseViewModel

2.1.3、繼承BaseViewModel

LoginViewModel繼承BaseViewModel

public class LoginViewModel extends BaseViewModel {
    public LoginViewModel(@NonNull Application application) {
        super(application);
    }
    ....
}

BaseViewModel與BaseActivity通過LiveData來處理常用UI邏輯墩新,即可在ViewModel中使用父類的showDialog()贸弥、startActivity()等方法。在這個LoginViewModel中就可以盡情的寫你的邏輯了海渊!

BaseFragment的使用和BaseActivity一樣绵疲,詳情參考Demo哲鸳。

2.2、數(shù)據(jù)綁定

擁有databinding框架自帶的雙向綁定,也有擴(kuò)展

2.2.1、傳統(tǒng)綁定

綁定用戶名:

在LoginViewModel中定義

//用戶名的綁定
public ObservableField<String> userName = new ObservableField<>("");

在用戶名EditText標(biāo)簽中綁定

android:text="@={viewModel.userName}"

這樣一來沛申,輸入框中輸入了什么,userName.get()的內(nèi)容就是什么婿奔,userName.set("")設(shè)置什么,輸入框中就顯示什么驯用。 注意: @符號后面需要加=號才能達(dá)到雙向綁定效果脸秽;userName需要是public的儒老,不然viewModel無法找到它蝴乔。

點(diǎn)擊事件綁定:

在LoginViewModel中定義

//登錄按鈕的點(diǎn)擊事件
public View.OnClickListener loginOnClick = new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
};

在登錄按鈕標(biāo)簽中綁定

android:onClick="@{viewModel.loginOnClick}"

這樣一來,用戶的點(diǎn)擊事件直接被回調(diào)到ViewModel層了驮樊,更好的維護(hù)了業(yè)務(wù)邏輯

這就是強(qiáng)大的databinding框架雙向綁定的特性薇正,不用再給控件定義id,setText()囚衔,setOnClickListener()挖腰。

但是,光有這些练湿,完全滿足不了我們復(fù)雜業(yè)務(wù)的需求昂锫亍!MVVMHabit閃亮登場:它有一套自定義的綁定規(guī)則肥哎,可以滿足大部分的場景需求辽俗,請繼續(xù)往下看。

2.2.2篡诽、自定義綁定

還拿點(diǎn)擊事件說吧崖飘,不用傳統(tǒng)的綁定方式,使用自定義的點(diǎn)擊事件綁定杈女。

在LoginViewModel中定義

//登錄按鈕的點(diǎn)擊事件
public BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {
    @Override
    public void call() {

    }
});

在activity_login中定義命名空間

xmlns:binding="http://schemas.android.com/apk/res-auto"

在登錄按鈕標(biāo)簽中綁定

binding:onClickCommand="@{viewModel.loginOnClickCommand}"

這和原本傳統(tǒng)的綁定不是一樣嗎朱浴?不,這其實是有差別的达椰。使用這種形式的綁定翰蠢,在原本事件綁定的基礎(chǔ)之上,帶有防重復(fù)點(diǎn)擊的功能啰劲,1秒內(nèi)多次點(diǎn)擊也只會執(zhí)行一次操作躏筏。如果不需要防重復(fù)點(diǎn)擊,可以加入這條屬性

binding:isThrottleFirst="@{Boolean.TRUE}"

那這功能是在哪里做的呢呈枉?答案在下面的代碼中趁尼。

//防重復(fù)點(diǎn)擊間隔(秒)
public static final int CLICK_INTERVAL = 1;

/**
* requireAll 是意思是是否需要綁定全部參數(shù), false為否
* View的onClick事件綁定
* onClickCommand 綁定的命令,
* isThrottleFirst 是否開啟防止過快點(diǎn)擊
*/
@BindingAdapter(value = {"onClickCommand", "isThrottleFirst"}, requireAll = false)
public static void onClickCommand(View view, final BindingCommand clickCommand, final boolean isThrottleFirst) {
    if (isThrottleFirst) {
        RxView.clicks(view)
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    } else {
        RxView.clicks(view)
        .throttleFirst(CLICK_INTERVAL, TimeUnit.SECONDS)//1秒鐘內(nèi)只允許點(diǎn)擊1次
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    }
}

onClickCommand方法是自定義的埃碱,使用@BindingAdapter注解來標(biāo)明這是一個綁定方法。在方法中使用了RxView來增強(qiáng)view的clicks事件酥泞,.throttleFirst()限制訂閱者在指定的時間內(nèi)重復(fù)執(zhí)行砚殿,最后通過BindingCommand將事件回調(diào)出去,就好比有一種攔截器芝囤,在點(diǎn)擊時先做一下判斷似炎,然后再把事件沿著他原有的方向傳遞。

是不是覺得有點(diǎn)意思悯姊,好戲還在后頭呢羡藐!

2.2.3、自定義ImageView圖片加載

綁定圖片路徑:

在ViewModel中定義

public String imgUrl = "http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg";

在ImageView標(biāo)簽中

binding:url="@{viewModel.imgUrl}"

url是圖片路徑悯许,這樣綁定后仆嗦,這個ImageView就會去顯示這張圖片,不限網(wǎng)絡(luò)圖片還是本地圖片先壕。

如果需要給一個默認(rèn)加載中的圖片瘩扼,可以加這一句

binding:placeholderRes="@{R.mipmap.ic_launcher_round}"

R文件需要在data標(biāo)簽中導(dǎo)入使用,如:<import type="com.goldze.mvvmhabit.R" />

BindingAdapter中的實現(xiàn)

@BindingAdapter(value = {"url", "placeholderRes"}, requireAll = false)
public static void setImageUri(ImageView imageView, String url, int placeholderRes) {
    if (!TextUtils.isEmpty(url)) {
        //使用Glide框架加載圖片
        Glide.with(imageView.getContext())
            .load(url)
            .placeholder(placeholderRes)
            .into(imageView);
    }
}

很簡單就自定義了一個ImageView圖片加載的綁定垃僚,學(xué)會這種方式集绰,可自定義擴(kuò)展。

如果你對這些感興趣谆棺,可以下載源碼栽燕,在binding包中可以看到各類控件的綁定實現(xiàn)方式

2.2.4、RecyclerView綁定

RecyclerView也是很常用的一種控件改淑,傳統(tǒng)的方式需要針對各種業(yè)務(wù)要寫各種Adapter碍岔,如果你使用了mvvmhabit,則可大大簡化這種工作量溅固,從此告別setAdapter()付秕。

在ViewModel中定義:

//給RecyclerView添加items
public final ObservableList<NetWorkItemViewModel> observableList = new ObservableArrayList<>();
//給RecyclerView添加ItemBinding
public final ItemBinding<NetWorkItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_network);

ObservableList<>和ItemBinding<>的泛型是Item布局所對應(yīng)的ItemViewModel

在xml中綁定

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    binding:itemBinding="@{viewModel.itemBinding}"
    binding:items="@{viewModel.observableList}"
    binding:layoutManager="@{LayoutManagers.linear()}"
    binding:lineManager="@{LineManagers.horizontal()}" />

layoutManager控制是線性(包含水平和垂直)排列還是網(wǎng)格排列,lineManager是設(shè)置分割線

網(wǎng)格布局的寫法:binding:layoutManager="@{LayoutManagers.grid(3)}
水平布局的寫法:binding:layoutManager="@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}"

使用到相關(guān)類侍郭,則需要導(dǎo)入該類才能使用询吴,和導(dǎo)入Java類相似

<import type="me.tatarka.bindingcollectionadapter2.LayoutManagers" />
<import type="me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers" />
<import type="android.support.v7.widget.LinearLayoutManager" />

這樣綁定后,在ViewModel中調(diào)用ObservableList的add()方法亮元,添加一個ItemViewModel猛计,界面上就會實時繪制出一個Item。在Item對應(yīng)的ViewModel中爆捞,同樣可以以綁定的形式完成邏輯

可以在請求到數(shù)據(jù)后奉瘤,循環(huán)添加observableList.add(new NetWorkItemViewModel(NetWorkViewModel.this, entity));詳細(xì)可以參考例子程序中NetWorkViewModel類。

注意: 在以前的版本中,ItemViewModel是繼承BaseViewModel盗温,傳入Context藕赞,新版本3.x中可繼承ItemViewModel,傳入當(dāng)前頁面的ViewModel

更多RecyclerView卖局、ListView斧蜕、ViewPager等綁定方式,請參考 https://github.com/evant/binding-collection-adapter

2.3砚偶、網(wǎng)絡(luò)請求

網(wǎng)絡(luò)請求一直都是一個項目的核心批销,現(xiàn)在的項目基本都離不開網(wǎng)絡(luò),一個好用網(wǎng)絡(luò)請求框架可以讓開發(fā)事半功倍染坯。

2.3.1均芽、Retrofit+Okhttp+RxJava

現(xiàn)今,這三個組合基本是網(wǎng)絡(luò)請求的標(biāo)配单鹿,如果你對這三個框架不了解掀宋,建議先去查閱相關(guān)資料。

square出品的框架羞反,用起來確實非常方便布朦。MVVMHabit中引入了

api "com.squareup.okhttp3:okhttp:3.10.0"
api "com.squareup.retrofit2:retrofit:2.4.0"
api "com.squareup.retrofit2:converter-gson:2.4.0"
api "com.squareup.retrofit2:adapter-rxjava2:2.4.0"

構(gòu)建Retrofit時加入

Retrofit retrofit = new Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

或者直接使用例子程序中封裝好的RetrofitClient

2.3.2囤萤、網(wǎng)絡(luò)攔截器

LoggingInterceptor: 全局?jǐn)r截請求信息昼窗,格式化打印Request、Response涛舍,可以清晰的看到與后臺接口對接的數(shù)據(jù)澄惊,

LoggingInterceptor mLoggingInterceptor = new LoggingInterceptor
    .Builder()//構(gòu)建者模式
    .loggable(true) //是否開啟日志打印
    .setLevel(Level.BODY) //打印的等級
    .log(Platform.INFO) // 打印類型
    .request("Request") // request的Tag
    .response("Response")// Response的Tag
    .addHeader("version", BuildConfig.VERSION_NAME)//打印版本
    .build()

構(gòu)建okhttp時加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(mLoggingInterceptor)
    .build();

CacheInterceptor: 緩存攔截器,當(dāng)沒有網(wǎng)絡(luò)連接的時候自動讀取緩存中的數(shù)據(jù)富雅,緩存存放時間默認(rèn)為3天掸驱。
創(chuàng)建緩存對象

//緩存時間
int CACHE_TIMEOUT = 10 * 1024 * 1024
//緩存存放的文件
File httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
//緩存對象
Cache cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);

構(gòu)建okhttp時加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(new CacheInterceptor(mContext))
    .build();

2.3.3、Cookie管理

MVVMHabit提供兩種CookieStore:PersistentCookieStore (SharedPreferences管理)和MemoryCookieStore (內(nèi)存管理)没佑,可以根據(jù)自己的業(yè)務(wù)需求毕贼,在構(gòu)建okhttp時加入相應(yīng)的cookieJar

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))
    .build();

或者

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new MemoryCookieStore()))
    .build();

2.3.4、綁定生命周期

請求在ViewModel層蛤奢。默認(rèn)在BaseActivity中注入了LifecycleProvider對象到ViewModel鬼癣,用于綁定請求的生命周期,View與請求共存亡啤贩。

RetrofitClient.getInstance().create(DemoApiService.class)
    .demoGet()
    .compose(RxUtils.bindToLifecycle(getLifecycleProvider())) // 請求與View周期同步
    .compose(RxUtils.schedulersTransformer())  // 線程調(diào)度
    .compose(RxUtils.exceptionTransformer())   // 網(wǎng)絡(luò)錯誤的異常轉(zhuǎn)換
    .subscribe(new Consumer<BaseResponse<DemoEntity>>() {
        @Override
        public void accept(BaseResponse<DemoEntity> response) throws Exception {

        }
    }, new Consumer<ResponseThrowable>() {
        @Override
        public void accept(ResponseThrowable throwable) throws Exception {

        }
    });

在請求時關(guān)鍵需要加入組合操作符.compose(RxUtils.bindToLifecycle(getLifecycleProvider()))
注意: 由于BaseActivity/BaseFragment都實現(xiàn)了LifecycleProvider接口待秃,并且默認(rèn)注入到ViewModel中,所以在調(diào)用請求方法時可以直接調(diào)用getLifecycleProvider()拿到生命周期接口痹屹。如果你沒有使用 mvvmabit 里面的BaseActivity或BaseFragment章郁,使用自己定義的Base,那么需要讓你自己的Activity繼承RxAppCompatActivity志衍、Fragment繼承RxFragment才能用RxUtils.bindToLifecycle(lifecycle)方法暖庄。

2.3.5聊替、網(wǎng)絡(luò)異常處理

網(wǎng)絡(luò)異常在網(wǎng)絡(luò)請求中非常常見,比如請求超時培廓、解析錯誤佃牛、資源不存在、服務(wù)器內(nèi)部錯誤等医舆,在客戶端則需要做相應(yīng)的處理(當(dāng)然俘侠,你可以把一部分異常甩鍋給網(wǎng)絡(luò),比如當(dāng)出現(xiàn)code 500時蔬将,提示:請求超時爷速,請檢查網(wǎng)絡(luò)連接,此時偷偷將異常信息發(fā)送至后臺(手動滑稽))霞怀。

在使用Retrofit請求時惫东,加入組合操作符.compose(RxUtils.exceptionTransformer()),當(dāng)發(fā)生網(wǎng)絡(luò)異常時毙石,回調(diào)onError(ResponseThrowable)方法廉沮,可以拿到異常的code和message,做相應(yīng)處理徐矩。

mvvmhabit中自定義了一個ExceptionHandle滞时,已為你完成了大部分網(wǎng)絡(luò)異常的判斷,也可自行根據(jù)項目的具體需求調(diào)整邏輯滤灯。

注意: 這里的網(wǎng)絡(luò)異常code坪稽,并非是與服務(wù)端協(xié)議約定的code。網(wǎng)絡(luò)異沉壑瑁可以分為兩部分豫尽,一部分是協(xié)議異常,即出現(xiàn)code = 404渤滞、500等陈症,屬于HttpException,另一部分為請求異常趴腋,即出現(xiàn):連接超時、解析錯誤颁井、證書驗證失等蠢护。而與服務(wù)端約定的code規(guī)則葵硕,它不屬于網(wǎng)絡(luò)異常,它是屬于一種業(yè)務(wù)異常蜀变。在請求中可以使用RxJava的filter(過濾器)介评,也可以自定義BaseSubscriber統(tǒng)一處理網(wǎng)絡(luò)請求的業(yè)務(wù)邏輯異常。由于每個公司的業(yè)務(wù)協(xié)議不一樣寒瓦,所以具體需要你自己來處理該類異常坪仇。

3、輔助功能

一個完整的快速開發(fā)框架烟很,當(dāng)然也少不了常用的輔助類雾袱。下面來介紹一下MVVMabit中有哪些輔助功能官还。

3.1、事件總線

事件總線存在的優(yōu)點(diǎn)想必大家都很清楚了望伦,android自帶的廣播機(jī)制對于組件間的通信而言屯伞,使用非常繁瑣,通信組件彼此之間的訂閱和發(fā)布的耦合也比較嚴(yán)重珠移,特別是對于事件的定義,廣播機(jī)制局限于序列化的類(通過Intent傳遞)暇韧,不夠靈活浓瞪。

3.3.1乾颁、RxBus

RxBus并不是一個庫,而是一種模式骂倘。相信大多數(shù)開發(fā)者都使用過EventBus历涝,對RxBus也是很熟悉漾唉。由于MVVMabit中已經(jīng)加入RxJava,所以采用了RxBus代替EventBus作為事件總線通信赵刑,以減少庫的依賴分衫。

使用方法:

在ViewModel中重寫registerRxBus()方法來注冊RxBus,重寫removeRxBus()方法來移除RxBus

//訂閱者
private Disposable mSubscription;
//注冊RxBus
@Override
public void registerRxBus() {
    super.registerRxBus();
    mSubscription = RxBus.getDefault().toObservable(String.class)
        .subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {

            }
        });
    //將訂閱者加入管理站
    RxSubscriptions.add(mSubscription);
}

//移除RxBus
@Override
public void removeRxBus() {
    super.removeRxBus();
    //將訂閱者從管理站中移除
    RxSubscriptions.remove(mSubscription);
}

在需要執(zhí)行回調(diào)的地方發(fā)送

RxBus.getDefault().post(object);

3.3.2般此、Messenger

Messenger是一個輕量級全局的消息通信工具,在我們的復(fù)雜業(yè)務(wù)中铐懊,難免會出現(xiàn)一些交叉的業(yè)務(wù)邀桑,比如ViewModel與ViewModel之間需要有數(shù)據(jù)交換科乎,這時候可以輕松地使用Messenger發(fā)送一個實體或一個空消息壁畸,將事件從一個ViewModel回調(diào)到另一個ViewModel中。

使用方法:

定義一個靜態(tài)String類型的字符串token

public static final String TOKEN_LOGINVIEWMODEL_REFRESH = "token_loginviewmodel_refresh";

在ViewModel中注冊消息監(jiān)聽

//注冊一個空消息監(jiān)聽 
//參數(shù)1:接受人(上下文)
//參數(shù)2:定義的token
//參數(shù)3:執(zhí)行的回調(diào)監(jiān)聽
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() {
    @Override
    public void call() {

    }
});

//注冊一個帶數(shù)據(jù)回調(diào)的消息監(jiān)聽 
//參數(shù)1:接受人(上下文)
//參數(shù)2:定義的token
//參數(shù)3:實體的泛型約束
//參數(shù)4:執(zhí)行的回調(diào)監(jiān)聽
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new BindingConsumer<String>() {
    @Override
    public void call(String s) {

    }
});

在需要回調(diào)的地方使用token發(fā)送消息

//發(fā)送一個空消息
//參數(shù)1:定義的token
Messenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);

//發(fā)送一個帶數(shù)據(jù)回調(diào)消息
//參數(shù)1:回調(diào)的實體
//參數(shù)2:定義的token
Messenger.getDefault().send("refresh",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);

token最好不要重名茅茂,不然可能就會出現(xiàn)邏輯上的bug,為了更好的維護(hù)和清晰邏輯空闲,建議以aa_bb_cc的格式來定義token令杈。aa:TOKEN,bb:ViewModel的類名逗噩,cc:動作名(功能名)悔常。

為了避免大量使用Messenger给赞,建議只在ViewModel與ViewModel之間使用,View與ViewModel之間采用ObservableField去監(jiān)聽UI上的邏輯残邀,可在繼承了Base的Activity或Fragment中重寫initViewObservable()方法來初始化UI的監(jiān)聽

注冊了監(jiān)聽,當(dāng)然也要解除它耻台。在BaseActivity盆耽、BaseFragment的onDestroy()方法里已經(jīng)調(diào)用Messenger.getDefault().unregister(viewModel);解除注冊,所以不用擔(dān)心忘記解除導(dǎo)致的邏輯錯誤和內(nèi)存泄漏摄杂。

3.2坝咐、文件下載

文件下載幾乎是每個app必備的功能,圖文的下載析恢,軟件的升級等都要用到墨坚,mvvmhabit使用Retrofit+Okhttp+RxJava+RxBus實現(xiàn)一行代碼監(jiān)聽帶進(jìn)度的文件下載。

下載文件

String loadUrl = "你的文件下載路徑";
String destFileDir = context.getCacheDir().getPath();  //文件存放的路徑
String destFileName = System.currentTimeMillis() + ".apk";//文件存放的名稱
DownLoadManager.getInstance().load(loadUrl, new ProgressCallBack<ResponseBody>(destFileDir, destFileName) {
    @Override
    public void onStart() {
        //RxJava的onStart()
    }

    @Override
    public void onCompleted() {
        //RxJava的onCompleted()
    }

    @Override
    public void onSuccess(ResponseBody responseBody) {
        //下載成功的回調(diào)
    }

    @Override
    public void progress(final long progress, final long total) {
        //下載中的回調(diào) progress:當(dāng)前進(jìn)度 映挂,total:文件總大小
    }

    @Override
    public void onError(Throwable e) {
        //下載錯誤回調(diào)
    }
});

在ProgressResponseBody中使用了RxBus泽篮,發(fā)送下載進(jìn)度信息到ProgressCallBack中,繼承ProgressCallBack就可以監(jiān)聽到下載狀態(tài)柑船∶背牛回調(diào)方法全部執(zhí)行在主線程,方便UI的更新椎组,詳情請參考例子程序油狂。

3.3、ContainerActivity

一個盛裝Fragment的一個容器(代理)Activity寸癌,普通界面只需要編寫Fragment,使用此Activity盛裝弱贼,這樣就不需要每個界面都在AndroidManifest中注冊一遍

使用方法:

在ViewModel中調(diào)用BaseViewModel的方法開一個Fragment

startContainerActivity(你的Fragment類名.class.getCanonicalName())

在ViewModel中調(diào)用BaseViewModel的方法蒸苇,攜帶一個序列化實體打開一個Fragment

Bundle mBundle = new Bundle();
mBundle.putParcelable("entity", entity);
startContainerActivity(你的Fragment類名.class.getCanonicalName(), mBundle);

在你的Fragment中取出實體

Bundle mBundle = getArguments();
if (mBundle != null) {
    entity = mBundle.getParcelable("entity");
}

3.4、6.0權(quán)限申請

對RxPermissions已經(jīng)熟悉的朋友可以跳過吮旅。

使用方法:

例如請求相機(jī)權(quán)限溪烤,在ViewModel中調(diào)用

//請求打開相機(jī)權(quán)限
RxPermissions rxPermissions = new RxPermissions((Activity) context);
rxPermissions.request(Manifest.permission.CAMERA)
    .subscribe(new Consumer<Boolean>() {
        @Override
        public void accept(Boolean aBoolean) throws Exception {
            if (aBoolean) {
                ToastUtils.showShort("權(quán)限已經(jīng)打開味咳,直接跳入相機(jī)");
            } else {
                ToastUtils.showShort("權(quán)限被拒絕");
            }
        }
    });

更多權(quán)限申請方式請參考RxPermissions原項目地址

3.5、圖片壓縮

為了節(jié)約用戶流量和加快圖片上傳的速度檬嘀,某些場景將圖片在本地壓縮后再傳給后臺槽驶,所以特此提供一個圖片壓縮的輔助功能。

使用方法:

RxJava的方式壓縮單張圖片鸳兽,得到一個壓縮后的圖片文件對象

String filePath = "mnt/sdcard/1.png";
ImageUtils.compressWithRx(filePath, new Consumer<File>() {
    @Override
    public void accept(File file) throws Exception {
        //將文件放入RequestBody
        ...
    }
});

RxJava的方式壓縮多張圖片掂铐,按集合順序每壓縮成功一張,都將在onNext方法中得到一個壓縮后的圖片文件對象

List<String> filePaths = new ArrayList<>();
filePaths.add("mnt/sdcard/1.png");
filePaths.add("mnt/sdcard/2.png");
ImageUtils.compressWithRx(filePaths, new Subscriber() {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(File file) {

    }
});

3.6揍异、其他輔助類

ToastUtils: 吐司工具類

MaterialDialogUtils: Material風(fēng)格對話框工具類

SPUtils: SharedPreferences工具類

SDCardUtils: SD卡相關(guān)工具類

ConvertUtils: 轉(zhuǎn)換相關(guān)工具類

StringUtils: 字符串相關(guān)工具類

RegexUtils: 正則相關(guān)工具類

KLog: 日志打印全陨,含json格式打印

4、附加

4.1衷掷、編譯錯誤解決方法

使用databinding其實有個缺點(diǎn)辱姨,就是會遇到一些編譯錯誤,而AS不能很好的定位到錯誤的位置戚嗅,這對于剛開始使用databinding的開發(fā)者來說是一個比較郁悶的事雨涛。那么我在此把我自己在開發(fā)中遇到的各種編譯問題的解決方法分享給大家,希望這對你會有所幫助懦胞。

4.1.1镜悉、綁定錯誤

綁定錯誤是一個很常見的錯誤,基本都會犯医瘫。比如TextView的 android:text="" 侣肄,本來要綁定的是一個String類型,結(jié)果你不小心醇份,可能綁了一個Boolean上去稼锅,或者變量名寫錯了,這時候編輯器不會報紅錯僚纷,而是在點(diǎn)編譯運(yùn)行的時候矩距,在AS的Messages中會出現(xiàn)錯誤提示,如下圖:

[圖片上傳失敗...(image-9838f4-1590991012257)]

解決方法:把錯誤提示拉到最下面 (上面的提示找不到BR類這個不要管它)怖竭,看最后一個錯誤 锥债,這里會提示是哪個xml出了錯,并且會定位到行數(shù)痊臭,按照提示找到對應(yīng)位置哮肚,即可解決該編譯錯誤的問題。

注意: 行數(shù)要+1广匙,意思是上面報出第33行錯誤允趟,實際是第34行錯誤,AS定位的不準(zhǔn)確 (這可能是它的一個bug)

4.1.2鸦致、xml導(dǎo)包錯誤

在xml中需要導(dǎo)入ViewModel或者一些業(yè)務(wù)相關(guān)的類潮剪,假如在xml中導(dǎo)錯了類涣楷,那一行則會報紅,但是res/layout卻沒有錯誤提示抗碰,有一種場景狮斗,非常特殊,不容易找出錯誤位置弧蝇。就是你寫了一個xml碳褒,導(dǎo)入了一個類,比如XXXUtils捍壤,后來因為業(yè)務(wù)需求骤视,把那個XXXUtils刪了,這時候res/layout下不會出現(xiàn)任何錯誤鹃觉,而你在編譯運(yùn)行的時候专酗,才會出現(xiàn)錯誤日志〉辽龋苦逼的是祷肯,不會像上面那樣提示哪一個xml文件,哪一行出錯了疗隶,最后一個錯誤只是一大片的報錯報告佑笋。如下圖:

[圖片上傳失敗...(image-dec9db-1590991012257)]

解決方法:同樣找到最后一個錯誤提示,找到Cannot resolve type for xxx這一句 (xxx是類名)斑鼻,然后使用全局搜索 (Ctrl+H) 蒋纬,搜索哪個xml引用了這個類,跟蹤點(diǎn)擊進(jìn)去坚弱,在xml就會出現(xiàn)一個紅錯蜀备,看到錯誤你就會明白了,這樣就可解決該編譯錯誤的問題荒叶。

4.1.3碾阁、build錯誤

構(gòu)建多module工程時,如出現(xiàn)【4.1.1些楣、綁定錯誤】脂凶,且你能確定這個綁定是沒有問題的,經(jīng)過修改后出現(xiàn)下圖錯誤:

[圖片上傳失敗...(image-7b2ae8-1590991012257)]

解決方法: 這種是databinding比較大的坑愁茁,清理蚕钦、重構(gòu)和刪build都不起作用,網(wǎng)上很難找到方法埋市。經(jīng)過試驗冠桃,解決辦法是手動創(chuàng)建異常中提到的文件夾,或者拷貝上一個沒有報錯的版本中對應(yīng)的文件夾道宅,可以解決這個異常

4.1.4食听、自動生成類錯誤

有時候在寫完xml時,databinding沒有自動生成對應(yīng)的Binding類及屬性污茵。比如新建了一個activity_login.xml樱报,按照databinding的寫法加入<layout> <variable>后,理論上會自動對應(yīng)生成ActivityLoginBinding.java類和variable的屬性泞当,可能是as對databding的支持還不夠吧迹蛤,有時候偏偏就不生成,導(dǎo)致BR.xxx報紅等一些莫名的錯誤襟士。

解決方法:其實確保自己的寫法沒有問題盗飒,是可以直接運(yùn)行的,報紅不一定是你寫的有問題陋桂,也有可能是編譯器抽風(fēng)了逆趣。或者使用下面的辦法
第一招:Build->Clean Project嗜历;
第二招:Build->Rebuild Project宣渗;
第三招:重啟大法。

4.1.5梨州、gradle錯誤

如果遇到以下編譯問題:

錯誤: 無法將類 BindingRecyclerViewAdapters中的方法 setAdapter應(yīng)用到給定類型; 需要: RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory 找到: RecyclerView,ItemBinding,ObservableList,BindingRecyclerViewAdapter<CAP#1>,ItemIds,ViewHolderFactory 原因: 推斷類型不符合等式約束條件 推斷: CAP#1 等式約束條件: CAP#1,NetWorkItemViewModel 其中, T是類型變量: T擴(kuò)展已在方法 setAdapter(RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory)中聲明的Object 其中, CAP#1是新類型變量: CAP#1從?的捕獲擴(kuò)展Object

一般是由于gradle plugin版本3.5.1造成的痕囱,請換成gradle plugin 3.5.0以下版本

混淆

例子程序中給出了最新的【MVVMHabit混淆規(guī)則】,包含MVVMHabit中依賴的所有第三方library暴匠,可以將規(guī)則直接拷貝到自己app的混淆規(guī)則中鞍恢。在此基礎(chǔ)上你只需要關(guān)注自己業(yè)務(wù)代碼以及自己引入第三方的混淆,【MVVMHabit混淆規(guī)則】請參考app目錄下的proguard-rules.pro文件每窖。

原文連接與源碼:https://github.com/goldze/MVVMHabit

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帮掉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岛请,更是在濱河造成了極大的恐慌旭寿,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崇败,死亡現(xiàn)場離奇詭異盅称,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)后室,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門缩膝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岸霹,你說我怎么就攤上這事疾层。” “怎么了贡避?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵痛黎,是天一觀的道長予弧。 經(jīng)常有香客問我,道長湖饱,這世上最難降的妖魔是什么掖蛤? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任井厌,我火速辦了婚禮蚓庭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仅仆。我一直安慰自己器赞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布墓拜。 她就那樣靜靜地躺著港柜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撮弧。 梳的紋絲不亂的頭發(fā)上潘懊,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機(jī)與錄音贿衍,去河邊找鬼授舟。 笑死,一個胖子當(dāng)著我的面吹牛贸辈,可吹牛的內(nèi)容都是我干的释树。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼擎淤,長吁一口氣:“原來是場噩夢啊……” “哼奢啥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嘴拢,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤桩盲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后席吴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赌结,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年孝冒,在試婚紗的時候發(fā)現(xiàn)自己被綠了柬姚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡庄涡,死狀恐怖量承,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤撕捍,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布拿穴,位于F島的核電站,受9級特大地震影響卦洽,放射性物質(zhì)發(fā)生泄漏贞言。R本人自食惡果不足惜斜棚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一阀蒂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弟蚀,春花似錦蚤霞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捶闸,卻和暖如春夜畴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背删壮。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工贪绘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人央碟。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓税灌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親亿虽。 傳聞我的和親對象是個殘疾皇子菱涤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353