原文:https://proandroiddev.com/zen-android-mvvm-160c26f3203c
在各种各样的Android应用上,用多种方式和迭代实现MVP模式后,我决定以Android的Data Binding库作为基础,探索MVVM。结果使得我处于一种接近Android编码狂喜的状态。
在带领你们穿过我采取的接近涅槃的步骤之前,我想先分享这次尝试的目标:
一个MVVM单元应该仅仅包含一个ViewModel(VM),一个状态(M)和一个绑定的布局资源文件(V)。
- 每个MVVM单元应该是模块化的和可嵌套的。一个MVVM单元应该可以包含一个或者多个子单元,子单元本身可能包括它们自己的子单元。
- 不需要扩展Activity、Fragment或者自定义View等基本类。
- ViewModel基本类是可接受的,同时也是可预期的。但是它必须不需要Android特定的依赖。它应该用平常的JUnit是可测试的。
- 所有的ViewModel依赖应该是注入的
- ViewModel属性和方法的单向和双向数据绑定,应该可以在布局文件中声明完成
- ViewModel必须不知道它支持的View。它应该不会从android.view和android.widget导入任何东西。
- ViewModel应该自动绑定到他匹配View的附上或者分离的生命周期。
- ViewModel应该独立于activity生命周期,但是需要的时候可以获取它。
- 这个模式不管采取单个或者多个activity的方法,都必须运行。
重要的事情先说
一开始,我选择了一些工具,处理一些低垂但是可口的果实:依赖注入的Toothpick、我自己的导航和管理回退栈的Okuki库。其他人可能认为DI使用Dagger;导航,你可能喜欢用Intent、EventBus或者其他的自定义导航管理机制;你可能更喜欢用Activity或者fragment作为回退栈管理。这些都不与我相关。我仅仅推荐用中心化和解耦的方式解决这些自己的问题,而不用管你是选取了MVP、MVVM或者其他的UI架构。
*提示,在文章最后介绍了建议的方法:用FragmentManager管理回退栈。
基本ViewModel和生命周期
需要处理DI、导航和回退栈,我下一步定义了一个ViewModel基类和一种绑定它到View装卸生命周期的机制。
首先定义一个ViewModel接口
public interface ViewModel {
void onAttach();
void onDetach();
}
下一步,我使用了View.OnAttachStateListener的绑定,它由Data Binding库提供,把android:onViewAttachedToWindow和android:onViewDetachedFromWindow属性映射到基本ViewModel基类的相应方法。我实现了这些方法,把它们联系到ViewModel接口的onAttach和onDetach方法,所以扩展类中可以隐藏必要的View参数。此外,我整合了依赖注入和为Rx订阅自动处理机制。我也把这些绑定到View的生命周期。
最后的BaseViewModel类如下:
public abstract class BaseViewModel implements ViewModel {
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
public BaseViewModel() {
App.inject(this);
}
@Override
public void onAttach() {
}
@Override
public void onDetach() {
}
public final void onViewAttachedToWindow(View view) {
onAttach();
}
public final void onViewDetachedFromWindow(View view) {
compositeDisposable.clear();
onDetach();
}
protected void addToAutoDispose(Disposable... disposables) {
compositeDisposable.addAll(disposables);
}
}
现在,使用扩展这个基类的任何ViewModel,只是简单地把ViewModel绑定到这个布局,把装卸属性映射到根ViewGroup,就像如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="vm" type="MyViewModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onViewAttachedToWindow="@{vm::onViewAttachedToWindow}"
android:onViewDetachedFromWindow="@{vm::onViewDetachedFromWindow}">
</FrameLayout>
</layout>
模块化单元
既然我一种方法把ViewModel绑定到View和它的生命周期,下一步,我需要一种方法把MVVM单元以一致和模块化的方式加载到一个容器之中。首先,我定义了一个接口,提供了ViewModel和布局资源之间的映射。
public interface MvvmComponent {
int getLayoutResId();
ViewModel getViewModel();
}
然后,我为MvvmComponent定义了一个自定义的数据绑定,可以渲染提供的布局,绑定到ViewModel,加载到ViewGroup里面:
@BindingAdapter("component")
public static void loadComponent(ViewGroup viewGroup, MvvmComponent component) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(
viewGroup.getContext()), component.getLayoutResId(), viewGroup, false);
View view = binding.getRoot();
binding.setVariable(BR.vm, component.getViewModel());
binding.executePendingBindings();
viewGroup.removeAllViews();
viewGroup.addView(view);
}
注意,在渲染的时候,把attachToParent参数设为false,然后在ViewModel绑定后,显式地执行addView(view)。
这么做的理由是,在渲染的View被装载之前需要ViewModel绑定,这样可以在ViewModel的onViewAttachedToWindow方法可以正常被调用。
现在可以用这个新的绑定。在我的布局中,我添加一个新的component属性来定义了一个ViewGroup容器:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="MyViewModel"/>
</data>
<FrameLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onViewAttachedToWindow="@{vm::onViewAttachedToWindow}"
android:onViewDetachedFromWindow="@{vm::onViewDetachedFromWindow}"
app:component="@{vm.myComponent}"
/>
</layout>
在绑定的ViewModel中,我使用了ObservableField提供了断开component的一种方式:
public class MyViewModel extends BaseViewModel {
public final ObservableField<MvvmComponent> myComponent
= new ObservableField<>();
@Override
public void onAttach() {
myComponent.set(new HelloWorldComponent("World"));
}
}
component类本身提取了布局资源的ID和从父ViewModel调用的子ViewModel的定义,只接受来自父ViewModel的初始化子ViewModel的数据:
public class HelloWorldComponent implements MvvmComponent {
private final String name;
public HelloWorldComponent(String name){
this.name = name;
}
@Override
public int getLayoutResId() {
return R.layout.hello_world;
}
@Override
public ViewModel getViewModel() {
return new HelloWorldViewModel(name);
}
}
现在,子部件很容易从ViewModel状态加载,ViewModel不需要知道关于布局、View或者其他的ViewModel。
Activity生命周期
就像起初打算的,MVVM单元是和Activity生命周期独立的。但是有时候我们可能需要访问它。我们可能需要用实例状态Bundle保存和存储数据,或者你可能需要响应pause/resume事件。这些需要的时候,可以很容易提供。仅仅把这些事件委派给一个单例,这个单例实现了Application.ActivityLifecycleCallbacks而且注册到Application。这个单例然后可以通过Listener或者Observable暴露这些事件,并且注入到ViewModel需要访问它们的任何事物。
用Fragment作为回退栈
就像帖子最开始提到的,我用了一个自定义库管理回退栈。然而,对上面代码的稍微调整,我使用Android的FragmentManager。一些额外的方法加到MvvmComponent接口:
public interface MvvmComponent {
int getLayoutResId();
ViewModel getViewModel();
String getTag();
boolean addToBackStack();
}
下一步,创建一个Fragment来包装你的MVVM单元,如下:
public class MvvmFragment extends Fragment {
private int layoutResId;
private ViewModel vm;
public MvvmFragment newInstance(int layoutResId, ViewModel vm){
MvvmFragment fragment = new MvvmFragment();
fragment.layoutResId = layoutResId;
fragment.vm = vm;
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewDataBinding binding = DataBindingUtil.inflate(inflater, layoutResId, container, false);
binding.setVariable(BR.vm, vm);
binding.setVariable(BR.fm, getChildFragmentManager());
return binding.getRoot();
}
public void setLayoutResId(int layoutResId){
this.layoutResId = layoutResId;
}
public void setViewModel(ViewModel vm){
this.vm = vm;
}
}
注意,布局需要用fm数据变量定义和设置为ViewGroup容器的属性。而且,注意configuration改变的含义和处理你MvvmFragment的layoutResId和vm属性
现在,你可以用MvvmFragment修改自定义的component的绑定,而不是直接渲染和绑定ViewModel:
@BindingAdapter({"component", "fm"})
public static void loadComponent(ViewGroup viewGroup, MvvmComponent component, FragmentManager fm) {
MvvmFragment fragment = fm.findFragmentByTag(component.getTag());
if(fragment == null) {
fragment = MvvmFragment.newInstance(component.getLayoutResId, component.getViewModel());
}
FragmentTransaction ft = beginTransaction();
ft.replace(viewGroup.getId, fragment, component.getTag());
if(component.addToBackStack()){
ft.addToBackStack(component.getTag());
}
ft.commit();
}
范例应用
用MVVM方法(没有用Fragment)的完整功能应用,可以看我的这个例子