MVVM下的Jetpack核心组件

前言

Jetpack 架构组件及 “标准化开发模式” 确立,意味着Android 开发已步入成熟阶段,只有对 MVVM 确有深入理解,才能自然而然写出标准化、规范化代码。

本次笔者会浅入浅出的介绍以下内容,由于它是一个我的学习总结记录,所以比较适合对MVVM不是很熟悉,但又想了解下全貌的读者:

  • Jetpack MVVM
  • Jetpack Lifecycle
  • Jetpack LiveData
  • Jetpack ViewModel
  • Jetpack DataBinding

Jetpack MVVM

在正文开始前,先回顾下MVP

MVP,Model-View-Presenter,职责分类如下:

  • Model,数据模型层,用于获取和存储数据。
  • View,视图层,即Activity/Fragment
  • Presenter,控制层,负责业务逻辑。

我们知道,MVP是对MVC的改进,解决了MVC的两个问题:

  • View责任明确,逻辑不再写在Activity中,放到了Presenter中;
  • Model不再持有View

MVP最常用的实现方式是这样的:

View层接收到用户操作事件,通知到PresenterPresenter进行逻辑处理,然后通知Model更新数据,Model 把更新的数据给到PresenterPresenter再通知到View 更新界面。

MVP本质是面向接口编程,它也存在一些痛点:

  • 会引入大量的IViewIPresenter接口,增加实现的复杂度。
  • ViewPresenter相互持有,形成耦合。

随着发展,Jetpack MVVM 就应势而生,它是MVVM 模式在Android 开发中的一个具体实现,是Google 官方提供并推荐的MVVM实现方式。它的分层:

  • Model层:用于获取和存储数据
  • View层:即Activity/Fragment
  • ViewModel层:负责业务逻辑

MVVM的核心是 数据驱动,把解耦做的更彻底(ViewModel不持有view )。

View 产生事件,使用ViewModel进行逻辑处理后,通知Model更新数据,Model把更新的数据给ViewModelViewModel自动通知View更新界面

Jetpack Lifecycle

起源

在没有Lifecycle之前,生命周期的管理都是靠手工维持。比如我们经常会在ActivityonStart初始化某些成员(比如MVPPresenterMediaPlayer)等,然后在onStop中释放这些成员的内部资源。

class MyActivity extends AppCompatActivity {
    private MyPresenter presenter;
​
    public void onStart(...) {
        presenter= new MyPresenter ();
        presenter.start();
    }
​
    public void onStop() {
        super.onStop();
        presenter.stop();
    }
}
​
class MyPresenter{
    public MyPresenter() {
    }
  
    void start(){
       // 耗时操作
      checkUserStatus{
        if (result) {
          myLocationListener.start();
        }
      }
    }
​
    void stop() {
      // 释放资源
      myLocationListener.stop();
    }
}

上述的代码本身是没有太大问题的。它的缺点在于实际生产环境下,会有很多的页面和组件需要响应生命周期的状态变化,就得在生命周期方法中放置大量的代码,这样的方式就会导致代码(如 onStart()onStop())变得臃肿,难以维护。

除此之外还有一个问题就是:

MyPresenter类中onStart里的checkUserStatus是个耗时操作,如果耗时过长,Activity 销毁的时候,还没有执行过来,就已经stop了,然后等一会儿执行过来的时候,myLocationListenerstart,但后面不会再有myLocationListenerstop,这样这个组件的资源就不能正常释放了。如果它内部还持有Activity的引用,还会造成内存泄露。

Lifecycle

于是,Lifecycle就出来了,它通过 “模板方法模式” 和 “观察者模式”,将生命周期管理的复杂操作,放到LifecycleOwner(如 Activity、Fragment 等 “视图控制器” 基类)中封装好。

对于开发者来说,在 “视图控制器” 的类中只需一句 getLifecycle().addObserver(new MyObserver()) ,当Lifecycle的生命周期发生变化时,MyObserver就可以在自己内部感知到。

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_lifecycle);
   // 使MyObserver感知生命周期
   getLifecycle().addObserver(new MyObserver());
}

