我理解的MVVM架构及实战Demo
简介
MVVM架构模式,即Model-View-ViewModel三个层级,MVVM模式出来的时间已经很长了,网上关于MVVM模式的解析也有很多,我这里只说一下我自己的理解,基本上是和MVP模式相比较的一个差异。
先说一下MVVM中,我理解的各层的作用:
Model: 数据处理层,用来存储数据和处理数据,这一层的作用和MVP中的Model层类似;
View:UI显示更新和处理与用户的交互。这一层和MVP中的View层有一点区别,在MVP模式中,View的界面更新是由Presenter层驱动的,而在MVVM模式中,View的界面更新是采用观察者模式去动态监听数据变化,数据改变后由自己来改变,变被动为主动,这样View层与其他层的耦合度进一步降低了。并且若采用了
DataBinding
的形式后,View的UI数据更改完全由程序自动化更改,我们少去很多初始化工作和setText()
等繁复操作。ViewModel:处理逻辑中转任务的媒介,很类似于MVP模式中的Presenter层,但是它相比Presenter层的优点在于,它不用去通知View层去更新UI界面,降低了和View的耦合度,并且同一个ViewModel可以同时绑定多个View层,达到共同更新数据的目的。
MVVM模式中的一些相关知识点
Demo中会涉及一下一些关于MVVM模式的知识,不清楚的同学可以先了解一下,加深印象。
Lifecycle
Lifecycle是Google官方提供的方便管理生命周期事件的方式,可以更方便的监听生命周期变化事件,它以注解的方式作用在方法上,当生命周期方法调用时,它也会被跟随调用。
LiveData
LiveData是一种用来持有数据的对象,并且当数据改变时,去通知处于active
状态的观察者去做出相应的改变。在Android中,处于STARTED
或者RESUME
状态下的观察者被认为是active
状态。LiveData基于Lifecycle。
也就是说,当多个View层共享同一组数据时(一个Activity
和多个Fragment
),这组数据可以通过LiveData的形式持有,因为LiveData持有的数据是根据View的当前状态去通知的,因此,它不会产生一些类似于View被销毁后还去更改UI的一些奇怪的crash现象。
并且,LiveData通过Lifecycle的方式,已经监听了View的生命周期,它会跟随View层在View被销毁的时候销毁(旋转屏等重建行为不会受影响)。
DataBinding
Databing是Google发布的一个用来支持MVVM模式的框架,它的主要作用是用来降低布局和逻辑的耦合度。
以前,我们都是在布局xml文件中写好控件,然后通过findViewById
的形式去手动更新UI,DataBindind的出现,让我们省去了查找控件和手动更新UI的操作,我们仅需要把布局中的控件和Model层的数据关联起来,当数据改变时,UI也会跟着改变,达到双向绑定的目的。
实战演练
理论知识就介绍到这里,网上有很多相关的资料介绍,大家可以自行查找。这里,我通过实战Demo来告诉一下大家我理解的MVVM模式是什么样的,若有不正确,还请指正。
准备工作
这次的Demo也是参考网上其他人的博客来写的,功能是获取Bing网站首页的图片并显示,Bing网站首页图片的获取地址是:
https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
其中,idx
的数字表示不同的图片,获取到的Json文件格式如下:
{
"images":Array[1],
"tooltips":{
"loading":"正在加载...",
"previous":"上一个图像",
"next":"下一个图像",
"walle":"此图片不能下载用作壁纸。",
"walls":"下载今日美图。仅限用作桌面壁纸。"
}
}
其中,"images":Array[1]
中包含图片的URL和描述,展开后是这样的:
{
"startdate":"20190901",
"fullstartdate":"201909011600",
"enddate":"20190902",
"url":"/th?id=OHR.RamsauWimbachklamm_ZH-CN1602837695_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp",
"urlbase":"/th?id=OHR.RamsauWimbachklamm_ZH-CN1602837695",
"copyright":"拉姆绍的Wimbachklamm峡谷,德国巴伐利亚 (© Westend61 GmbH/Alamy)",
"copyrightlink":"https://www.bing.com/search?q=Wimbachklamm%E5%B3%A1%E8%B0%B7&form=hpcapt&mkt=zh-cn",
"title":"",
"quiz":"/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20190901_RamsauWimbachklamm%22&FORM=HPQUIZ",
"wp":true,
"hsh":"9f822f5607e3cec381408303a73c1986",
"drk":1,
"top":1,
"bot":1,
"hs":[
]
}
通过Json文件我们得到,真正的图片地址是https://www.bing.com/
开头,接上Json中Array
中的url
,即为图片地址。
下面不多说,直接开撸代码。
因为本Demo使用了DataBinding
+ LiveData
的形式,因此需要首先在module的build.gradle
文件中加上:
android {
...
dataBinding {
enabled = true
}
}
然后,依赖如下框架:
dependencies {
...
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
implementation 'com.github.bumptech.glide:glide:4.6.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'
}
当然,还要记得在AndroidManifest
文件上加上访问网络的权限:
<uses-permission android:name="android.permission.INTERNET" />
1、Model层
Model层的职责就是用来处理数据。
1.1 Bean文件
由于我们只关心Json数据中的url地址和图片描述,因此,我们的Bean文件极为简单:
获取Url和描述的Bean文件ImageUrlBean
:
public class ImageUrlBean {
private List<UrlBean> images;
public List<UrlBean> getImages() {
return images;
}
public void setImages(List<UrlBean> images) {
this.images = images;
}
public static class UrlBean {
public static final String BASE_IMAGE_ADDRESS_URL = "https://cn.bing.com/";
private String url;
private String copyright;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getCopyright() {
return copyright;
}
public void setCopyright(String copyright) {
this.copyright = copyright;
}
}
}
由于网络等其他原因会存在获取数据失败的风险,,因此在ImageUrlBean
的基础上,加封了一层数据层UrlData
,用来判断是否获取到了真实数据:
public class UrlData<T> {
private T data;
private String errorMsg;
public UrlData(){}
public UrlData(T data, String errorMsg) {
this.data = data;
this.errorMsg = errorMsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
}
1.2 封装网络
本次Demo的网络框架用的是Retrofit2
+ RxJava2
,不在本次的研究范围内,不了解的朋友请自行学习。
获取网络的接口IGetUrlService
:
public interface IGetUrlService {
@GET("HPImageArchive.aspx")
Observable<ImageUrlBean> getUrl(@Query("format") String format,
@Query("idx") int idx,
@Query("n") int n);
}
获取到Url数据后的回调GetUrlCallback
:
public interface GetUrlCallback {
void handleUrl(UrlData<ImageUrlBean.UrlBean> data);
}
网络工具类HttpUtil
:
public class HttpUtil {
private Retrofit mRetrifit;
private HttpUtil(){
mRetrifit = new Retrofit.Builder()
.baseUrl(ImageUrlBean.UrlBean.BASE_IMAGE_ADDRESS_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
}
public Observable<ImageUrlBean> getImageUrl(String format, int idx, int n) {
return mRetrifit.create(IGetUrlService.class)
.getUrl(format, idx, n);
}
public static HttpUtil getInstance() {
return Holder.mInstance;
}
private static class Holder {
private static final HttpUtil mInstance = new HttpUtil();
}
}
1.3 获取数据的Model
用来获取数据的ImageUrlModel
:
public class ImageUrlModel {
private HttpUtil mHttpUtil;
public ImageUrlModel() {
mHttpUtil = HttpUtil.getInstance();
}
public void getImageUrl(String format, int idx, int n, final GetUrlCallback callback) {
mHttpUtil.getImageUrl(format, idx, n)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ImageUrlBean>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ImageUrlBean imageUrlBean) {
UrlData<ImageUrlBean.UrlBean> data =
new UrlData<>(imageUrlBean.getImages().get(0), null);
callback.handleUrl(data);
}
@Override
public void onError(Throwable e) {
UrlData<ImageUrlBean.UrlBean> data = new UrlData<>(null, e.getMessage());
callback.handleUrl(data);
}
@Override
public void onComplete() {
}
});
}
}
2、ViewModel层
ViewModel层的职责是用来处理中转逻辑,将真正获取数据的操作交给Model去执行,然后将获取到的数据更改到自身的LiveData中去。LiveData中的数据改变后,会去通知监听它改变的、当前状态是可见的View们去更改UI。
ImageUrlViewModel
:
public class ImageUrlViewModel extends ViewModel {
public static final int STATE_NO_PRE_IMAGE = 0; // 没有前一张图片时的状态
public static final int STATE_NORMAL_IMAGE = 1; // 图片正常获取的状态
/**
* 放在LiveData中的数据,会被View监听,
* 当数据改变且View层属于可见状态,则会去通知View层更新数据
*/
private MutableLiveData<UrlData<ImageUrlBean.UrlBean>> mData;
// 用来当特殊情况下更新UI,比如没有前一张图片时,提醒View层弹出Toast提醒用户
private MutableLiveData<Integer> mState;
/**
* 本例没有用到Context,
* 若需要,则必须传Application的Context,达到与View层独立的目的。
*/
private Context mContext;
private ImageUrlModel model;
private GetUrlCallback callback;
private int index; // 当前获取的哪一张图片的索引
public ImageUrlViewModel(){
this(null);
}
public ImageUrlViewModel(Context context) {
this.mContext = context != null ?
context.getApplicationContext() : null;
mData = new MutableLiveData<>();
mState = new MutableLiveData<>();
model = new ImageUrlModel();
callback = new GetUrlCallback() {
@Override
public void handleUrl(UrlData<ImageUrlBean.UrlBean> data) {
// ViewModel层仅仅处理数据,界面的改变逻辑由Activity去执行
mData.setValue(data);
}
};
}
public MutableLiveData<UrlData<ImageUrlBean.UrlBean>> getImageUrl() {
return mData;
}
public MutableLiveData<Integer> getState() {
return mState;
}
public void loadImage() {
model.getImageUrl("js", index, 1, callback);
}
public void loadNextImage() {
index++;
model.getImageUrl("js", index, 1, callback);
}
public void loadPreImage() {
if(index == 0) {
// 没有数据时,通过更改state的值,去通知View层更改数据
mState.setValue(STATE_NO_PRE_IMAGE);
mState.setValue(STATE_NORMAL_IMAGE);
return;
}
index--;
model.getImageUrl("js", index, 1, callback);
}
}
3、View层
首先编写xml布局,由于本例采用DataBinding
框架,因此布局可能有所不同:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
>
<data>
<variable
name="image"
type="com.zw.mvvmdemo.bean.ImageUrlBean.UrlBean"/>
<!-- 用来处理点击事件 -->
<variable
name="clicker"
type="com.zw.mvvmdemo.BingImageActivity.Clicker"/>
<!-- 用来处理一些特殊UI更新变化 -->
<variable
name="uiChanger"
type="Integer"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv"
url="@{image.BASE_IMAGE_ADDRESS_URL + image.url}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="200dp" />
<TextView
android:id="@+id/tv"
android:text="@{image.copyright}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv"
android:layout_marginTop="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"/>
<Button
android:id="@+id/btn_pre"
android:onClick="@{clicker.onClick}"
android:layout_width="0dp"
android:layout_height="50dp"
android:text="上一张"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv"
android:layout_marginTop="20dp"
app:layout_constraintWidth_percent="0.5"/>
<Button
android:id="@+id/btn"
android:onClick="@{clicker.onClick}"
android:layout_width="0dp"
android:layout_height="50dp"
android:text="加载"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_pre"
android:layout_marginTop="20dp"
app:layout_constraintWidth_percent="0.5"/>
<Button
android:id="@+id/btn_next"
android:onClick="@{clicker.onClick}"
android:layout_width="0dp"
android:layout_height="50dp"
android:text="下一张"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn"
android:layout_marginTop="20dp"
app:layout_constraintWidth_percent="0.5"/>
</android.support.constraint.ConstraintLayout>
</layout>
布局和我们常见的布局不一样,根标签是<layout>
,然后是两个平级的标签<data>
和真正的布局文件,布局文件比较好理解,就不过多介绍了,介绍一下<data>
:
当你需要与布局文件中的某个控件进行双向绑定的时候,可以将你的某个对象与控件绑定。比如上述文件中的
<variable
name="image"
type="com.zw.mvvmdemo.bean.ImageUrlBean.UrlBean"
/>
它与布局文件中的
<ImageView
android:id="@+id/iv"
url="@{image.BASE_IMAGE_ADDRESS_URL + image.url}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
对应,其中variable
标签中:
-
name
:需要绑定数据的名称,可按照自己的命名习惯命名; -
type
:需要绑定数据的类;
在此例中,当需要用到UrlBean
时,可以通过@{}
操作符并通过name
来填写符合需求的表达式。比如在此例中,我们给ImageView
的url
属性绑定了UrlBean
这个实例,当UrlBean
的image.url
变化时,ImageView
的url
也会跟着改变。
url
是我们自定义的一个属性,这个属性并不是在style.xml
文件中定义的,而是通过DataBinding
的注解BindingAdapter
来完成的,我们可以通过自定义属性,来完成我们想要进行处理的逻辑操作。
GetBingImageAdapter
:
public class GetBingImageAdapter {
@BindingAdapter("url")
public static void setImage(ImageView iv, String url) {
Glide.with(iv)
.load(url)
.into(iv);
}
}
当ImageView
中的url改变时,会回调此Adapter
,然后回调setImage()
。
然后,我们看看BingImageActivity
:
public class BingImageActivity extends AppCompatActivity {
private ActivityBingImageBinding binding;
private ImageUrlViewModel mViewModel;
private ProgressDialog mDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil
.setContentView(this, R.layout.activity_bing_image);
// 建立ViewModel,并将Activity的生命周期绑定到ViewModel上
mViewModel = new ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()))
.get(ImageUrlViewModel.class);
// 为ViewModel的UrlData建立数据监听,并监听数据变化,根据数据更新UI
mViewModel.getImageUrl().observe(this, new Observer<UrlData<ImageUrlBean.UrlBean>>() {
@Override
public void onChanged(@Nullable UrlData<ImageUrlBean.UrlBean> data) {
mDialog.dismiss();
if(data.getErrorMsg() != null) {
Toast.makeText(BingImageActivity.this, data.getErrorMsg(),
Toast.LENGTH_LONG).show();
}
// 监听到数据变化后,通过databinding更改布局UI,若未使用databinding,则需要自己写相关UI更新逻辑
binding.setImage(data.getData());
}
});
// 监听特殊状态下更新UI的操作
mViewModel.getState().observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mDialog.dismiss();
if(integer == ImageUrlViewModel.STATE_NO_PRE_IMAGE) {
Toast.makeText(BingImageActivity.this, "没有前一张图片了!",
Toast.LENGTH_LONG).show();
}
}
});
binding.setClicker(new Clicker());
mDialog = new ProgressDialog(this);
mDialog.setTitle("加载中");
mDialog.show();
mViewModel.loadImage();
}
public class Clicker {
public void onClick(View view) {
mDialog.show();
switch (view.getId()) {
case R.id.btn_pre:
mViewModel.loadPreImage();
break;
case R.id.btn:
mViewModel.loadImage();
break;
case R.id.btn_next:
mViewModel.loadNextImage();
break;
}
}
}
}
实战总结
这次编写的MVVM模式的Demo,让我对MVVM模式有了一点理解,它的优点和缺点都同样明显。
优点:
- 数据和布局文件实现双向绑定,我们仅关心数据的改变即可,View层UI的改变会通过数据的改变而改变。省去写
findViewById
和setText()
等繁复的代码的工作。 - View层更新UI的操作完全由自己决定,而无需其他层级的通知,变被动为主动,同时也减轻了其他层级的负担。
- View层和ViewModel层完全解耦,并且ViewModel层可同时绑定多个View层,去同时更新数据的变化。
缺点:
- View层对ViewModel层数据变化采用监听的方式,若数据种类过多,则需要写多个监听器观测。
- 若ViewModel层数据未改变,而需要View层去改变UI时,则非常不方便,需要使用其他方式实现(比如增加额外监听器)。
- Android Studio对MVVM模式的支持还不是很完善,
Binding类
的生成可能需要多次Rebuild
的操作,若Binding类
过多,则编译工作会耗时很大; - MVVM模式下,因为View层和ViewModel层完全独立的关系,程序如果报错则很难定位具体问题,排查问题困难。
总的来说,MVVM模式我认为还是一个非常优秀的模式,它省去了MVP模式中编写巨量接口的麻烦,同时将View层和ViewModel层完全解耦,若可以接受上述总结中的缺点项,我认为项目完全可以采用MVVM模式去进行开发。
Demo下载