安卓 MVVM 之禅

我之前在多个 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:onViewAttachedToWindowandroid:onViewDetachedFromWindow 映射到我的 ViewModel 类的对应方法当中。我实现了这些方法,并将其关联到 ViewModel 接口的 onAttachonDetach 方法上。通过这种方式,我可以在相应的扩展类当中隐藏所必需的 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 即可。就像下面这样:

\\
\u0026lt;layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\u0026gt;\\u0026gt;  \u0026lt;data\u0026gt;\    \u0026lt;variable name=\"vm\" type=\"MyViewModel\"/\u0026gt;\  \u0026lt;/data\u0026gt;\\u0026lt;FrameLayout\  android:layout_width=\"match_parent\"\  android:layout_height=\"match_parent\"\  android:onViewAttachedToWindow=\"@{vm::onViewAttachedToWindow}\"\  android:onViewDetachedFromWindow=\"@{vm::onViewDetachedFromWindow}\"\\u0026gt;\\u0026lt;/FrameLayout\u0026gt;\\u0026lt;/layout\u0026gt;
\\

模块化单元

\\

到现在,我已经能够实现将 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 容器。

\\
\u0026lt;layout xmlns:android=\"http://schemas.android.com/apk/res/android\"         xmlns:app=\"http://schemas.android.com/apk/res-auto\"\u0026gt;   \u0026lt;data\u0026gt;     \u0026lt;variable       name=\"vm\"       type=\"MyViewModel\"/\u0026gt;   \u0026lt;/data\u0026gt;   \u0026lt;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}\"   /\u0026gt;\\u0026lt;/layout\u0026gt;
\\

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

\\
public class MyViewModel extends BaseViewModel {\  public final ObservableField myComponent \     = new ObservableField\u0026lt;\u0026gt;();\  @Override\  public void onAttach() {\    myComponent.set(new HelloWorldComponent(\"World\"));\  }\}
\\

组件类本身通过对父 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\
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值