看看它是怎么实现的:

# ComponentActivity
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);   
public Lifecycle getLifecycle() {
    return mLifecycleRegistry;
}
​
# LifecycleRegistry
public LifecycleRegistry(@NonNull LifecycleOwner provider) {
    this(provider, true);
}
​
private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap =
            new FastSafeIterableMap<>();
public void addObserver(@NonNull LifecycleObserver observer) {
  mObserverMap.putIfAbsent(observer, statefulObserver);
  ...
}
public void removeObserver(@NonNull LifecycleObserver observer) {
   mObserverMap.remove(observer);
}
​
void dispatchEvent(LifecycleOwner owner, Event event) {
  State newState = event.getTargetState();
  mState = min(mState, newState);
  mLifecycleObserver.onStateChanged(owner, event);
  mState = newState;
}

正因为Activity实现了LifecycleOwner,所以才能直接使用getLifecycle()

# ComponentActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
    // 关键代码:通过ReportFragment完成生命周期事件分发
    ReportFragment.injectIfNeededIn(this); 
    if (mContentLayoutId != 0) {
        setContentView(mContentLayoutId);
    }
}
# ReportFragment
static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
    if (activity instanceof LifecycleOwner) {
        Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
        if (lifecycle instanceof LifecycleRegistry) {
          // 处理生命周期事件,更新当前都状态并通知所有的注册的LifecycleObserver
          ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
        }
    }
}
​
# LifecycleRegistry
public void handleLifecycleEvent(@NonNull Lifecycle.Event event) {
    enforceMainThreadIfNeeded("handleLifecycleEvent");
    moveToState(event.getTargetState());
}

小结

所以Lifecycle 的存在,是为了解决 “生命周期管理” 一致性的问题。

Jetpack LiveData

起源

在没有LiveData的时候,我们在网络请求回调、跨页面通信等场景分发消息,大多是通过EventBus、接口callback的方式去完成。

比如经常使用的EventBus等消息总线的方式会有问题:

它缺乏一种约束,当我们去使用时,很容易因为随处使用,最后追溯数据来源的难度就会很大。

另外,EventBus在处理生命周期上也很麻烦,由于需要手动去控制,会容易出现生命周期管理不一致的问题。

LiveData

先看下官方的介绍:

LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意味着它遵循其他应用组件(如 Activity/Fragment)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

如果观察者的生命周期处于 STARTEDRESUMED状态,则 LiveData 会认为该观察者处于活跃状态,就会将更新通知给活跃的观察者,非活跃的观察者不会收到更改通知。

LiveData观察者模式 的体现,先从LiveDataobserve方法看起:

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
  // LifecycleOwner是DESTROYED状态,直接忽略
  if (owner.getLifecycle().getCurrentState() == DESTROYED) {
      return;
  }
  // 绑定生命周期的Observer
  LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
  ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
  // 让该Observer可以感知生命周期
  owner.getLifecycle().addObserver(wrapper);
}

observeForeverobserve()类似,只不过它会认为观察者一直是活跃状态,不会自动移除观察者。

LiveData很重要的一部分就是数据更新:·

LiveData原生的API提供了2种方式供开发者更新数据, 分别是setValue()postValue(),调用它们都会 触发观察者并更新UI

setValue()方法必须在 主线程 进行调用,而postValue()方法更适合在 子线程 中进行调用。postValue()最终也会调用setValue,只需要看下setValue方法就可以了:

protected void setValue(T value) {
  assertMainThread("setValue");
  mVersion++;
  mData = value;
  dispatchingValue(null);
}
​
void dispatchingValue(@Nullable ObserverWrapper initiator) {
  ...
  for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
          mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
      considerNotify(iterator.next().getValue());
  }
}
​
private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    ...
    observer.mObserver.onChanged((T) mData);
}

小问题:我们在使用LiveData有一个优势是不会发生内存泄漏,是怎么做到的呢?

这需要从上面提到的observe方法中寻找答案

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
  LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
  owner.getLifecycle().addObserver(wrapper);
}

