MVVM下的Jetpack核心组件

  • Post author:
  • Post category:其他




前言


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

层接收到用户操作事件,通知到

Presenter



Presenter

进行逻辑处理,然后通知

Model

更新数据,

Model

把更新的数据给到

Presenter



Presenter

再通知到

View

更新界面。


MVP

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

  • 会引入大量的

    IView



    IPresenter

    接口,增加实现的复杂度。

  • View



    Presenter

    相互持有,形成耦合。

随着发展,

Jetpack MVVM

就应势而生,它是

MVVM

模式在

Android

开发中的一个具体实现,是

Google

官方提供并推荐的

MVVM

实现方式。它的分层:

  • Model层:用于获取和存储数据
  • View层:即

    Activity/Fragment
  • ViewModel层:负责业务逻辑


MVVM

的核心是

数据驱动

,把解耦做的更彻底(

ViewModel

不持有

view

)。


View

产生事件,使用

ViewModel

进行逻辑处理后,通知

Model

更新数据,

Model

把更新的数据给

ViewModel



ViewModel


自动通知View更新界面



Jetpack Lifecycle



起源

在没有

Lifecycle

之前,生命周期的管理都是靠手工维持。比如我们经常会在

Activity



onStart

初始化某些成员(比如

MVP



Presenter



MediaPlayer

)等,然后在

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

了,然后等一会儿执行过来的时候,

myLocationListener



start

,但后面不会再有

myLocationListener



stop

,这样这个组件的资源就不能正常释放了。如果它内部还持有

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

仅更新处于活跃生命周期状态的应用组件观察者。

如果观察者的生命周期处于

STARTED



RESUMED

状态,则

LiveData

会认为该观察者处于活跃状态,就会将更新通知给活跃的观察者,非活跃的观察者不会收到更改通知。


LiveData



观察者模式

的体现,先从

LiveData



observe

方法看起:

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);
}


observeForever



observe()

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


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

等相关的引用导致测试代码的难以编写(比如,

MVP



Presenter

层代码的测试就需要额外成本,比如依赖注入或者

Mock

,以保证单元测试的进行)。

也正是这样的规范要求,

ViewModel

不能持有UI层引用,自然也就避免了可能发生的内存泄漏。

2.更便于保存数据

当组件被销毁并重建后,原来组件相关的数据也会丢失。最简单的例子就是

屏幕的旋转

,如果数据类型比较简单,同时数据量也不大,可以通过

onSaveInstanceState()

存储数据,组件重建之后通过

onCreate()

,从中读取

Bundle

恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。


ViewModel

的扩展类则会在这种情况下自动保留其数据,如果

Activity

被重新创建了,它会收到被之前相同

ViewModel

实例。当所属

Activity

终止后,框架调用

ViewModel



onCleared()

方法释放对应资源。

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();
  }
}

这一步是基石:把

ViewModelStoreOwner



mViewModelStore

绑定到了

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

,又被传入过某个

Fragment



this

情况,实际上是生成了两个不同的

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.name



set

新值时,被绑定了该数据的控件即可获得通知和刷新。就是说,在使用

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

去更新…

因此,为了保证不会无限的死循环导致

App



ANR

异常的发生,我们需要在最初的代码块中加一个判断,保证只有

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 动画使用详解



版权声明:本文为m0_71506521原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。