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会加入DataBindingViewModelLiveDataretrofit并且使用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

更多相关文章

  1. “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
  2. Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
  3. 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
  4. Android中多个Activity间的数据共享
  5. Android(安卓)SQLite教程:内部架构及SQLite使用办法
  6. Android在开发中的实用技巧之Parcelable的使用以及如何传递复杂
  7. android ListView分页加载
  8. Android加密
  9. [置顶] JuheNews For aNdroid (改进版)

随机推荐

  1. PHP168整站系统山寨版闪亮登场
  2. PHP 源码 —— is_array 函数源码分析
  3. 无法从Ajax POST请求中将带空格的数据导
  4. phpMyAdmin的安装配置
  5. thinkphp5 编辑时 唯一验证 解决办法
  6. 无法从mysql中选择最新的而不是相同的数
  7. curl POST的数据大于1024字节
  8. PHP如何区分继承链中的$ this指针?
  9. 请问做PHP相关工作是不是基本都要懂前端
  10. php的冷门函数之——call_user_func_arra