传递的第一个是 LifecycleOwner,第二个参数Obserser实际就是我们的观察后的回调。这两个参数被封装成了LifecycleBoundObserver对象。

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
        @NonNull Lifecycle.Event event) {
    Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
    if (currentState == DESTROYED) {
      // Destoryed状态下,自动移除mObserver,避免内存泄漏
      removeObserver(mObserver);
      return;
    }
    activeStateChanged(shouldBeActive());
    ...
 }

这里就解释了为什么LiveData能够 自动解除订阅而避免内存泄漏 了,因为它内部能够感应到Activity或者Fragment的生命周期。

PS:这种设计非常巧妙,给我们一个启发点:

在我们初识 Lifecycle 组件对它不是理解很透彻的时候,总是下意识认为它能够对大的对象进行有效生命周期的管理(比如Presenter),实际上,这种生命周期的管理我们完全可以应用到各个功能的基础组件中,比如大到吃内存的MediaPlayer、绘制设计复杂的自定义View,小到随处可见的LiveData,都可以通过实现LifecycleObserver接口达到感应生命周期的能力,并内部释放重资源的目的。

小结

LiveData在感知生命周期的能力下,让应用数据发生变化时通过观察者去更新界面,并且不会出现内存泄露的情况。

Jetpack ViewModel

起源

在没有ViewModel,我们用MVP开发的时候,我们为了实现数据在UI上的展示,往往会写很多UI层和Model层相互调用的代码,这些代码写起来繁琐且一定程度的模版化。另外,某些场景(例如屏幕旋转)销毁和重新创建界面,那么存储在其中的界面相关数据都会丢失,一般都需要手动存储和恢复。

为了解决这两个痛点,ViewModel就出场,用ViewModel用于代替MVP中的Presenter

ViewModel 的概念就是这样被提出来的,它就像一个 状态存储器 ,存储着UI中各种各样的状态。

ViewModel的好处

1.更规范化的抽象接口

Google官方建议ViewModel尽量保证 纯的业务代码,不要持有任何View层(Activity或者Fragment)或Lifecycle的引用,这样保证了ViewModel内部代码的可测试性,避免因为Context等相关的引用导致测试代码的难以编写(比如,MVPPresenter层代码的测试就需要额外成本,比如依赖注入或者Mock,以保证单元测试的进行)。

也正是这样的规范要求,ViewModel不能持有UI层引用,自然也就避免了可能发生的内存泄漏。

2.更便于保存数据

当组件被销毁并重建后,原来组件相关的数据也会丢失。最简单的例子就是屏幕的旋转,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()存储数据,组件重建之后通过onCreate(),从中读取Bundle恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。

ViewModel的扩展类则会在这种情况下自动保留其数据,如果Activity被重新创建了,它会收到被之前相同ViewModel实例。当所属Activity终止后,框架调用ViewModelonCleared()方法释放对应资源。

3.更方便UI组件之间的通信

一个Activity中的多个Fragment相互通讯是很常见的,如果ViewModel的实例化作用域为Activity的生命周期,则两个Fragment可以持有同一个ViewModel的实例,这也就意味着数据状态的共享

接下来,分析它的源码是怎么做到这些的:

我们可以通过ViewModelProvider注入ViewModelStoreOwner,从而为引用ViewModel 的页面(比如Activity)创建一个临时的、单独的 ViewModelProvider 实例。并通过这个ViewModelProvider可以获取到ViewModel

# this: ViewModelStoreOwner(interface)
ViewModelProvider(this).get(viewModelClass)

分创建、获取两步来看,先看创建ViewModelProvider做了什么:

# ViewModelProvider 
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
  // owner.getViewModelStore(),比如:owner是ComponentActivity
  this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
          ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
          : NewInstanceFactory.getInstance());
}
​
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}
​
public interface ViewModelStoreOwner {
    ViewModelStore getViewModelStore();
}
​
# ComponentActivity implements ViewModelStoreOwner
public ViewModelStore getViewModelStore() {
    // 为空就创建
    ensureViewModelStore();
    return mViewModelStore;
}
​
void ensureViewModelStore() {
 if (mViewModelStore == null) {
     mViewModelStore = new ViewModelStore();
  }
}

