ViewModels and LiveData: Patterns + AntiPatterns

官网已经对ViewModel做了一些说明,比如不能在ViewModel中引入Activity的Context,但是还有很多注意事项,或者说idioms(惯用语法)来更好的使用ViewModel。

本文参考自Google官网推荐的一篇博文:ViewModels and LiveData: Patterns + AntiPatterns

先来一张官网给出的使用架构组件的整体描述图:

理想情况下,ViewModels不应该引入Android任何东西。 这提高了可测试性,泄漏安全性和模块化。 一般的经验法则是确保ViewModel中没有android .*导入(android.arch.*除外)。 这同样适用于presenter层。

 注意点1: 不要在ViewModel(或者presenter层)引入android的framework类

条件语句,循环和一般决策应在ViewModel或应用程序的其他层中完成,而不是在Activity或Fragment中完成。 View通常不经过单元测试(除非你使用Robolectric)所以代码行越少越好。 VIew视图应该只知道如何显示数据,并将用户事件发送到ViewModel(或Presenter)。 这称为Passive View被动视图模式。

即:在Activity和fragment中应该保留最小化的逻辑代码。

注意点2: 在《ViewModel官网学习总结》一文中,已经说明了,由于ViewModel的生命周期大于Activity的,所以不能在ViewModel层中引入Activity或者fragment的context,以避免内存泄漏或者crash。

✅  :ViewModels和View之间的交互,建议使用的方式是观察者模式,比如使用LiveData或者其他库中的观察者。即不要直接将数据传递到UI层,而是让UI层观察数据的变化,即数据驱动UI。

   

 

 避免臃肿的ViewModels

参考:Lifecycle Aware Data Loading with Architecture Components

✅  :即避免把数据处理逻辑直接放到ViewModel中,而是放到LiveData(或者其他的observable)中,因为ViewModels设计的目的就是用来创建和组织LiveData的。可以将一些逻辑转移到presenter层中。

✅  :将逻辑封装在LiveData中,也有利于在很多ViewModels中复用相同的LiveData,,通过MediatorLiveData将多个LiveData源组合在一起,或者在一个Service中使用它们

参见以下代码:

public class JsonViewModel extends AndroidViewModel {
  // You probably have something more complicated
  // than just a String. Roll with me
  private final MutableLiveData<List<String>> data =
      new MutableLiveData<List<String>>();
  public JsonViewModel(Application application) {
    super(application);
    loadData();
  }
  public LiveData<List<String>> getData() {
    return data;
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void... voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        this.data.setValue(data);
      }
    }.execute();
  }
}

以上代码,将很多数据获取或者处理逻辑放到了ViewModel层中,这是不合理的,应该改成如下的方式:

public class JsonViewModel extends AndroidViewModel {
  private final JsonLiveData data;
  public JsonViewModel(Application application) {
    super(application);
    data = new JsonLiveData(application);
  }
  public LiveData<List<String>> getData() {
    return data;
  }
}
public class JsonLiveData extends LiveData<List<String>> {
  private final Context context;
  public JsonLiveData(Context context) {
    this.context = context;
    loadData();
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void… voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        setValue(data);
      }
    }.execute();
  }
}

现在我们的ViewModel变得相当简单。我们的LiveData现在完全封装了加载过程,只加载一次数据。

 

使用数据repository层

根据参考博客中的描述,其实官网文档中也有介绍,当数据可能来源于网络,或者本地内存,或者缓存等,这时候应该添加一个repository层,封装这些数据处理操作,然后presentation层(presenter或者ViewModels)就不用关心数据具体来源于哪里,repository层是数据对外的统一入口。

✅  :即:添加一个数据的repository层,作为外部获取数据的唯一入口。

 

处理数据的状态

请考虑以下情形:您正在观察由ViewModel公开的LiveData,该ViewModel包含要显示的一组列表数据。 视图如何区分加载的数据,网络错误和空列表?
您可以从ViewModel公开LiveData <MyDataState>。 例如,MyDataState可以包含有关数据当前是否正在加载,是否已成功加载或失败的信息,或者其他一些原始数据信息。

✅  :即:使用一个包装类来暴露出你的数据的状态信息,或者使用另外一个LiveData。

 

保存Activity的状态

Activity状态是Activity消失后重新创建屏幕所需的信息,表示Activity已被破坏或进程被终止。 旋转是最明显的情况,我们已经用ViewModels覆盖了它。 如果状态保存在ViewModel中,则状态是安全的。
但是,您可能需要在ViewModel也已消失的其他方案中恢复状态:例如,当操作系统资源不足并导致您的进程终止时。
要有效地保存和恢复UI状态,请使用persistence、onSaveInstanceState()和ViewModels的组合。
有关示例,请参阅:ViewModels:Persistence,onSaveInstanceState(),还原UI状态和加载程序

 

Event 事件

