1、Views和ViewModels
理想情况下,ViewModel不应该了解有关Android的任何信息。这提高了可测试性、泄露安全性和模块化性。一般的经验法则是确保android.*
包,您的ViewModel没有导入(除了像android.arch.*
之类的例外)。这同样适用于Presenters。
❌不要让ViewModel(和Presenter)知道Android框架类。
条件语句、循环和一般决策应在ViewModel
或应用程序的其他层中完成,而不是在Activity
或Fragment
中完成。View通常未经单元测试(除非您使用Robolectric
),因此代码越少越好。View层应该值知道如何显示数据并将用户事件发送到ViewModel(或Presenter)。这称为被动视图模式。
✅将Activity和Fragment中的逻辑保持最低限度。
2、查看ViewModel中的引用
ViewModel具有与与Activity或Fragment不同的范围。当ViewModel处于活动状态并正在运行,activity可以处于任何生命周期状态。activity和fragment可以在ViewModel不知情的情况下被销毁并再次创建。
将View(activity或fragment)的引用传递给ViewModel是一个严重的风险。我们假设ViewModel从网络请求数据,并且数据在一段时间后返回。此时,View引用可能会被销毁,或者可能是不再可见的旧activity,从而产生内存泄露,甚至可能导致崩溃。
❌避免在ViewModel中引用视图
在ViewModel和View之间进行通信的推荐方法是观察者模式,使用LiveData或其他库中的可观察对象。
3、观察者模式
在Android中设计表示层的一个非常方便的方法是让View(activity或fragment)观察(订阅)ViewModel中的更改。由于ViewModel不了解Android,因此它不知道Android如何频繁杀死View。这有一些优点:
- ViewModel在配置更改时保持不变,因此在发生轮转时无需重新查询外部数据源(例如数据库或网络)
- 当长时间运行的操作完成时,ViewModel中的可观察量将被更新。数据是否被观察并不重要。尝试更新不存在的View时不会发生空指针异常。
- ViewModel不引用View,因此内存泄露的风险较小。
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观察数据的变化。
4、臃肿的ViewModel
任何能让你分离关注点的方法都是好主意。如果您的ViewModel包含太多代码或承担太多责任,请考虑:
- 将一些逻辑移出到Presenter,其范围与ViewModel相同。它将与应用程序的其他部分通信并更新ViewModel中的LiveData持有者。
- 添加Domain层并采用
Clean Architecture
。这导致了一个非常可测试和可维护的架构。它还有助于快速脱离主线程,architecture-samples中有一个干净的架构示例。
✅职责分配,如果需要,添加Domain层
5、使用data repository
大多数应用程序都有多个数据源,例如:
- 远程:网络或云端
- 数据库或文件
- 内存缓存
在您的应用程序中拥有一个Data层是一个好主意,完全不知道您的View层。保持缓存和数据库与网络同步的算法并非易事。建议使用单独的存储库类作为处理这种复杂性的单一入口。
如果您有多个且截然不同的数据模式,请考虑添加多个Repositories.
✅添加data repository作为数据的单一入口
6、处理数据状态
考虑一下场景:您正在观察ViewModel公开的LiveData,其中包含要显示的项目列表。View如何区分正在加载的数据、网络错误和空列表?
- 您可以从ViewModel中公开拿到一个
LivaData<MyDataState>
。例如,NyDataState
可以包含有关数据当前是否正在加载、已加载成功或失败的信息。
您可以将数据包装在具有状态和其他元数据(例如错误消息)的类中。
✅使用包装器或其他LiveData公开有关数据状态的信息。
7、保存activity状态
Activity状态时当activity消失时重新创建屏幕所需的信息,这意味着活动被破坏或进程被终止。旋转是最明显的情况,我们已经用ViewModel涵盖了这一点。如果状态保存在ViewModel中,那么它就是安全的。
但是,您可能需要在ViewModel也消失的其他情况下恢复状态:例如,当操作系统资源不足并终止进程时。
要有效地保存和恢复UI状态,请结合使用持久性onSavedInstanceState()
和ViewModel。
8、Event
事件是发生一次的事情。ViewModel公开数据,但是事件呢?例如,导航事件或显示Snackbar消息是只执行一次的操作。
事件的概念并不完全符合LiveData存储和恢复数据的方式。考虑一个具有以下字段的ViewModel:
LiveData<String> SnackbarMessage = new MutableLiveData<>()
活动开始观察此情况,并且ViewModel完成操作,因此需要更新消息:
SnackbarMessage.setValue("项目已保存!");
该活动接受该值并显示Snackbar。显然,它有效。
但是,如果用户先旋转手机,则会创建新活动并开始观察。当LiveData观察开始时,activity立即收到旧值,这导致消息再次显示!
不应尝试通过库或架构组件扩展来解决此问题,而应将其视为设计问题。我们建议您将您的事件视为你所在状态的一部分。
✅将事件作为您的状态设计。
9、ViewModel泄露
响应式范式在Android中运行良好,因为它允许UI和应用程序的其余层之间建立便捷的连接。LiveData是此结构的关键组成部分,因此通常您的activity和fragment将观察LiveData实例。
ViewModel如何与其他组件通信取决于您,但要注意泄露和边缘情况。考虑下图,其中View层使用观察者模式,数据层使用回调:
如果用户退出应用程序,View将消失,因此不再观察到ViewModel。如果Repository是单例或以其他方式限定于应用程序,则在进程被终止之前,Repository不会被销毁。仅当系统需要资源或用户手动终止应用程序时才会发生这种情况。如果Repository持有对ViewModel中的回调的引用,则ViewModel将暂时泄露。
如果ViewModel很轻或者操作保证快速完成,那么这种泄露并不是什么大问题。然而,这并非总是如此。理想情况下,只要没有任何View观察它们,ViewModel就应该可以自由运行:
您有多重选择来实现这一目标:
- 使用
ViewModel.onCleared()
,您可以告诉Repository删除对ViewModel的回调。 - 在Repository中,您可以使用
ReakReference
,也可以使用EventBus
(两者都很容易被误用,甚至被认为是有害的) - 使用LiveData在Repository和ViewModel之间进行通信的方式与在View和ViewModel之间使用LiveData的方式类似。
✅考虑边缘情况、泄露以及长时间运行的操作如何影响架构中的实例。
❌不要将对于保存干净状态或与数据相关的关键逻辑放入ViewModel中。您从ViewModel进行的任何调用都可以是最后一次。
10、Repository中的LiveData
为了避免泄露ViewModel和回调地狱,可以这样观察Repository:
当ViewModel被清除或者View的生命周期结束时,订阅也会被清除:
如果您尝试这种方法,就会遇到一个问题:如果您无权访问LifecycleOwner
,如何从ViewModel订阅Repository?使用Transformation
是解决这个问题的一种非常方便的方法。Transformations.switchMap
允许您创建一个新的LiveData,以相应其他LiveData实例的更改。它还允许跨链携带观察者生命周期信息:
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);
在此示例中,当触发器获得更新时,将应用该函数并将结果发送到下游。活动将进行观察repo
,并且相同的LifecycleOwner
将用于调用repository.loadRepo(id)
。
✅每当您认为ViewModel中需要一个Lifecycle对象时,Transformation可能就是解决方案。
11、扩展LiveData
LiveData最常见的用例是MutableLiveData
在ViewModel中使用并将它们公开,以LiveData
使它们对观察者来说是不可变的。
如果您需要更多功能,扩展LiveData会让您知道何时有活动观察者。例如,当您想要开始监听位置或传感器服务时,这非常有用。
public class MyLiveDate extends LiveData<MyData> {
public MyLiveData(Context context) {
// Initialize service
}
@Override
protected void onActive() {
// Start listening
}
@Override
protected void onInactive() {
// Stop listening
}
}
12、何时不扩展LiveData
还可以用onActive()
启动一些加载数据的服务,但除非您有充分的理由,否则您不需要等待LiveData
被观察到。一些常见的模式:
- 向ViewModel添加一个
start()
方法并尽快调用它 - 设置启动加载的属性
❌您通常不会扩展LiveData。让那您的Activity或Fragment告诉ViewModel如何开始加载数据。