这一步是基石:把ViewModelStoreOwnermViewModelStore绑定到了ViewModelProvider中。简单点说就是同一个ViewModelStoreOwner拿到的是同一个mViewModelStore

如何获取对应的ViewModel

# ViewModelProvider
private final ViewModelStore mViewModelStore;
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
​
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);
    // 直接返回已存在的viewModel
    if (modelClass.isInstance(viewModel)) {
        return (T) viewModel;
    }
    if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
    } else {
        viewModel = mFactory.create(modelClass);
    }
    // 存储viewModel
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}
​
# ViewModelStore
public class ViewModelStore {
  private final HashMap<String, ViewModel> mMap = new HashMap<>();
  final void put(String key, ViewModel viewModel) {
    ViewModel oldViewModel = mMap.put(key, viewModel);
    if (oldViewModel != null) {
        oldViewModel.onCleared();
    }
  }
}

即通过这样的设计,来实现类似于单例的效果:每个页面都可以通过ViewModelProvider 注入Activity 这个ViewModelStoreOwner,来共享跨页面的状态;

同时,又不至于完全沦为简单粗暴的单例:每个页面都可以通过 ViewModelProvider 注入this,来管理私有的状态。

比如下面这个具体的例子:

当应用中某个ViewModel 存在既被ViewModelProvider 传入过 Activity,又被传入过某个 Fragmentthis 情况,实际上是生成了两个不同的 ViewModel实例,属于不同的 ViewModelStoreOwner。当引用被this 持有的ViewModel 的 页面destory 时,被Activity 持有的ViewModel 的页面并不受影响。

小结

ViewModel是为了解决 “状态管理” 和 “页面通信” 问题。有了ViewModel,我们在开发的时候,可以大幅减少UI层和Model层相互调用的代码,将更多的重心投入到业务代码的编写

Jetpack DataBinding

起源

DataBinding 出现以前,想要更新视图就要引用该视图,然后调用setxxx方法:

TextView textView = findViewById(R.id.sample_text);
if (textView != null && viewModel != null) {
    textView.setText(viewModel.getUserName());
}

这种方式有几个不好的地方:

  • 容易出现空指针(存在差异的横、竖两种布局,如横屏存在此 textView 控件,而竖屏没有),引用该视图一般要先判空
  • 需要写模板代码 findViewById
  • 业务复杂的话,一个控件会在多处调用

DataBinding

DataBinding是个受争议比较大的组件。很多人对 DataBinding 的认知就是在xml中写逻辑:

  • xml中写表达式逻辑,出错了debug不了
  • 逻辑写在xml里面的话 xml 就承担了 Presenter/ViewModel 的职责,职责变得混乱了

当然如果站在把逻辑写在xml中的角度看,确实会造成xml中是不能调试的、职责混乱。

但这不是DataBinding的本质。DataBinding,含义是 数据绑定,即 布局中的控件可观察的数据 进行绑定。

<TextView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@{user.name}"/>

user.nameset 新值时,被绑定了该数据的控件即可获得通知和刷新。就是说,在使用DataBinding 后,唯一的改变是,你无需手动调用视图来 set 新状态,你只需 set 数据本身。

所以,DataBinding 并非是将 UI 逻辑搬到 XML 中写导致而难以调试 ,它只负责绑定数据,将 UI 控件与其需要的终态数据进行绑定。

双向绑定

上面介绍的例子,数据的流向是单向的,只需要监听到数据的变更然后展示到UI上,是个单向绑定。

但有些场景,UI的变化需要影响到ViewModel层的数据状态,比如UI层的EditText,对它进行编辑并需要更新LiveData的数据。这时就需要 双向绑定

Android原生控件中,绝大多数的双向绑定使用场景,DataBinding都已经帮我们实现好了,比如EditText

<EditText
  android:id="@+id/etPassword"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="@={fragment.viewModel.password }" />

相比单向绑定,只需要多一个=符号,就能保证View层和ViewModel层的 状态同步

双向绑定使用起来很简单,但定义却稍微比单向绑定麻烦一些,即使原生的控件DataBinding已经帮助我们实现好了,对于三方的控件或者自定义控件,还需要我们自己实现

举个栗子

这里举个下拉刷新SwipeRefreshLayout的例子,来看看双向绑定是怎么实现的:

我们的需求时:当我们为LiveData手动设置值时,SwipeRefreshLayout的UI也会发生对应的变更;反之,当用户手动下拉执行刷新操作时,LiveData的值也会对应的变成为true(代表刷新中的状态):

// refreshing实际是一个LiveData:
val refreshing: MutableLiveData<Boolean> = MutableLiveData()
​
object SwipeRefreshLayoutBinding {
  // 1.@BindingAdapter 在数据发生更改时要执行的操作:
  // 每当LiveData的状态发生了变更,SwipeRefreshLayout的刷新状态也会发生对应的更新。
  @JvmStatic
  @BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
  fun setSwipeRefreshLayoutRefreshing(
          swipeRefreshLayout: SwipeRefreshLayout,
          newValue: Boolean
  ) {
      // 判断值是否变化了,避免无限循环
      if (swipeRefreshLayout.isRefreshing != newValue)
          swipeRefreshLayout.isRefreshing = newValue
  }
  
  // 2.@InverseBindingAdapter: view视图发生更改时要调用的内容
  // 但是它不知道特性何时或如何更改,所以还需要设置视图监听器
  @JvmStatic
  @InverseBindingAdapter(
          attribute = "app:bind_swipeRefreshLayout_refreshing",  
          event = "app:bind_swipeRefreshLayout_refreshingAttrChanged"    // tag
  )
  fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
          swipeRefreshLayout.isRefreshing
 }
  
  // 3. @BindingAdapter: 事件监听器与相应的 View 实例相关联
  // 观察view的状态变化,每当swipeRefreshLayout刷新状态被用户的操作改变
  @JvmStatic
  @BindingAdapter(
          "app:bind_swipeRefreshLayout_refreshingAttrChanged",     // tag
          requireAll = false
  )
  fun setOnRefreshListener(
          swipeRefreshLayout: SwipeRefreshLayout,
          bindingListener: InverseBindingListener?
  ) {
      if (bindingListener != null)
          // 监听下拉刷新
          swipeRefreshLayout.setOnRefreshListener {
              bindingListener.onChange()
          }
  }

双向绑定将SwipeRefreshLayout的刷新状态抽象成为了一个LiveData<Boolean>,我们只需要在xml中定义好,之后就可以在ViewModel中围绕这个状态进行代码的编写。

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:bind_swipeRefreshLayout_refreshing="@={fragment.viewModel.refreshing}">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

注意事项:避免死循环

双向绑定有一个致命的问题,那就是无限循环会导致的ANR异常。

View层UI状态被改变,ViewModel对应发生更新,同时,这个更新又回通知View层去刷新UI,这个刷新UI的操作又会通知ViewModel去更新…

因此,为了保证不会无限的死循环导致AppANR异常的发生,我们需要在最初的代码块中加一个判断,保证只有View状态发生了变更,才会去更新UI。

小结

DataBinding通过让 “控件” 与 “可观察数据” 发生绑定,它的本质是将终态数据 绑定到View ,而不是在xml写逻辑,当该数据被 set 新内容时,被绑定该数据的控件即可被通知和刷新。


为了帮助大家更好的熟知Jetpack 这一套体系的知识点,这里记录比较全比较细致的《Jetpack 入门到精通》(内含Compose) 学习笔记!!! 对Jetpose Compose这块感兴趣的小伙伴可以参考学习下……

Jetpack 全家桶(Compose)

Jetpack 部分

  1. Jetpack之Lifecycle
  2. Jetpack之ViewModel
  3. Jetpack之DataBinding
  4. Jetpack之Navigation
  5. Jetpack之LiveData

Compose 部分
1.Jetpack Compose入门详解
2.Compose学习笔记
3.Compose 动画使用详解

猜你喜欢

转载自blog.csdn.net/m0_71506521/article/details/132237646