Android MVVM之禅

本文介绍了一种基于Android DataBinding库的MVVM模式实现方法。通过自定义ViewModel基类及组件接口,实现视图与ViewModel的解耦,支持双向数据绑定,并通过FragmentManager管理回退栈。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文: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)的完整功能应用,可以看我的这个例子

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值