安卓 MVVM 之禅

阅读 1467
收藏 80
2017-09-06
原文链接:mp.weixin.qq.com
版权声明
作者:Cain Wong
译者:李瑞丰
原文:https://proandroiddev.com/zen-android-mvvm-160c26f3203c
本文由作者授权翻译并发布,未经允许不得转载。

我之前在多个 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:

https://github.com/stephanenicolas/toothpick

以及用于导航和管理栈回退(back-stack)的 Okuki(我自己写的):

https://github.com/wongcain/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 基础类:

现在,就可以直接使用该基类的任意 ViewModel 扩展了。你只需要将相应的 ViewModel 绑定到这个布局当中,同时把附加、分离属性映射到根 ViewGroup 即可。就像下面这样:

模块化单元

到现在,我已经能够实现将 ViewModel 绑定到一个视图以及视图的生命周期。下一步我需要一种一致的、模块化的方式将 MVVM 单元加载到容器当中。首先我定义了一个接口,在这个接口中规定了 ViewModel 和布局资源的关联关系。

public interface MvvmComponent {
    int getLayoutResId();
    ViewModel getViewModel();
}

接下来,我在 MvvmComponent 中定义了一个自定义的数据绑定关系。这个绑定帮助完成布局的渲染、ViewModel 的绑定,并加载到一个 ViewGroup 当中。

需要注意的是,我在渲染的过程中将 attachToParent 参数设置为 false,然后在绑定完成后通过显式地执行 addView(view) 方法来完成附加。我这样做的原因是为了 ViewModel 的 onViewAttachedToWindow 方法能够正常被调用,因为这个方法需要 View 在渲染之前就绑定 ViewModel。

现在我可以使用新的绑定关系了。在我的布局文件中,我通过新增 component 属性的方式来添加一个 ViewGroup 容器。

我通过使用 ObservableField 来在我的 ViewModel 中提供断开组件的方式。

组件类本身通过对父 ViewModel 的调用,提取出了资源 ID 和子 ViewModel 的定义,并且在父 ViewModel 传递过来的数据中,只接受那些子 ViewModel 初始化过程需要的参数。

到现在,子组件可以轻松在 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 单元进行包装,像下面这样:

注意布局文件中需要声明 fm 数据变量,并且将其设置为 ViewGroup 容器的属性。同时,需要关注的还有:配置变化时造成的关联影响、layoutResId 进程僵死,以及你的 MvvmFragment 的 vm 成员属性。适当的调整你的 Fragment 参数也很有必要。

现在你可以通过修改自定义组件的方式来使用你的 MvvmFragment,而不是直接渲染并绑定 ViewModel。

示例应用

如果你想参考一个完整的、使用 MVVM 来实现的(没有 Fragments)应用示例,可以参考我的例子:

https://github.com/wongcain/okuki/tree/master/okuki-sample-rx2-mvvm

编程愉快!


活动推荐:

2017 年 8 月 10-11 日,听云联合极客邦科技、InfoQ 将共同主办国内第二届应用性能管理大会 -APMCon 2017,会议聚焦行业内最新的技术和最接地气的实践案例,共同探讨 APM 相关性能优化、技术方案以及创新思路,为更多的行业从业者指点应用效能提升的迷津。为回馈读者,特奉上买一赠一优惠码: APMCon_0802,立即行动吧!

评论