目前擎浴,android流行的MVC贮预、MVP模式的開發(fā)框架很多链嘀,然而一款基于MVVM模式開發(fā)框架卻很少。MVVMHabit是以谷歌DataBinding+LiveData+ViewModel框架為基礎(chǔ),整合Okhttp+RxJava+Retrofit+Glide等流行模塊售葡,加上各種原生控件自定義的BindingAdapter,讓事件與數(shù)據(jù)源完美綁定的一款容易上癮的實用性MVVM快速開發(fā)框架齿坷。從此告別findViewById(),告別setText()悬嗓,告別setOnClickListener()...
框架流程
框架特點(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中注冊一遍增拥。
-
全局操作
- 全局的Activity堆棧式管理,在程序任何地方可以打開棵帽、結(jié)束指定的Activity逗概,一鍵退出應(yīng)用程序忘衍。
- LoggingInterceptor全局?jǐn)r截網(wǎng)絡(luò)請求日志,打印Request和Response铅搓,格式化json星掰、xml數(shù)據(jù)顯示,方便與后臺調(diào)試接口怀偷。
- 全局Cookie播玖,支持SharedPreferences和內(nèi)存兩種管理模式蜀踏。
- 通用的網(wǎng)絡(luò)請求異常監(jiān)聽,根據(jù)不同的狀態(tài)碼或異常設(shè)置相應(yīng)的message木西。
- 全局的異常捕獲八千,程序發(fā)生異常時不會崩潰燎猛,可跳入異常界面重啟應(yīng)用重绷。
- 全局事件回調(diào),提供RxBus愤钾、Messenger兩種回調(diào)方式候醒。
- 全局任意位置一行代碼實現(xiàn)文件下載進(jìn)度監(jiān)聽(暫不支持多文件進(jìn)度監(jiān)聽)倒淫。
- 全局點(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文件每窖。