我之前在多个 Android 应用中采用过多种途径来实现 MVP 设计模式,并且过程中经历了反复迭代。在历经多个项目后,我决定尝试以 Android Data Binding 类库为基础来实现 MVVM。这次尝试仿佛让我陷入了 Android 编程的极乐世界一般。
在带你尝试这些让我涅槃的步骤之前,我想先与你分享我在之前给自己设定的一些目标:
- 一个 MVVM 单元应当仅由 ViewModel(VM)、ViewModel 的状态(M)以及一个绑定的布局资源文件(V)构成。
- MVVM 单元应当是模块化的,并且支持嵌套。每个 MVVM 单元应支持包含一个或多个子单元,其中每个子单元仍可能包含自己的子单元。
- 不需要扩展 Activity 类、Fragment 类,或者自定义视图。
- 每个 ViewModel 的行为应当是可接受和可预期的,并且不依赖任何特殊的 Android 类库。应该可以使用 Vanilla JUnit 对其进行单元测试。
- ViewModel 间的关系应当通过依赖注入来实现。
- 应在布局文件中声明对 ViewModel 属性或者方法单向和双向的数据绑定。
- ViewModel 不应了解其所支持的 View 的细节。ViewModel 中不应当包含来自
theandroid.view
或者android.widgetpackages
的任何引用。 - ViewModel 应当自动绑定到与其配对的 View 的生命周期,并在生命周期结束后自动解除绑定。
- ViewModel 应当独立于 Activity 的生命周期,但是当 Activity 需要的时候也可以访问到 ViewModel。
- 这个模式需要支持单个或者多个 Activity 的情况。
写在前面的话
在开始的时候,我选择了一些不出名(但是同样好用的)工具:用于管理依赖注入的 Toothpick ,以及用于导航和管理栈回退(back-stack)的 Okuki (我自己写的)。我猜别人可能喜欢使用 Dagger 来管理依赖注入(DI),也可能喜欢使用 Intents、EnentBus 来完成导航功能,甚至于使用自定义的导航管理机制。你也可能倾向于使用 Activity 和 Fragments 来进行栈回退的管理。* 以上完全取决于个人。我仅推荐你遵循中心化和松耦合的原则来实现上述功能。只要保证这两个原则不变,采用了什么设计模式,如 MVP、MVVM,还是其他 UI 框架都不重要。
- 在文章最后包含了一种建议的栈回退的管理方式:FragmentManager。
基础 ViewModel 及其生命周期
接下来的步骤里,为了实现依赖注入、导航和栈回退,我定义了一个 ViewModel 基础接口,并规定了附加、分离相关 View 生命周期的方法。
首先我定义了一个 ViewModel 接口:
public interface ViewModel { void onAttach(); void onDetach(); }
下一步,我使用了 data binding 库中的 View.OnAttachStateListener
来实现绑定,然后将 android:onViewAttachedToWindow
和 android:onViewDetachedFromWindow
映射到我的 ViewModel 类的对应方法当中。我实现了这些方法,并将其关联到 ViewModel 接口的 onAttach
和 onDetach
方法上。通过这种方式,我可以在相应的扩展类当中隐藏所必需的 View
参数。此外,我还在 View 的生命周期中集成了依赖注入和 Rx 自动订阅机制。
我实现的 ViewModel 基础类:
public abstract class BaseViewModel implements ViewModel { private final CompositeDisposable compositeDisposable = new CompositeDisposable(); @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="<a href="http://schemas.android.com/apk/res/android%22">http://schemas.android.com/apk/res/android"</a>> > <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 绑定到一个视图以及视图的生命周期。下一步我需要一种一致的、模块化的方式将 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
,然后在绑定完成后通过显式地执行 addView(view)
方法来完成附加。我这样做的原因是为了 ViewModel 的 onViewAttachedToWindow
方法能够正常被调用,因为这个方法需要 View
在渲染之前就绑定 ViewModel。
现在我可以使用新的绑定关系了。在我的布局文件中,我通过新增 component
属性的方式来添加一个 ViewGroup 容器。
<layout xmlns:android="<a href="http://schemas.android.com/apk/res/android%22">http://schemas.android.com/apk/res/android"</a> xmlns:app="<a href="http://schemas.android.com/apk/res-auto%22">http://schemas.android.com/apk/res-auto"</a>> <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>
我通过使用 ObservableField<MvvmComponent>
来在我的 ViewModel 中提供断开组件的方式。
public class MyViewModel extends BaseViewModel { public final ObservableField<mvvmcomponent> myComponent = new ObservableField<>(); @Override public void onAttach() { myComponent.set(new HelloWorldComponent("World")); } }</mvvmcomponent>
组件类本身通过对父 ViewModel 的调用,提取出了资源 ID 和子 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 实例中保存、恢复的方式来实现,也可以通过实现对暂停、恢复事件的响应的办法来完成。这些都可以根据实际需求来选择,并且比较简单。只需要把这些事件委托给一个继承了 Application.ActivityLifecycleCallbacks
的单例类,就能实现。当然这个单例类需要注册到当前应用之上。这样这个单例类就能通过 Listeners 或者 Observables 来暴露出这些事件,并把他们注入到任何需要响应这些事件的 ViewModel 当中。
使用 Fragments 完成栈回退
我在本帖一开始就提到过,我的栈回退是通过自定义的库来实现的。但是仅需要一些简单的改动,你就能将其替换为 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 容器的属性。同时,需要关注的还有:配置变化时造成的关联影响、layoutResId 进程僵死,以及你的 MvvmFragment 的 vm 成员属性。适当的调整你的 Fragment 参数也很有必要。
现在你可以通过修改自定义组件的方式来使用你的 MvvmFragment,而不是直接渲染并绑定 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 来实现的(没有 Fragments)应用示例,可以在 这里 参考我的例子。
编程愉快!
查看英文原文: Zen Android MVVM
感谢冬雨对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论