/DomeMvvm

mvvm dome

Primary LanguageJava

1、MVVM 模式简介

MVVM 软件设计模式由微软在2005年提出,下图及介绍总结自微软The MVVM PatternImplementing 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类似,但是做了更细致的模块划分。

来自Google开发者网站

  • 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中的数据绑定技术由 DataBindingLiveData共同实现。当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中一些不重要的属性,仅保留了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">

    <data>
	    <!--导包,类似java导包。下面要用到这个枚举进行判断-->
        <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>
		<!--app:onInputFinish,这个是自定义的接口,当输入完成后回调eventHandler.onTextSubmit(text)。-->
		<!--BindingAdapter相关知识-->		
        <android.support.v7.widget.AppCompatEditText
            android:imeOptions="actionDone"
            android:inputType="text"
            android:lines="1"
            app:onInputFinish="@{(text)->eventHandler.onTextSubmit(text)}" />

	    <!--visibleGone,自定义的BindingAdapter,处理View的显示和隐藏-->
	    <!--当loadStatus为SUCCESS时显示此LinearLayout,绑定具体的用户信息-->   
        <LinearLayout visibleGone="@{loadStatus==Status.SUCCESS}">         
	        <!--imgUrl,自定义的BindingAdapter,绑定ImageView的url,由Glide处理-->   
            <ImageView app:imgUrl="@{user.avatarUrl}" />
            <!--@string,引用字符串,格式化user.name-->   
            <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>

		<!--当loadStatus为ERROR时显示此View,text绑定错误信息-->
        <TextView
            visibleGone="@{loadStatus==Status.ERROR}"
            android:text="@{resource.message}" />
		<!--当loadStatus为LOADING时显示此View,表示正在请求-->
        <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<Resource<User>> userLiveData = mainViewModel.getUser();
        //观察userLivedata中的数据(User)变化
        userLiveData.observe(this, userResource -> {
			//绑定到DataBinding,set**()方法根据xml中的<var.. >标签自动生成.
            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<String> userNameLiveData = new MutableLiveData<>();
    private final LiveData<Resource<User>> 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<Resource<User>> 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<Resource<User>> getUser(String userId) {
        MutableLiveData<Resource<User>> userEntityLiveData = new MutableLiveData<>();
        userEntityLiveData.postValue(Resource.loading(null));
        //请求网络
        ApiService.INSTANCE.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                ApiResponse<User> 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<User> 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<User> getUser(@Path("login") String login);
}

超级简单的写法.. 这里我们获取网络请求返回的是Call<User>对象,其实我们可以自定义一个转化器使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 地址

参考链接: