我之前在多个 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 即可。就像下面这样:
\\\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\