这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
Views and ViewModels
Distributing responsibilities
理想情况下,ViewModels不应该知道关于Android的任何事情。这可以提高可测试性、泄漏安全性和模块化。一般的经验法则是,确保在你的ViewModels中没有android.的导入(android.arch.等例外)。这同样适用于presenters。
- ❌ 不要让ViewModels(和Presenters)知道Android框架类的情况
条件语句、循环和一般决策应该在ViewModels或应用程序的其他层中完成,而不是在Activities或Fragments中。视图通常没有单元测试(除非你使用Robolectric),所以代码行数越少越好。视图应该只知道如何显示数据并将用户事件发送到ViewModel(或Presenter)。这就是所谓的被动视图模式。
- ✅将Activity和Fragment中的逻辑保持在最低限度
View references in ViewModels
视图模型与Activity或Fragment有不同的作用域。当一个ViewModel活着并运行时,一个Activity可以处于其生命周期的任何状态。在ViewModel不知道的情况下,Activity和Fragment可以被销毁并再次创建。
将视图(Activity或Fragment)的引用传递给ViewModel是一个严重的风险。让我们假设ViewModel从网络上请求数据,并且数据在一段时间后回来。这时,View的引用可能会被破坏,也可能是一个不再可见的旧Activity,产生内存泄漏,并可能导致崩溃。
- ❌ 避免在ViewModels中对View进行引用。
在ViewModels和View之间进行通信的推荐方式是观察者模式,使用LiveData或来自其他库的观察变量方式。
Observer Pattern
在Android中设计表现层的一个非常方便的方法是让View(Activity或Fragment)观察(订阅)ViewModel的变化。由于ViewModel并不了解Android,所以它不知道Android是如何喜欢频繁地杀死View的。这有一些好处。
- ViewModel在配置变化时被持久化,所以当重新请求发生时,不需要重新查询外部数据源(如数据库或网络)。
- 当长期运行的操作结束时,ViewModel中的观察变量会被更新。数据是否被观察并不重要。当试图更新不存在的视图时,不会发生空指针异常。
- ViewModels不引用视图,所以内存泄漏的风险较小。
private void subscribeToModel() {
// Observe product data
viewModel.getObservableProduct().observe(this, new Observer<Product>() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}
- ✅不要把数据推送给UI,而是让UI观察到它的变化。
Fat ViewModels
只要能让你分离关注点,就是一个好主意。如果你的ViewModel容纳了太多的代码或者有太多的责任,可以考虑。
- 将一些逻辑转移到与ViewModel相同范围的presenter中。它将与你的应用程序的其他部分通信,并更新ViewModel中的LiveData持有者。
- 添加一个Domain layer并采用Clean Architecture。这将导致一个非常可测试和可维护的架构。它也有利于快速离开主线程。在Architecture Blueprints中有一个Clean Architecture的例子。
例子在这里:https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
- ✅ 分散责任,如果需要的话,添加领域层。
Using a data repository
正如在《应用程序架构指南》中看到的那样,大多数应用程序都有多个数据源,例如。
- 远程:网络或云
- 本地:数据库或文件
- 内存中的缓存
在你的应用程序中设置一个数据层是个好主意,完全不知道你的表现层。让缓存和数据库与网络保持同步的算法并非易事。建议有一个单独的存储库类作为处理这种复杂性的单一入口。
如果你有多个非常不同的数据模型,可以考虑添加多个存储库。
- ✅ 添加一个数据存储库作为你的数据的单点入口
Dealing with data state
考虑这个场景:你正在观察一个由ViewModel暴露的LiveData,它包含一个要显示的项目列表。视图如何区分正在加载的数据、网络错误和一个空列表?
你可以从ViewModel中暴露出一个LiveData。例如,MyDataState可以包含关于数据是否正在加载、是否已经成功加载或失败的信息。
你可以把数据包装在一个有状态和其他元数据(如错误信息)的类中。参见我们样本中的资源类:https://developer.android.com/jetpack/guide#addendum。
- ✅使用包装器或另一个LiveData暴露你的数据的状态信息。
Saving activity state
Activity状态是你在一个Activity消失时重新创建屏幕所需要的信息,这意味着该Activity被破坏或进程被杀死。旋转是最常见的情况,我们已经用ViewModels覆盖了这种情况。所以,状态被保存在ViewModel中是安全的。
然而,你可能需要在ViewModels也消失的其他情况下恢复状态:例如,当操作系统资源不足并杀死了你的进程时。
为了有效地保存和恢复UI状态,可以使用持久性、onSaveInstanceState()和ViewModels的组合。
对于一个例子,请看。ViewModels: 持久性、onSaveInstanceState()、恢复UI状态和加载器
Events
事件是发生一次的事情。ViewModels暴露了数据,但事件呢?例如,导航事件或显示Snackbar信息是只应执行一次的动作。
事件的概念与LiveData存储和恢复数据的方式并不完全相符。考虑一个有以下字段的ViewModel。
LiveData<String> snackbarMessage = new MutableLiveData<>();
一个Activity开始观察这个,ViewModel完成了一个操作,所以它需要更新消息。
snackbarMessage.setValue("Item saved!");
该Activity接收该值并显示Snackbar。这显然是有效的。
然而,如果用户旋转手机,新的Activity被创建并开始观察。当LiveData观察开始时,该Activity立即收到旧的值,这导致消息再次显示出来。
与其试图用库或架构组件的扩展来解决这个问题,不如将其作为一个设计问题来面对。我们建议你把你的事件作为你的状态的一部分。
- ✅将事件设计成你的状态的一部分。更多细节请阅读LiveData与SnackBar、Navigation和其他事件(SingleLiveEvent案例)。
Leaking ViewModels
反应式范式在Android中运行良好,因为它允许在UI和你的应用程序的其他层之间建立一个方便的连接。LiveData是这个结构的关键组件,所以通常你的Activity和Fragment会观察LiveData实例。
ViewModels如何与其他组件通信由你决定,但要注意泄漏和边缘情况。考虑一下这个图,视图层使用观察者模式,数据层使用回调。
如果用户退出了应用程序,视图就会消失,所以ViewModel就不会再被观察。如果repository是一个单例或其他范围的应用程序,repository将不会被销毁,直到进程被杀死。这只会在系统需要资源或用户手动杀死应用程序时发生。如果repository持有对ViewModel中回调的引用,ViewModel将被暂时泄露。
如果ViewModel是轻量级的,或者操作被保证快速完成,这种泄漏就不是什么大问题。然而,情况并不总是这样的。理想情况下,只要没有任何视图在观察它们,ViewModel就应该是自由的。
你有很多选择来实现这一点。
- 通过ViewModel.onCleared()你可以告诉repository放弃对ViewModel的回调。
- 在repository中,你可以使用WeakReference,也可以使用事件总线(两者都容易被滥用,甚至被认为是有害的)。
- 使用LiveData在存储库和ViewModel之间进行通信,其方式类似于在View和ViewModel之间使用LiveData。
这点用Flow也可以解决。
- ✅考虑边缘情况、泄漏以及长期运行的操作会如何影响你架构中的实例。
- ❌ 不要在ViewModel中放置对保存清洁状态或与数据有关的逻辑。你从ViewModel进行的任何调用都可能是最后一次。
LiveData in repositories
为了避免泄露ViewModels和回调地狱,可以像这样观察存储库。
当ViewModel被清除或视图的生命周期结束时,订阅被清除。
如果你尝试这种方法,会有一个问题:如果你不能访问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对象,一个Transformation可能就是解决方案。
Extending LiveData
LiveData最常见的用例是在ViewModels中使用MutableLiveData,并将它们作为LiveData公开,使它们从观察者那里不可改变。
如果你需要更多的功能,扩展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
}
}
When not to extend LiveData
你也可以使用onActive()来启动一些加载数据的服务,但除非你有很好的理由,否则你不需要等待LiveData的观察。一些常见的模式。
给ViewModel添加一个start()方法,并尽快调用它:https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.java#L64
- ❌ 你通常不会扩展LiveData。让你的Activity或Fragment告诉ViewModel何时开始加载数据。
原文链接:https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54
向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问