这里指的事件是只发生一次的。 ViewModels公开数据,但事件呢? 例如,导航事件、权限申请、或者显示Snackbar消息是应该只执行一次的操作。
事件的概念与LiveData存储和恢复数据的方式不完全吻合。 考虑具有以下字段的ViewModel:

LiveData<String> snackbarMessage = new MutableLiveData<>();

Activity开始观察此操作,ViewModel完成操作,因此需要更新消息:

snackbarMessage.setValue("Item saved!");

这时Activity获取到值的改变,并显示Snackbar,看上去没有什么问题,但是如果旋转屏幕,新的activity被创建,并会接收到旧的值,会导致Snackbar再次显示该message。

要解决这个问题,不应该用第三方库或者对架构组件进行扩展,而应该把事件当成状态的一部分。

即:把事件设计成状态的一部分。更多信息参考:LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case).

 

注意ViewModel的泄漏

考虑以下场景:

其中Presentation层使用观察者模式,而数据层使用接口回调。

如果用户退出app,View消失,ViewModel不再被观察,如果repository层是一个单例或者跟application生命周期一样,那么repository不会被销毁,直到进程被杀掉。而只有在系统资源紧张或者用户手动杀掉时,进程才会结束。而如果repository层如果有ViewModel的回调,则ViewModel会暂时性的发生内存泄漏。

 

如果ViewModel比较轻量级或者操作能很快结束,不会有很大影响。但事实并非总是如此。理想的情况是,只要没有任何View观察ViewModel,那么ViewModel就应该被销毁。

有很多方法可以做到这点:

  • 通过ViewModel的onCleared()方法,可以告诉repository层,丢掉往ViewModel的回调。
  • 在repository中,可以使用弱引用或者可以使用EventBus(两者都容易被误用甚至被认为是有害的)
  • 像使用LiveData在View和ViewModel之间交互一样,使用LiveData在ViewModel和repository层之间交互

✅  :要考虑到边界情况,泄漏和耗时操作对架构中的实例对象的影响。

 

在repository层中使用LiveData

为了避免泄漏ViewModel和回调地狱,repository层可以像这样被观察:

当ViewModel被清除的时候,或者View的生命周期结束的时候,ViewModel和repository间的订阅关系就被清理掉了。

如果您尝试这种方法,有一个问题:如果您无权访问LifecycleOwner,您如何从ViewModel订阅repository? 使用转换Transformations是解决此问题的一种非常方便的方法。 Transformations.switchMap允许您创建一个新的LiveData,以响应其他LiveData实例的更改。 它还允许在整个链中携带观察者生命周期信息:

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
        if (repoId.isEmpty()) {
            return AbsentLiveData.create();
        }
        return repository.loadRepo(repoId);
    }
);

在此示例中,当触发器获得更新时,将应用该函数并在下游调度结果。 Activity将观察repo,并且相同的LifecycleOwner将用于repository.loadRepo(id)调用。

✅  即:当你需要在ViewModel中获取一个Lifecycle生命周期的对象时,这时候可以使用Transformations

 

扩展LiveData

通常使用LiveData的方式,是在ViewModel中使用MutableLiveData,并暴露出一个get方法。

如果您需要更多功能,扩展LiveData会在有活跃观察者时通知您。 例如,当您想要开始收听位置或传感器服务时,这非常有用:

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // Initialize service
    }

    @Override
    protected void onActive() {
        // Start listening
    }

    @Override
    protected void onInactive() {
        // Stop listening
    }
}

什么时候不应该扩展LiveData

您还可以使用onActive()来启动一些加载数据的服务,但除非您有充分的理由,否则您无需等待LiveData直到能被观察到。 一些常见的模式:

❌  您通常不会扩展LiveData。 让您的Activity或Fragment告诉ViewModel什么时候开始加载数据

 

最后:

关于使用ViewModel,官网推荐了一篇博客:ViewModels : A Simple Example

一个Android codelab中关于使用ViewModel的例子:https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0

YouTube上一个官方对ViewModel的使用介绍:https://www.youtube.com/watch?v=c9-057jC1ZA

Android Jetpack: ViewModel | 中文教学视频 :https://mp.weixin.qq.com/s/uGWH1os8Kq3Pp6_x5hXI8Q

对于使用 Architecture Components,官网给出了一些简单的例子:

https://github.com/googlesamples/android-architecture-components/blob/master/README.md

以及一个官网推荐的使用Jetpack的例子:  Sunflower

 

一个使用了生命周期组件的MVVM版本的GitHub客户端 Github Browser Sample with Android Architecture Components

This is a sample app that uses Android Architecture Components with Dagger 2.

NOTE It is a relatively more complex and complete example so if you are not familiar with Architecture Components, you are highly recommended to check other examples in this repository first.

官网介绍这是一个相对更加复杂和完备的例子,需要对依赖注入框架Dagger2有一定的了解,非常非常非常推荐大家来学习该例子。

 

后续有时间了再多写一些代码,并总结一些架构组件的学习心得。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值