Android中MVVM是什么?
转载请标明出处:
https://blog.csdn.net/xuehuayous/article/details/82777022;
本文出自:【Kevin.zhou的博客】
前言:去年对项目的架构进行了调整,迁移到了MVVM架构,还好之前的代码写的还算清晰,在调整的过程中也没有遇到太多的问题。改造的过程中也查找了大量资料,不管是架构相关的还是针对MVVM的代码或文章,发现在Android中对于MVVM大家还没有达成共识。或许每一种方式在固定的业务场景中都是有益的,并没有谁对谁错。之前在技术群里也听到有很多人想了解MVVM,正好我也总结下希望对想了解的朋友能起到帮助作用;如果有不同的意见,也许是我功力尚浅,希望能够留下评论,我们共同探讨,相互学习。
关于什么是MVC/MVP/MVVM以及他们之间的区别,这里就不赘述了,如果想了解网上的资料很多,而且很容易陷入他们的对比,最终搞的云里雾里。
一句话了解MVVM
- View 只处理用户的即时交互;
- ViewModel 只处理业务逻辑;
- Model 只处理数据存储与获取。
View
View层,不再是我们之前理解的是一个TextView、LinearLayout等View控件,只要是可以和用户进行交互的都可以归属到View层,比如:Activity、Fragment、Dialog、PopupWindow、XML布局、布局Adapter、系统及自定义View控件等等,只要是用户看到的、摸到的都归View层管。也就是说禁止在这里做业务逻辑、数据操作等和View无直接相关的事情。
Model
model层,与我们之前把定义的实体bean对象称为model不同的是,这里的model被赋予了数据管理的职责。数据的管理包括数据存储与数据获取,这里存储的位置不仅限于本地(SharedPreferences、SQLite),而且也会是网络上的任何存储方式。
ViewModel
ViewModel层,说是只处理业务逻辑,更准确的说法是Model层和View层的粘合剂,从Model中获取数据整合之后提供给View层进行显示,响应View层的事件调用Model层进行响应的落地。
它们的关系
通过以上大致了解了View-ViewModel-Model是什么、主要干什么,通过这三个层次的定义把视图、业务、数据进行了切割,这样职责与分层清楚了,那他们之间怎么传递数据呢?
View层包含两大类,Activity、Fragment、Dialog、PopupWindow等代码类和XML布局类,代码类与布局类交互的方式是DataBinding,通过DataBinding的双向绑定技术可以轻松实现View数据的双向通知。
Model层数据的获取与保存绝大部分应该是在服务端的,网络访问是数据交互的重头戏,这部分必须在异步中完成,异步处理方面大家都弃Handler而去投奔了Rx的怀抱,所有在Model层的网络部分与ViewModel层交互通过Rx的观察者模式Observable。
ViewModel层从Model层获取数据后,需求将数据通知到View层并显示出来,采用Android架构组件中的LiveData。
Demo
通过以上说明,大家应该是对MVVM架构有了大致的了解,并且具体怎么操作是云里雾里。下面就以一个示例的方式进行说明。思考再三我决定用Java而不是Kotlin来进行编写,尽管Kotlin用起来很舒服,但是照顾到可能一部分朋友不太了解。
需求一
需求内容
编写字符串,保存到本地,再读取出来修改下并保存。
撸码
XML布局
布局很简单,上面一个EditText,下面是两个Button。
也许有朋友对这种布局的编写有些疑惑,请移步上一篇博客《认识Android中的双向绑定》 ,对Android中的databinding进行初识。
其中android:text="@={view.content}"
是做数据双向绑定用的;
android:onClick="@{view::onSaveClick}"
即可调用view中的onSaveClick(view)
方法。
<?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"> <data class="com.kevin.mvvmdemo.MainActivityBinding"> <variable name="view" type="com.kevin.mvvmdemo.MainActivity" /> data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp"> <EditText android:id="@+id/et_content" android:layout_width="0dp" android:layout_height="125dp" android:background="#EEEEEE" android:hint="请输入内容" android:inputType="textMultiLine" android:padding="8dp" android:text="@={view.content}" android:textSize="16sp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_save" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="保存" android:onClick="@{view::onSaveClick}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/btn_load" app:layout_constraintTop_toBottomOf="@+id/et_content" app:layout_constraintVertical_chainStyle="spread" /> <Button android:id="@+id/btn_load" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="加载" android:onClick="@{view::onLoadClick}" app:layout_constraintLeft_toRightOf="@+id/btn_save" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/et_content" app:layout_constraintVertical_chainStyle="spread" /> android.support.constraint.ConstraintLayout>layout>
编写MainActivity
创建MainActivity,Activity中为设置databinding及两个回调方法,onSaveClick(view)
、onLoadClick(view)
这两个方法是在XML布局中调用的。
public class MainActivity extends AppCompatActivity { private MainActivityBinding mBinding; public MutableLiveData<String> content = new MutableLiveData<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBinding = MainActivityBinding.inflate(getLayoutInflater()); mBinding.setLifecycleOwner(this); setContentView(mBinding.getRoot()); mBinding.setView(this); } public void onSaveClick(View view) { Toast.makeText(this, "点击了 保存 按钮", Toast.LENGTH_SHORT).show(); } public void onLoadClick(View view) { Toast.makeText(this, "点击了 加载 按钮", Toast.LENGTH_SHORT).show(); }}
目前看到的效果是这样的,Activity中响应XML布局的的点击。
注意
这里需要注意的是,在《认识Android中的双向绑定》中可观察对象使用的是:
public ObservableField<String> content = new ObservableField<>();
这里使用的是:
public MutableLiveData<String> content = new MutableLiveData<>();
其中ObservableField
是databinding中提供的,MutableLiveData
是android arch 中提供的,二者都可以,Google推荐是MutableLiveData
,Android架构会管理它的生命周期,在使用MutableLiveData
,需要添加如下:
mBinding.setLifecycleOwner(this);
编写MainRepository
创建MainRepository,Repository
主要做的就是数据的获取与保存,这里简单的保存在SharedPreferences
,提供了保存和读取的两个方法。
public class MainRepository { private Application mApplication; public MainRepository(Application application) { this.mApplication = application; } public void saveData(String content) { SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE); sp.edit().putString("content", content).commit(); } public String loadData() { SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE); return sp.getString("content", ""); }}
编写MainViewModel
创建MainViewModel,继承自Android架构组件中的ViewModel
或者AndroidViewModel
,其中AndroidViewModel
运行传递Application
对象。
作为业务逻辑的处理者以及Model层与View层的粘结剂,这里没有需要处理的物业逻辑,仅仅是对数据层封装了一下,提供给View层使用。
public class MainViewModel extends AndroidViewModel { private MainRepository mRepository; public MainViewModel(@NonNull Application application) { super(application); mRepository = new MainRepository(application); } public void setData(String content) { mRepository.saveData(content); } public String getData() { return mRepository.loadData(); }}
修改MainActiviy
添加依赖库
实例化ViewModel需要用到Android架构组件中的另外一个库,在build.gradle中添加依赖:
dependencies { // ... ... implementation 'android.arch.lifecycle:extensions:latest.release'}
实例化ViewModel
在onCreate()方法中初始化ViewModel:
mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
调用ViewModel方法
之后就可以在点击的监听中进行响应的操作啦,代码如下:
public void onSaveClick(View view) { mViewModel.setData(content.getValue()); } public void onLoadClick(View view) { String data = mViewModel.getData(); content.setValue(data); }
完整代码
public class MainActivity extends AppCompatActivity { public MutableLiveData<String> content = new MutableLiveData<>(); private MainActivityBinding mBinding; private MainViewModel mViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBinding = MainActivityBinding.inflate(getLayoutInflater()); mBinding.setLifecycleOwner(this); setContentView(mBinding.getRoot()); mBinding.setView(this); mViewModel = ViewModelProviders.of(this).get(MainViewModel.class); } public void onSaveClick(View view) { mViewModel.setData(content.getValue()); } public void onLoadClick(View view) { String data = mViewModel.getData(); content.setValue(data); }}
效果
通过以上,就完成了一个最简单的MVVM,效果如下:
大家可能觉得这么简单的东西,我在Activity中简单的几行就能搞定,你却来这么一堆调用,不是很麻烦。那么我们做一下修改呢?
需求二
需求内容
由于输入的字数不能够无线长,那我们限制最长为100个字,太少了也不行,最少10个字,如果不合法就提示。
撸码
修改布局
增加两个文案
<TextView android:id="@+id/tv_max_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:layout_marginRight="8dp" android:text="/100" android:textColor="@color/gray" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="@+id/et_content" app:layout_constraintRight_toRightOf="@+id/et_content" /><TextView android:id="@+id/tv_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="@+id/et_content" app:layout_constraintRight_toLeftOf="@+id/tv_max_count" tools:text="0" />
大家先想一下,如果用原生的怎么搞?给EditText控件设置内容更改监听,当变化后将获取内容长度设置给TextView。不复杂,就是一堆模板代码。
但是如果使用databinding,也许你不信,两行代码就搞定。
<TextView android:id="@+id/tv_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:text="@{String.valueOf(view.content.length())}" android:textColor="@{(view.content.length() > 9 && view.content.length() < 101) ? @color/gray : @color/red, [email protected]/red}" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="@+id/et_content" app:layout_constraintRight_toLeftOf="@+id/tv_max_count" tools:text="0" />
一行是动态绑定text,一行是动态设置颜色。
其中设置文案的比较好理解,就是获取可观察对象(输入内容)的长度,设置给TextView显示。
android:text="@{String.valueOf(view.content.length())}"
下面设置颜色的,可观察对象(输入内容)的长度大于9 并且 小于 101 则显示灰色,其他情况显示红色。
android:textColor="@{(view.content.length() > 9 && view.content.length() < 101) ? @color/gray : @color/red, [email protected]/red}"
通过以上演示大家已经看到了View层databinding的好处。
如果输入不合法的时候不允许提交怎么做呢?想必大家都想到了在XML中处理。
<Button android:id="@+id/btn_save" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="8dp" android:enabled="@{view.content.length() > 9 && view.content.length() < 101, default=false}" android:onClick="@{view::onSaveClick}" android:text="保存" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/btn_load" app:layout_constraintTop_toBottomOf="@+id/et_content" app:layout_constraintVertical_chainStyle="spread" />
也是一行代码就可以搞定:
android:enabled="@{view.content.length() > 9 && view.content.length() < 101, default=false}"
需求三
需求内容
如果我想保存到网络呢?
撸码
本想着在我的服务器上搭一个简单的服务,提供保存和读取的接口。思来想去,我们的重点不在这里,于是改为本地模拟的形式。
网络实体
由于网络交互中有共同的状态、信息、数据等部分,这里进行抽取下。不太了解的同学可以看下我之前的一篇博客《一步步封装Retrofit + RxJava2》,这里有MVVM架构中Model层是怎样一步步生成的。
public class HttpResult<T> { public int status; public String msg; public T data;}
模拟网络交互
仍然是两个方法,保存数据和获取数据,获取数据返回的是具体数据,
在保存数据中,返回的是数据本身。并且每个交互都做了延迟2秒的操作。
public class NetWork { private Application mApplication; public NetWork(Application application) { this.mApplication = application; } public Observable<HttpResult<String>> saveData(final String content) { return Observable.create( new ObservableOnSubscribe<HttpResult<String>>() { @Override public void subscribe(ObservableEmitter<HttpResult<String>> e) { SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE); boolean success = sp.edit().putString("content", content).commit(); HttpResult httpResult = new HttpResult(); if (success) { httpResult.status = 200; httpResult.msg = "保存成功"; } else { httpResult.status = 5; httpResult.msg = "保存失败"; } httpResult.data = content; e.onNext(httpResult); e.onComplete(); } } ) .delay(2, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } public Observable<HttpResult<String>> loadData() { return Observable.create( new ObservableOnSubscribe<HttpResult<String>>() { @Override public void subscribe(ObservableEmitter<HttpResult<String>> e) { SharedPreferences sp = mApplication.getSharedPreferences("demo", Context.MODE_PRIVATE); HttpResult httpResult = new HttpResult(); httpResult.data = sp.getString("content", ""); httpResult.status = 200; httpResult.msg = "获取成功"; e.onNext(httpResult); e.onComplete(); } } ) .delay(2, TimeUnit.SECONDS) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); }}
MainRepository修改
之前在MainRepository中进行了SharedPreferences的操作,如果把MainRepository定义为数据仓库是明显不合理的,他不能涉及具体的操作,应该做在哪(本地、网络)获取、存储数据,以及缓存策略等等。让它回归本质,调用我们模拟的网络层。
public class MainRepository { private NetWork mNetWork; public MainRepository(Application application) { this.mNetWork = new NetWork(application); } public Observable<String> saveData(String content) { return mNetWork.saveData(content) .map(new Function<HttpResult<String>, String>() { @Override public String apply(HttpResult<String> result) { return result.data; } }); } public Observable<String> loadData() { return mNetWork.loadData() .map(new Function<HttpResult<String>, String>() { @Override public String apply(HttpResult<String> result) { return result.data; } }); }}
由于网络层向上暴露的返回是带有网络层信息的Observable
,那这里要把HttpResult
转换掉,通过Rx的map
操作符可以轻松完成。
MainViewModel修改
为了使用方便,将之前定义在MainActivity中的可观察对象移动到ViewModel层,调用MainRepository进行对应操作。
public class MainViewModel extends AndroidViewModel { private MainRepository mRepository; public MutableLiveData<String> content = new MutableLiveData<>(); public MainViewModel(@NonNull Application application) { super(application); mRepository = new MainRepository(application); } public void setData() { mRepository.saveData(content.getValue()).subscribe(); } public void getData() { mRepository.loadData().subscribe(new Consumer<String>() { @Override public void accept(String s) { content.setValue(s); } }); }}
效果
需求四
需求内容
给网络访问加上等待圈
撸码
添加加载圈,肯定是在View层进行添加,但是数据获取真正执行在Model层,调用在ViewModel层,所以调用的时候才知道什么时候该显示、什么时候隐藏。但是我们也知道View层持有ViewModel层的引用,ViewModel层是不可能去调用View层方法的,那么该怎么办呢?
可以在View层添加对ViewModel层的数据监听,这样显示与隐藏改变的时候View层即可做相应的操作。
XML布局修改
在布局中添加ProgressBar
<ProgressBar android:id="@+id/loading" android:layout_width="38dp" android:layout_height="38dp" android:layout_gravity="center_horizontal" android:indeterminateBehavior="repeat" android:indeterminateDrawable="@drawable/circular" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:visibility="gone"/>
ViewModel修改
添加可观察对象,并且在方法执行前设置显示,在方法执行后设置隐藏。
public class MainViewModel extends AndroidViewModel { private MainRepository mRepository; public MutableLiveData<String> content = new MutableLiveData<>(); public MutableLiveData<Boolean> showLoading = new MutableLiveData(); // 是否显示加载圈 public MainViewModel(@NonNull Application application) { super(application); mRepository = new MainRepository(application); } public void setData() { showLoading.setValue(true); mRepository.saveData(content.getValue()).subscribe(new Consumer<String>() { @Override public void accept(String s) { showLoading.setValue(false); } }); } public void getData() { showLoading.setValue(true); mRepository.loadData().subscribe(new Consumer<String>() { @Override public void accept(String s) { content.setValue(s); showLoading.setValue(false); } }); }}
MainActivity修改
在onCreate()
方法中添加对viewModel的可观察对象的监听,进而对ProgressBar
设置显示与隐藏。
mViewModel.showLoading.observe(this, new Observer<Boolean>() { @Override public void onChanged(@Nullable Boolean showLoading) { mBinding.loading.setVisibility(showLoading ? View.VISIBLE : View.GONE); } });
效果
到目前为止,已经完成了MVVM的小Demo,尽管还有很多小细节要处理,看下效果吧。
这样如果之后觉得这个加载圈不好看,也只需要更改View层的就可以了,ViewModel层的内容不必更改,ViewModel层不关心显示的样式,它只是给关心该数据(注册了数据监听)的进行通知,至于怎么显示它不会理会。
总结
通过以上大致了解了View-ViewModel-Model是什么,通过这三个层次的定义把视图、业务、数据进行了切割,了解了它们之间如何传递数据。
View 只处理用户的即时交互,ViewModel 只处理业务逻辑,Model 只处理数据存储与获取。View 持有 ViewModel ,直接调用ViewMode中的方法,ViewModel持有Model,View层响应用户事件通过ViewModel层调用Model层数据,数据通过可观察对象的形式上传到View层进行显示。
源码下载 Android-MVVM-demo
马上中秋了,预祝大家中秋节快乐!!!
更多相关文章
- android在代码中为new出的控件设置ID及setId()异常
- android activity与fragment的生命周期详细研究
- Android(安卓)布局文件的性能--使用include标签重用Layout---转
- Android(安卓)高级控件-ListView的扩展
- Android中的一个简单的List应用
- Android(安卓)四大组件——Service 生命周期
- Android(安卓)总结:ContentProvider 的使用
- Android(安卓)MVP设计模式实例详解
- Android(安卓)播放SD卡上指定的一首歌(初级版)