美文网首页面试jetpack收藏
MVVM下的Jetpack核心组件

MVVM下的Jetpack核心组件

作者: 我爱田Hebe | 来源:发表于2022-10-29 14:11 被阅读0次

    前言

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

    LifecycleRegistry本身的生命周期改变后,LifecycleRegistry就会逐个通知每一个注册的LifecycleObserver ,并执行对应生命周期的方法。

    小结

    所以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 新内容时,被绑定该数据的控件即可被通知和刷新。

    参考

    笔者学习过程参考了以下博客,想深入细节的可以看看:

    Android官方架构组件ViewModel:从前世今生到追本溯源

    Android官方架构组件DataBinding-Ex: 双向绑定篇

    “终于懂了“系列:Jetpack AAC完整解析(一)Lifecycle 完全掌握!

    “终于懂了“系列:Jetpack AAC完整解析(二)LiveData 完全掌握!

    作者:树獭非懒
    链接:https://juejin.cn/post/7159404464313466894

    相关文章

      网友评论

        本文标题:MVVM下的Jetpack核心组件

        本文链接:https://www.haomeiwen.com/subject/cbjhtdtx.html