官网已经对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。
参考: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> data =
new MutableLiveData>();
public JsonViewModel(Application application) {
super(application);
loadData();
}
public LiveData> getData() {
return data;
}
private void loadData() {
new AsyncTask>() {
@Override
protected List doInBackground(Void... voids) {
File jsonFile = new File(getApplication().getFilesDir(),
"downloaded.json");
List data = new ArrayList<>();
// Parse the JSON using the library of your choice
return data;
}
@Override
protected void onPostExecute(List 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> getData() {
return data;
}
}
public class JsonLiveData extends LiveData> {
private final Context context;
public JsonLiveData(Context context) {
this.context = context;
loadData();
}
private void loadData() {
new AsyncTask>() {
@Override
protected List doInBackground(Void… voids) {
File jsonFile = new File(getApplication().getFilesDir(),
"downloaded.json");
List data = new ArrayList<>();
// Parse the JSON using the library of your choice
return data;
}
@Override
protected void onPostExecute(List data) {
setValue(data);
}
}.execute();
}
}
现在我们的ViewModel变得相当简单。我们的LiveData现在完全封装了加载过程,只加载一次数据。
根据参考博客中的描述,其实官网文档中也有介绍,当数据可能来源于网络,或者本地内存,或者缓存等,这时候应该添加一个repository层,封装这些数据处理操作,然后presentation层(presenter或者ViewModels)就不用关心数据具体来源于哪里,repository层是数据对外的统一入口。
✅ :即:添加一个数据的repository层,作为外部获取数据的唯一入口。
请考虑以下情形:您正在观察由ViewModel公开的LiveData,该ViewModel包含要显示的一组列表数据。 视图如何区分加载的数据,网络错误和空列表?
您可以从ViewModel公开LiveData
✅ :即:使用一个包装类来暴露出你的数据的状态信息,或者使用另外一个LiveData。
Activity状态是Activity消失后重新创建屏幕所需的信息,表示Activity已被破坏或进程被终止。 旋转是最明显的情况,我们已经用ViewModels覆盖了它。 如果状态保存在ViewModel中,则状态是安全的。
但是,您可能需要在ViewModel也已消失的其他方案中恢复状态:例如,当操作系统资源不足并导致您的进程终止时。
要有效地保存和恢复UI状态,请使用persistence、onSaveInstanceState()和ViewModels的组合。
有关示例,请参阅:ViewModels:Persistence,onSaveInstanceState(),还原UI状态和加载程序
这里指的事件是只发生一次的。 ViewModels公开数据,但事件呢? 例如,导航事件、权限申请、或者显示Snackbar消息是应该只执行一次的操作。
事件的概念与LiveData存储和恢复数据的方式不完全吻合。 考虑具有以下字段的ViewModel:
LiveData snackbarMessage = new MutableLiveData<>();
Activity开始观察此操作,ViewModel完成操作,因此需要更新消息:
snackbarMessage.setValue("Item saved!");
这时Activity获取到值的改变,并显示Snackbar,看上去没有什么问题,但是如果旋转屏幕,新的activity被创建,并会接收到旧的值,会导致Snackbar再次显示该message。
要解决这个问题,不应该用第三方库或者对架构组件进行扩展,而应该把事件当成状态的一部分。
即:把事件设计成状态的一部分。更多信息参考:LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case).
考虑以下场景:
其中Presentation层使用观察者模式,而数据层使用接口回调。
如果用户退出app,View消失,ViewModel不再被观察,如果repository层是一个单例或者跟application生命周期一样,那么repository不会被销毁,直到进程被杀掉。而只有在系统资源紧张或者用户手动杀掉时,进程才会结束。而如果repository层如果有ViewModel的回调,则ViewModel会暂时性的发生内存泄漏。
如果ViewModel比较轻量级或者操作能很快结束,不会有很大影响。但事实并非总是如此。理想的情况是,只要没有任何View观察ViewModel,那么ViewModel就应该被销毁。
有很多方法可以做到这点:
✅ :要考虑到边界情况,泄漏和耗时操作对架构中的实例对象的影响。
为了避免泄漏ViewModel和回调地狱,repository层可以像这样被观察:
当ViewModel被清除的时候,或者View的生命周期结束的时候,ViewModel和repository间的订阅关系就被清理掉了。
如果您尝试这种方法,有一个问题:如果您无权访问LifecycleOwner,您如何从ViewModel订阅repository? 使用转换Transformations是解决此问题的一种非常方便的方法。 Transformations.switchMap允许您创建一个新的LiveData,以响应其他LiveData实例的更改。 它还允许在整个链中携带观察者生命周期信息:
LiveData 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的方式,是在ViewModel中使用MutableLiveData,并暴露出一个get方法。
如果您需要更多功能,扩展LiveData会在有活跃观察者时通知您。 例如,当您想要开始收听位置或传感器服务时,这非常有用:
public class MyLiveData extends LiveData {
public MyLiveData(Context context) {
// Initialize service
}
@Override
protected void onActive() {
// Start listening
}
@Override
protected void onInactive() {
// Stop listening
}
}
您还可以使用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有一定的了解,非常非常非常推荐大家来学习该例子。
后续有时间了再多写一些代码,并总结一些架构组件的学习心得。