Android(安卓)MVVM
1、MVVM 模式简介
MVVM 软件设计模式由微软在2005年提出,下图及介绍总结自微软The MVVM Pattern和Implementing the MVVM Pattern。上面两篇文章中和微软自家产品关联性很强,并很适用于Android,这里仅仅是介绍MVVM模式的概念及MVVM模式中各模块所承担的职责。
- View
就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI),决定如何呈现数据 - ViewModel
封装了View的显示逻辑和数据。不直接引用View。ViewModel实现来自View的命令(如点击事件)、处理(转换/聚合)View所需绑定的数据、通知View数据或状态的改变。ViewModel和数据和状态提供给View,但View决定了如何呈现。 - Model
封装了业务逻辑和数据(业务逻辑是指所有有关数据检索与处理的程序逻辑),并且保证数据的一致性和有效性。为了最大化重用机会,Model不应包含任何用于特定ViewModel的处理逻辑。 - Binder 绑定器
数据绑定技术的实现在MVVM中是必须的。Binder确保ViewModel中数据发生变化时能够及时通知View,使View呈现最新的数据。
2 、Android MVVM 模式
MVVM在不同的平台实现方式是有一定差异性的。在Google IO 2017 ,Google发布了一个官方应用架构库Architecture Components,这个架构库便是Google对Android应用架构的建议,也被称之为Android官方应用架构指南
。Android Architecture Components
在Google中国开发者网站中能找到。和Data Binding Library一样官方还没翻译为中文。
下图是Architecture的应用架构图。结合Android程序特点,整体上与微软的MVVM类似,但是做了更细致的模块划分。
View
显而易见 Activity/Fragment 便是MVVM中的View,当收到ViewModel传递来的数据时,Activity/Fragment负责将数据以你喜欢的方式显示出来。实际是View成还包括ViewDataBinding(根据xml自动生成),上面中并没有体现。ViewModel
ViewModel作为Activity/Fragment与其他组件的连接器。负责转换和聚合Model中返回的数据,使这些数据易于显示,并把这些数据改变及时的通知给Activity/Fragment。
ViewModel是具有生命周期意识
的,当Activity/Fragment销毁时ViewModel的onClear
方法会被回调,你可以在这里做一些清理工作。
LiveData是具有生命周期意识
的一个可观察的
的数据持有者,ViewModel中的数据由LiveData持有,并且只有当Activity/Fragment处于活动时才会通知UI数据的改变,避免无用的刷新UI;Model
Repository及其下方就是Model了。Repository负责提取和处理数据。数据可以来自本地数据库(Room),也可以来自网络,这些数据统一有Repository处理,对应隐藏数据来源及获取方式- Binder 绑定器
上图中并没有标出绑定器在哪里,其实在任何MVVM的实现中,数据绑定技术都是必须的。而上图仅仅是应用架构图。
Android中的数据绑定技术由 DataBinding和LiveData共同实现。当Activity/Fragment接收到来自ViewModel中的新数据时(由LiveData自动通知数据的改变),将这些数据通过DataBinding绑定到ViewDataBinding中,UI将会自动刷新,而不用书写类似setText
的方法。
3、Android MVVM 实战
上面都是一些理论,下面开始的按照Android Architecture Components
写一个的MVVM Demo。这个Dome会加入DataBinding
、ViewModel
、LiveData
、retrofit
并且使用java8
。不准备添加Room(数据库)
和Dagger2(依赖注入)
。
现在我们来写这个Dome
我们将在这个Dome里面通过Github用户的用户名,来获取具体的用户信息详情。其实Github返回很多,我们这里为了方便只显示用昵称,头像,公开库数量,最后修改时间。
效果图:
项目结构:
依赖:
首先,Android Studio 3.0
是必须的。然后添加依赖
..
android { ... //添加DataBinding支持 dataBinding { enabled = true } //添加java8支持 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }}dependencies { ... //LiveData,ViewModel implementation "android.arch.lifecycle:extensions:1.1.0" implementation "android.arch.lifecycle:common-java8:1.1.0" //网络请求 implementation "com.squareup.retrofit2:retrofit:2.3.0" implementation "com.squareup.retrofit2:converter-gson:2.3.0" //图片加载 implementation "com.github.bumptech.glide:glide:3.7.0" ...}
XML:
<?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> <import type="com.dome.mvvm.vo.Status" /> <variable name="eventHandler" type="com.dome.mvvm.ui.MainEventHandler" /> <variable name="user" type="com.dome.mvvm.vo.User" /> <variable name="loadStatus" type="Status" /> <variable name="resource" type="com.dome.mvvm.vo.Resource" /> data> <LinearLayout> <android.support.v7.widget.AppCompatEditText android:imeOptions="actionDone" android:inputType="text" android:lines="1" app:onInputFinish="@{(text)->eventHandler.onTextSubmit(text)}" /> <LinearLayout visibleGone="@{loadStatus==Status.SUCCESS}"> <ImageView app:imgUrl="@{user.avatarUrl}" /> <TextView android:text="@{@string/format_name(user.name)}" /> <TextView android:text="@{@string/format_repo(user.repoNumber)}" /> <TextView android:text="@{@string/format_time(user.lastUpdate)}" /> LinearLayout> <TextView visibleGone="@{loadStatus==Status.ERROR}" android:text="@{resource.message}" /> <ProgressBar style="?android:attr/progressBarStyleHorizontal" visibleGone="@{loadStatus==Status.LOADING}" android:indeterminate="true" /> LinearLayout>layout>
可以看到View的显示逻辑完全由数据驱动。 Activity只需要把相关的数据对象绑定到xml中,Data Binding 会自动把这些数据显示到相关的View。
事实上,Databinding会根据当前xml自动生成一个ViewDataBinding
的.java文件。上面写的有关属性与绑定都会在这个ViewDataBinding中实现。生成的ViewDataBinding在/app/build/generated/source/apt/debug/*包名*/databinding/
目录下,感兴趣可以看看。如果你对The mvp
这个框架有了解的话,就会发现它和DataBinding
的相似处,都是把View的显示逻辑放到Activity之外。接下来我们看MainEventHander.java:
MainEventHander
public class MainEventHandler { private MainActivity mainActivity; MainEventHandler(MainActivity mainActivity) { this.mainActivity = mainActivity; } /* * 这个方法由xml中的app:onInputFinish="@{(text)->eventHandler.onTextSubmit(text)}"调用。 */ public void onTextSubmit(String text) { mainActivity.onSearchUser(text); }}
这个java文件并不是必须的,你可以把点击事件直接放到Activity中去。之所以这样写,是不想让Activity去处理复杂的点击事件,简化Activity。
MainActivity
public class MainActivity extends AppCompatActivity { //自动生成的ViewDataBinding ,类名是根据xml名称自动生成 private ActivityMainBinding mainBinding; //ViewModel private MainViewModel mainViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 替换setContentView() mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); // 注意:这里不可以直接new MainViewModel() mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class); //设置事件处理器 mainBinding.setEventHandler(new MainEventHandler(this)); //获取userLiveData LiveData> userLiveData = mainViewModel.getUser(); //观察userLivedata中的数据(User)变化 userLiveData.observe(this, userResource -> { //绑定到DataBinding,set**()方法根据xml中的标签自动生成. mainBinding.setLoadStatus(userResource == null ? null : userResource.status); mainBinding.setUser(userResource == null ? null : userResource.data); mainBinding.setResource(userResource); }); } //eventHander调用这个 void onSearchUser(String text) { //通知ViewModel mainViewModel.setUserName(text); }}
Activity没有通过自身去获取数据,当数据返回时Activity也没有去处理数据,也没有处理简单显示逻辑,也没有处理点击事件(监听软件盘的输入完成+获取输入文字,在这里已经变成了onSearchUser)。这样Activity就被大大简化,没有动辄几百行的代码。
Activity的职责是:在数据更改时更新视图,或将用户操作通知给ViewModel
;
为什么不可以new MainViewModel ?
前面有说过ViewModel是具有
生命周期意识
的,但这并不是与生俱来的。直接new会让ViewModel的失去对生命周期的感知。
上述方式实际上是通过反射生成MainViewModel.class的对象,然后创建一个没有视图的Fragment添加到Activity,把这个viewModel对象交由Fragment持有,因为Fragment和Activity的生命周期是同步的,所以当Activity销毁时ViewModel的onClear()
会被回调并且销毁这个ViewModel。
上述写法使用的是默认的创建工厂(反射方式创建)。我们可以使用自定义的工厂来创建对象,我们可以在工厂里传入参数(一般都需要传参,这个简单而已)。而当我们使用了依赖注入(如dagger2)
后,就不需要传参了。为什么userLiveData不用removeObserve ?
和ViewModel一样,LiveData也能感知Activity的生命周期。当Activity销毁时,LiveData会自动的remove调,不用我们担心。
MainViewModel
public class MainViewModel extends ViewModel { private final UserRepo userRepo = UserRepo.getInstance(); private final MutableLiveData userNameLiveData = new MutableLiveData<>(); private final LiveData> userEntityLiveData; public MainViewModel() { //switchMap:当userNameLiveData中的数据发生变化时 触发input事件, userEntityLiveData = Transformations.switchMap(userNameLiveData, input -> { if (input == null) { return new MutableLiveData<>(); } else { //如果收到新的input(userName),那么就去UserRepo获取这个用户的信息 //返回值将赋值给userEntityLiveData; return userRepo.getUser(input); } }); } public LiveData> getUser() { return userEntityLiveData; } public void setUserName(String userName) { //将userName设置给userNameLiveData userNameLiveData.postValue(userName); }}
首先,ViewModel没有持有Activity对象或View对象,也必须不能持有这些对象。
其次,ViewModel不负责提取数据(如网络请求)。
而且,ViewModel不依赖特定的View
。他对所有引用它的对象提供相同的数据支持,也是是说同一个数据来源,我们可以有不同的展现方式。
ViewModel的职责是:1.处理数据逻辑,但是却不获取数据。2.作为Activity/Fragment 和其他组件之间的连接器
;
Repo
public class UserRepo { private static UserRepo userRepo = new UserRepo(); public static UserRepo getInstance() { return userRepo; } public LiveData> getUser(String userId) { MutableLiveData> userEntityLiveData = new MutableLiveData<>(); userEntityLiveData.postValue(Resource.loading(null)); //请求网络 ApiService.INSTANCE.getUser(userId).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { ApiResponse apiResponse = new ApiResponse<>(response); if (apiResponse.isSuccessful()) { userEntityLiveData.postValue(Resource.success(response.body())); } else { userEntityLiveData.postValue(Resource.error(apiResponse.errorMessage, null)); } } @Override public void onFailure(Call call, Throwable t) { userEntityLiveData.postValue(Resource.error(t.getMessage(), null)); } }); return userEntityLiveData; }}
虽然repo模块看上去没有必要,但他起着重要的作用。它为App的其他部分抽象出了数据源。现在我们的ViewModel并不知道数据是通过WebService来获取的,这意味着我们可以随意替换掉获取数据的实现。
ApiService
public interface ApiService { ApiService INSTANCE = new Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService.class); @GET("users/{login}") Call getUser(@Path("login") String login);}
超级简单的写法..
这里我们获取网络请求返回的是Call
对象,其实我们可以自定义一个转化器使retrofit
直接返回给我们LiveData<?>
对象。这个并不是mvvm的重点,所以这个dome里并没有这么做。
BindingAdapters
public class BindingAdapters { @BindingAdapter("visibleGone") public static void showHide(View view, boolean show) { view.setVisibility(show ? View.VISIBLE : View.GONE); } @BindingAdapter("imgUrl") public static void imgUrl(ImageView view, final String url) { Glide.with(view.getContext()).load(url).into(view); } @BindingAdapter("onInputFinish") public static void onInputFinish(TextView view, final OnInputFinish listener) { if (listener == null) { view.setOnEditorActionListener(null); } else { view.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE) { listener.onInputFinish(v.getText().toString()); } return false; }); } }}
上面xml
里面所使用的app:visibleGone
/ app:imgUrl
/ app:onInputFinish
属性都是这里定义的。前面两个很好理解,如果对onInputFinish
的参数理解不了,可以了解了java8 lambda
表达式相关知识。
4、最后
Dome 地址
- Dome Github 地址:https://github.com/zyawei/DomeMvvm
- Dome With Dagger2 :还没写..
- Google Architecture Sample : https://github.com/googlesamples/android-architecture-components
参考链接:
The MVVM Pattern : https://msdn.microsoft.com/en-us/library/hh848246.aspx
Implementing The MVVM Pattern : https://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx
Android Architecture Components : https://developer.android.com/topic/libraries/architecture/index.html
Android官方应用架构指南(中文) : http://www.cnblogs.com/zqlxtt/p/6895717.html
更多相关文章
- “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
- Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
- 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
- Android中多个Activity间的数据共享
- Android(安卓)SQLite教程:内部架构及SQLite使用办法
- Android在开发中的实用技巧之Parcelable的使用以及如何传递复杂
- android ListView分页加载
- Android加密
- [置顶] JuheNews For aNdroid (改进版)