目前项目需要使用Mvvm架构模式重构项目,因为之前没有使用过Mvvm架构模式,并且这种架构模式也是基于Google提供的data banding 和AAC架构模型来搭建的,所以就只能去找一个已经集成好的开源框架来研究分析一下Mvvm架构模式的具体实现是怎么样的?在Github搜索Mvvm,然后选择java语言,于是就有了我们今天要分析的Mvvm框架:MVVMHabit.
下载框架之后,感觉项目也非常的简单,最主要的是框架主要封装了一个叫“mvvmhabit”框架模块,而且提供远程集成和本地集成的两种方式。Demo主要就是说明的几种使用的场景如下图:
但是具体我们该怎样集成使用这个“mvvmhabit"框架模块呢?下面我们就来实践一下:
首先我们两个步骤引入这个框架:第一就是在build.gradle文件下的android目录下开启data banding 功能。
dataBinding {
enabled true
}
第二步就是引入这个mvvm”模块,有两种方式,为了我们能更好的使用这个开源框架,我们选择使用远程集成的方式。
implementation 'com.github.goldze:MVVMHabit:3.1.2'
目前为止,我们mvvmhabit架构模块就集成完毕了。
那么我们该如何使用呢?
因为我们data banding是基于页面的数据的,所以我们首先需要先创建资源文件和数据绑定的ViewModel对象,然后再去创建Activity时继承mvvmabit框架模块的BaseActivity需要传递两个参数,这两个参数就是我们创建资源文件后会自动根据资源文件生成的,规则就是你的资源文件名去掉下划线后首字母大写,再加上Binding这个单词。如资源文件名为:
activity_mvvm_test
则对于生成的资源绑定对象就是:ActivityMvvmTestBinding。保存后databinding会生成一个ActivityLoginBinding类。(如果没有生成,试着点击Build->Clean Project) 下面直接贴上这个资源文件的具体代码,最外层外层是 标签,然后第二层需要自定我们绑定的数据实体:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.fenjiread.learner.activity.model.MvvmTestModel"/>
<variable
name="viewNodel"
type="MvvmTestModel"/>
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="@dimen/dp_60"
android:textColor="@color/green"
android:textSize="@dimen/dp_25"
android:gravity="center"
android:text="@{viewNodel.name}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="@dimen/dp_60"
android:textSize="@dimen/dp_18"
android:textColor="@color/black"
android:text="多多岛"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"/>
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="@dimen/dp_60"
android:textSize="@dimen/dp_18"
android:textColor="@color/black"
android:text="@{viewNodel.dec}"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"/>
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="@dimen/dp_60"
android:textSize="@dimen/dp_18"
android:textColor="@color/black"
android:text="Mvvm测试"
android:gravity="center" />
</LinearLayout>
</layout>
ViewModel对象的代码实现如下:
/**
* Mvvm测试Model类
*/
public class MvvmTestModel extends BaseViewModel{
public int age;
public String name = "齐天大圣";
public String dec = "玉皇大帝";
public MvvmTestEntity mMvvmEntity;
public MvvmTestModel(@NonNull Application application) {
super(application);
mMvvmEntity = new MvvmTestEntity();
mMvvmEntity.setAge(11);
mMvvmEntity.setDec("哈哈哈哈");
mMvvmEntity.setName("齐天大圣");
}
public MvvmTestModel(@NonNull Application application, BaseModel model) {
super(application, model);
}
}
这里会实现ViewModel的两个构造方法,如果需要保存数据到本地,则需要在及继承BaseViewModel时传递一个数据持久化对象:
public class LoginViewModel extends BaseViewModel<DemoRepository> {
下面就是Activity的具体代码实现:
public class MvvmHabitTestActivity extends BaseActivity<ActivityMvvmTestBinding, MvvmTestModel> {
@Override
public int initContentView(Bundle savedInstanceState) {
return R.layout.activity_mvvm_test;
}
@Override
public int initVariableId() {
return BR.viewNodel;
}
}
现在已经能够实现数据绑定了,这里的BR是自动生成的,跟系统的资源文件类似。
继承自BaseActivity的lactivity的生命周期调用如下:
E/>>>>>>>>>>>>: onCreate
2019-05-22 17:18:30.638 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initParam
2019-05-22 17:18:30.638 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initContentView
2019-05-22 17:18:30.675 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initVariableId
2019-05-22 17:18:30.676 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initViewModel
2019-05-22 17:18:30.676 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: createViewModel
2019-05-22 17:18:30.681 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: registorUIChangeLiveDataCallBack
2019-05-22 17:18:30.683 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initData
2019-05-22 17:18:30.683 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: initViewObservable
2019-05-22 17:18:30.685 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: onStart
2019-05-22 17:18:30.690 24831-24831/com.on.person.learner E/>>>>>>>>>>>>: onResume
其他的activity生命周期都是可以正常调用的,你会发现这个继承自BaseActivity的Activity没有类似initVew或者initLayout的方法,这是因为Mvvm架构让View和model层分离了,中间使用ViewModel进行双向绑定。
现在我们再来打印一下继承自BaseViewModel类的
0/com.on.person.learner E/>>>>>>>>>>>>: onCreate
2019-05-22 17:36:35.115 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initParam
2019-05-22 17:36:35.115 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initContentView
2019-05-22 17:36:35.139 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initVariableId
2019-05-22 17:36:35.139 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initViewModel
2019-05-22 17:36:35.139 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: createViewModel
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: MvvmTestModel 1
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: injectLifecycleProvider
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: registorUIChangeLiveDataCallBack
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.140 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: getUC
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initData
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: initViewObservable
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: registerRxBus
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onCreate
2019-05-22 17:36:35.141 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:36:35.143 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onStart
2019-05-22 17:36:35.144 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onStart
2019-05-22 17:36:35.144 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:36:35.148 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onResume
2019-05-22 17:36:35.150 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onResume
2019-05-22 17:36:35.151 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
除了发现继承自BaseViewModel的ViewModel会根据不同的需求调用不同的构造方法,然后调用injectLifecycleProvider方法来注入Activity的生命周期的同时继承自BaseActivity的activity会调用registorUIChangeLiveDataCallBack()方法来注册UI更改后的接口回调。ViewModel的getUC方法会调用6次,具体原因还不清楚。然后ViewModel注册RxBus事件监听,并开始调用onCreate()方法,虽然每次调用生命周期方法,都会调用一次onAny()方法,但是现在来看ViewModel的生命周期已经可以同步Activity的具体生命周期之后调用了。
com.on.person.learner E/>>>>>>>MvvmTestModel:: onPause
2019-05-22 17:46:47.099 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:46:47.099 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onPause
2019-05-22 17:46:47.148 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onStop
2019-05-22 17:46:47.149 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:46:47.150 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onStop
关闭手机屏幕之后,发现ViewModel的onPase()生命周期方法调用已经优先于Activity的onPase()生命周期方法调用了。然后onStop()方法也一样优先于activity的onStop()方法调用。
er E/>>>>>>>>>>>>: onStart
2019-05-22 17:50:27.875 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onStart
2019-05-22 17:50:27.875 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:50:27.876 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onResume
2019-05-22 17:50:27.878 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onResume
2019-05-22 17:50:27.878 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
我们再按一下物理返回键:
com.on.person.learner E/>>>>>>>MvvmTestModel:: onPause
2019-05-22 17:51:08.258 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:51:08.258 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onPause
2019-05-22 17:51:08.262 368-2318/? E/ANDR-PERF-RESOURCEQS: Failed to apply optimization [4, 0]
2019-05-22 17:51:08.637 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onStop
2019-05-22 17:51:08.637 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:51:08.637 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onStop
2019-05-22 17:51:08.639 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onDestroy
2019-05-22 17:51:08.639 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onAny
2019-05-22 17:51:08.639 26070-26070/com.on.person.learner E/>>>>>>>>>>>>: onDestroy
2019-05-22 17:51:08.639 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: onCleared
2019-05-22 17:51:08.640 26070-26070/com.on.person.learner E/>>>>>>>MvvmTestModel:: removeRxBus
最后我们可以得出结论:ViewModel的生命周期方法是继承自BaseActivity的activity的生命周期的映射,并且是一一对应的。初始时ViewModel的onCreate()方法在调用registerRxBus之后执行,然后生命周期方法开始和activity的生命周期对应,在activity的生命周期执行之后执行。但是在手机熄屏或者是点击手机的物理返回键时,这时ViewModel的生命周期会优先于activity的onPuse和onStop、onDestroy方法执行。在ViewModel执行完OnDestroy之后,还会再调用onCleared和removeRxBus方法来释放资源。
在ViewModel里面我们可以创建ObservableField等事件绑定类。
绑定用户名:
在LoginViewModel中定义
//用户名的绑定
public ObservableField<String> userName = new ObservableField<>("");
在用户名EditText标签中绑定
android:text="@={viewModel.userName}"
这样一来,输入框中输入了什么,userName.get()的内容就是什么,userName.set("")设置什么,输入框中就显示什么。
注意: @符号后面需要加=号才能达到双向绑定效果;userName需要是public的,不然viewModel无法找到它。
点击事件绑定:
在LoginViewModel中定义
//登录按钮的点击事件
public View.OnClickListener loginOnClick = new View.OnClickListener() {
@Override
public void onClick(View v) {
}
};
在登录按钮标签中绑定
android:onClick="@{viewModel.loginOnClick}"
这样一来,用户的点击事件直接被回调到ViewModel层了,更好的维护了业务逻辑
这就是强大的databinding框架双向绑定的特性,不用再给控件定义id,setText(),setOnClickListener()。
2.2.2、自定义绑定
还拿点击事件说吧,不用传统的绑定方式,使用自定义的点击事件绑定。
在LoginViewModel中定义
//登录按钮的点击事件
public BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {
@Override
public void call() {
}
});
在activity_login中定义命名空间
xmlns:binding="http://schemas.android.com/apk/res-auto"
在登录按钮标签中绑定
binding:onClickCommand="@{viewModel.loginOnClickCommand}"
这和原本传统的绑定不是一样吗?不,这其实是有差别的。使用这种形式的绑定,在原本事件绑定的基础之上,带有防重复点击的功能,1秒内多次点击也只会执行一次操作。如果不需要防重复点击,可以加入这条属性
binding:isThrottleFirst="@{Boolean.TRUE}"
那这功能是在哪里做的呢?答案在下面的代码中。
//防重复点击间隔(秒)
public static final int CLICK_INTERVAL = 1;
/**
* requireAll 是意思是是否需要绑定全部参数, false为否
* View的onClick事件绑定
* onClickCommand 绑定的命令,
* isThrottleFirst 是否开启防止过快点击
*/
@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秒钟内只允许点击1次
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object object) throws Exception {
if (clickCommand != null) {
clickCommand.execute();
}
}
});
}
}
onClickCommand方法是自定义的,使用@BindingAdapter注解来标明这是一个绑定方法。在方法中使用了RxView来增强view的clicks事件,.throttleFirst()限制订阅者在指定的时间内重复执行,最后通过BindingCommand将事件回调出去,就好比有一种拦截器,在点击时先做一下判断,然后再把事件沿着他原有的方向传递。
2.2.3、自定义ImageView图片加载
绑定图片路径:
在ViewModel中定义
public String imgUrl = "http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg";
在ImageView标签中
binding:url="@{viewModel.imgUrl}"
url是图片路径,这样绑定后,这个ImageView就会去显示这张图片,不限网络图片还是本地图片。
如果需要给一个默认加载中的图片,可以加这一句
binding:placeholderRes="@{R.mipmap.ic_launcher_round}"
R文件需要在data标签中导入使用,如:
<import type="com.goldze.mvvmhabit.R" />
BindingAdapter中的实现
@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图片加载的绑定,学会这种方式,可自定义扩展。
如果你对这些感兴趣,可以下载源码,在binding包中可以看到各类控件的绑定实现方式
2.2.4、RecyclerView绑定
RecyclerView也是很常用的一种控件,传统的方式需要针对各种业务要写各种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布局所对应的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控制是线性(包含水平和垂直)排列还是网格排列,lineManager是设置分割线
网格布局的写法:binding:layoutManager="@{LayoutManagers.grid(3)}
水平布局的写法:binding:layoutManager="@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}"
使用到相关类,则需要导入该类才能使用,和导入Java类相似
<import type="me.tatarka.bindingcollectionadapter2.LayoutManagers" />
<import type="me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers" />
<import type="android.support.v7.widget.LinearLayoutManager" />
这样绑定后,在ViewModel中调用ObservableList的add()方法,添加一个ItemViewModel,界面上就会实时绘制出一个Item。在Item对应的ViewModel中,同样可以以绑定的形式完成逻辑
可以在请求到数据后,循环添加
observableList.add(new NetWorkItemViewModel(NetWorkViewModel.this, entity));
详细可以参考例子程序中NetWorkViewModel类。
注意: 在以前的版本中,ItemViewModel是继承BaseViewModel,传入Context,新版本3.x中可继承ItemViewModel,传入当前页面的ViewModel
更多RecyclerView、ListView、ViewPager等绑定方式,请参考 https://github.com/evant/binding-collection-adapter
2.3、网络请求
网络请求一直都是一个项目的核心,现在的项目基本都离不开网络,一个好用网络请求框架可以让开发事半功倍。
2.3.1、Retrofit+Okhttp+RxJava
现今,这三个组合基本是网络请求的标配,如果你对这三个框架不了解,建议先去查阅相关资料。
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"
构建Retrofit时加入
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
或者直接使用例子程序中封装好的RetrofitClient
2.3.2、网络拦截器
LoggingInterceptor: 全局拦截请求信息,格式化打印Request、Response,可以清晰的看到与后台接口对接的数据,
LoggingInterceptor mLoggingInterceptor = new LoggingInterceptor
.Builder()//构建者模式
.loggable(true) //是否开启日志打印
.setLevel(Level.BODY) //打印的等级
.log(Platform.INFO) // 打印类型
.request("Request") // request的Tag
.response("Response")// Response的Tag
.addHeader("version", BuildConfig.VERSION_NAME)//打印版本
.build()
构建okhttp时加入
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(mLoggingInterceptor)
.build();
CacheInterceptor: 缓存拦截器,当没有网络连接的时候自动读取缓存中的数据,缓存存放时间默认为3天。
创建缓存对象
//缓存时间
int CACHE_TIMEOUT = 10 * 1024 * 1024
//缓存存放的文件
File httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
//缓存对象
Cache cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);
构建okhttp时加入
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new CacheInterceptor(mContext))
.build();
2.3.3、Cookie管理
MVVMHabit提供两种CookieStore:PersistentCookieStore (SharedPreferences管理)和MemoryCookieStore (内存管理),可以根据自己的业务需求,在构建okhttp时加入相应的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层。默认在BaseActivity中注入了LifecycleProvider对象到ViewModel,用于绑定请求的生命周期,View与请求共存亡。
RetrofitClient.getInstance().create(DemoApiService.class)
.demoGet()
.compose(RxUtils.bindToLifecycle(getLifecycleProvider())) // 请求与View周期同步
.compose(RxUtils.schedulersTransformer()) // 线程调度
.compose(RxUtils.exceptionTransformer()) // 网络错误的异常转换
.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 {
}
});
在请求时关键需要加入组合操作符.compose(RxUtils.bindToLifecycle(getLifecycleProvider()))
注意: 由于BaseActivity/BaseFragment都实现了LifecycleProvider接口,并且默认注入到ViewModel中,所以在调用请求方法时可以直接调用getLifecycleProvider()拿到生命周期接口。如果你没有使用 mvvmabit 里面的BaseActivity或BaseFragment,使用自己定义的Base,那么需要让你自己的Activity继承RxAppCompatActivity、Fragment继承RxFragment才能用RxUtils.bindToLifecycle(lifecycle)
方法。
2.3.5、网络异常处理
网络异常在网络请求中非常常见,比如请求超时、解析错误、资源不存在、服务器内部错误等,在客户端则需要做相应的处理(当然,你可以把一部分异常甩锅给网络,比如当出现code 500时,提示:请求超时,请检查网络连接,此时偷偷将异常信息发送至后台(手动滑稽))。
在使用Retrofit请求时,加入组合操作符.compose(RxUtils.exceptionTransformer())
,当发生网络异常时,回调onError(ResponseThrowable)方法,可以拿到异常的code和message,做相应处理。
mvvmhabit中自定义了一个ExceptionHandle,已为你完成了大部分网络异常的判断,也可自行根据项目的具体需求调整逻辑。
注意: 这里的网络异常code,并非是与服务端协议约定的code。网络异常可以分为两部分,一部分是协议异常,即出现code = 404、500等,属于HttpException,另一部分为请求异常,即出现:连接超时、解析错误、证书验证失等。而与服务端约定的code规则,它不属于网络异常,它是属于一种业务异常。在请求中可以使用RxJava的filter(过滤器),也可以自定义BaseSubscriber统一处理网络请求的业务逻辑异常。由于每个公司的业务协议不一样,所以具体需要你自己来处理该类异常。
3、辅助功能
一个完整的快速开发框架,当然也少不了常用的辅助类。下面来介绍一下MVVMabit中有哪些辅助功能。
3.1、事件总线
事件总线存在的优点想必大家都很清楚了,android自带的广播机制对于组件间的通信而言,使用非常繁琐,通信组件彼此之间的订阅和发布的耦合也比较严重,特别是对于事件的定义,广播机制局限于序列化的类(通过Intent传递),不够灵活。
3.3.1、RxBus
RxBus并不是一个库,而是一种模式。相信大多数开发者都使用过EventBus,对RxBus也是很熟悉。由于MVVMabit中已经加入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);
}
在需要执行回调的地方发送
RxBus.getDefault().post(object);
3.3.2、Messenger
Messenger是一个轻量级全局的消息通信工具,在我们的复杂业务中,难免会出现一些交叉的业务,比如ViewModel与ViewModel之间需要有数据交换,这时候可以轻松地使用Messenger发送一个实体或一个空消息,将事件从一个ViewModel回调到另一个ViewModel中。
使用方法:
定义一个静态String类型的字符串token
public static final String TOKEN_LOGINVIEWMODEL_REFRESH = "token_loginviewmodel_refresh";
在ViewModel中注册消息监听
//注册一个空消息监听
//参数1:接受人(上下文)
//参数2:定义的token
//参数3:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() {
@Override
public void call() {
}
});
//注册一个带数据回调的消息监听
//参数1:接受人(上下文)
//参数2:定义的token
//参数3:实体的泛型约束
//参数4:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
}
});
在需要回调的地方使用token发送消息
//发送一个空消息
//参数1:定义的token
Messenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);
//发送一个带数据回调消息
//参数1:回调的实体
//参数2:定义的token
Messenger.getDefault().send("refresh",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);
token最好不要重名,不然可能就会出现逻辑上的bug,为了更好的维护和清晰逻辑,建议以
aa_bb_cc
的格式来定义token。aa:TOKEN,bb:ViewModel的类名,cc:动作名(功能名)。
为了避免大量使用Messenger,建议只在ViewModel与ViewModel之间使用,View与ViewModel之间采用ObservableField去监听UI上的逻辑,可在继承了Base的Activity或Fragment中重写initViewObservable()方法来初始化UI的监听
注册了监听,当然也要解除它。在BaseActivity、BaseFragment的onDestroy()方法里已经调用Messenger.getDefault().unregister(viewModel);
解除注册,所以不用担心忘记解除导致的逻辑错误和内存泄漏。
3.2、文件下载
文件下载几乎是每个app必备的功能,图文的下载,软件的升级等都要用到,mvvmhabit使用Retrofit+Okhttp+RxJava+RxBus实现一行代码监听带进度的文件下载。
下载文件
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) {
//下载成功的回调
}
@Override
public void progress(final long progress, final long total) {
//下载中的回调 progress:当前进度 ,total:文件总大小
}
@Override
public void onError(Throwable e) {
//下载错误回调
}
});
在ProgressResponseBody中使用了RxBus,发送下载进度信息到ProgressCallBack中,继承ProgressCallBack就可以监听到下载状态。回调方法全部执行在主线程,方便UI的更新,详情请参考例子程序。
3.3、ContainerActivity
一个盛装Fragment的一个容器(代理)Activity,普通界面只需要编写Fragment,使用此Activity盛装,这样就不需要每个界面都在AndroidManifest中注册一遍
使用方法:
在ViewModel中调用BaseViewModel的方法开一个Fragment
startContainerActivity(你的Fragment类名.class.getCanonicalName())
在ViewModel中调用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权限申请
对RxPermissions已经熟悉的朋友可以跳过。
使用方法:
例如请求相机权限,在ViewModel中调用
//请求打开相机权限
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("权限已经打开,直接跳入相机");
} else {
ToastUtils.showShort("权限被拒绝");
}
}
});
更多权限申请方式请参考RxPermissions原项目地址
3.5、图片压缩
为了节约用户流量和加快图片上传的速度,某些场景将图片在本地压缩后再传给后台,所以特此提供一个图片压缩的辅助功能。
使用方法:
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风格对话框工具类
SPUtils: SharedPreferences工具类
SDCardUtils: SD卡相关工具类
ConvertUtils: 转换相关工具类
StringUtils: 字符串相关工具类
RegexUtils: 正则相关工具类
KLog: 日志打印,含json格式打印
4、附加
4.1、编译错误解决方法
使用databinding其实有个缺点,就是会遇到一些编译错误,而AS不能很好的定位到错误的位置,这对于刚开始使用databinding的开发者来说是一个比较郁闷的事。那么我在此把我自己在开发中遇到的各种编译问题的解决方法分享给大家,希望这对你会有所帮助。
4.1.1、绑定错误
绑定错误是一个很常见的错误,基本都会犯。比如TextView的 android:text=""
,本来要绑定的是一个String类型,结果你不小心,可能绑了一个Boolean上去,或者变量名写错了,这时候编辑器不会报红错,而是在点编译运行的时候,在AS的Messages中会出现错误提示,如下图:
<img src="./img/error1.png" width="640" hegiht="640" align=center />
解决方法:把错误提示拉到最下面 (上面的提示找不到BR类这个不要管它),看最后一个错误 ,这里会提示是哪个xml出了错,并且会定位到行数,按照提示找到对应位置,即可解决该编译错误的问题。
注意: 行数要+1,意思是上面报出第33行错误,实际是第34行错误,AS定位的不准确 (这可能是它的一个bug)
4.1.2、xml导包错误
在xml中需要导入ViewModel或者一些业务相关的类,假如在xml中导错了类,那一行则会报红,但是res/layout却没有错误提示,有一种场景,非常特殊,不容易找出错误位置。就是你写了一个xml,导入了一个类,比如XXXUtils,后来因为业务需求,把那个XXXUtils删了,这时候res/layout下不会出现任何错误,而你在编译运行的时候,才会出现错误日志。苦逼的是,不会像上面那样提示哪一个xml文件,哪一行出错了,最后一个错误只是一大片的报错报告。如下图:
<img src="./img/error2.png" width="640" hegiht="640" align=center />
解决方法:同样找到最后一个错误提示,找到Cannot resolve type for xxx这一句 (xxx是类名),然后使用全局搜索 (Ctrl+H) ,搜索哪个xml引用了这个类,跟踪点击进去,在xml就会出现一个红错,看到错误你就会明白了,这样就可解决该编译错误的问题。
4.1.3、build错误
构建多module工程时,如出现【4.1.1、绑定错误】,且你能确定这个绑定是没有问题的,经过修改后出现下图错误:
<img src="./img/error3.png" width="640" hegiht="640" align=center />
解决方法:
这种是databinding比较大的坑,清理、重构和删build都不起作用,网上很难找到方法。经过试验,解决办法是手动创建异常中提到的文件夹,或者拷贝上一个没有报错的版本中对应的文件夹,可以解决这个异常
4.1.4、自动生成类错误
有时候在写完xml时,databinding没有自动生成对应的Binding类及属性。比如新建了一个activity_login.xml,按照databinding的写法加入<layout> <variable>
后,理论上会自动对应生成ActivityLoginBinding.java类和variable的属性,可能是as对databding的支持还不够吧,有时候偏偏就不生成,导致BR.xxx报红等一些莫名的错误。
解决方法:其实确保自己的写法没有问题,是可以直接运行的,报红不一定是你写的有问题,也有可能是编译器抽风了。或者使用下面的办法
第一招:Build->Clean Project;第二招:Build->Rebuild Project;第三招:重启大法。