ViewModel类被设计成可以存储和管理UI相关联的数据,并且能够感知生命周期。ViewModel类允许当配置更改的时候数据被保存,比如屏幕旋转的时候。
如果系统销毁或者重新创建了UI控制器(Activity),任何你存储的短暂的UI相关的数据就会丢失。对于一些简单的数据,Activity能够通过onSaveInstanceState()方法存储数据,但是这种方法仅仅适合小数量的可序列化和反序列化的数据,并不适合大数量的数据或者Bitmap。
另一个问题就是,UI控制器频繁地做一些异步操作,这些异步操作往往会消耗一定的时间再返回。UI控制器就需要管理这些操作并且确保在系统销毁之后清除它们,避免内存泄露,这种维护需要花费大量的精力,并且在配置改变(屏幕旋转)的情况下,维护更加困难。
Activity以及Fragment的主要目的其实是显示数据,响应用户的动作,与操作系统通讯,比如权限请求。如果你还要在其中加载数据库中的数据或者请求网络上的数据,指定过多的职责给它们会导致一个类中处理完了所有的事情,而不是将这些工作委托给其它类来处理。并且,指定这么多职责给UI控制器也会导致测试更加困难。
更简单和有效的做法是从UI控制器的逻辑中分离出 视图 和 数据 的职责。
实现一个ViewModel
Architecture Components(架构组件)为UI控制器提供ViewModel类来为UI准备数据。ViewModel对象在配置改变期间可以自动保存数据,因此在新的Activity或者Fragment实例中使用的是相同的数据,不会丢失。例如,如果你需要在你的App中显示一个用户列表,确保ViewModel获取这个List<User>,而不是将List<User>保存到Activity或者Fragment中。
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<User>>();
loadUsers();
}
return users;
}
private void loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
接下来你可以在Activity中访问List<User>,像下面这样:
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}
如果Activity重新创建(re-created)了,它会收到第一个Activity创建的同一个MyViewModel实例,当Activity被销毁了,框架(Framework)会调用ViewModel对象的onCleared()方法以释放资源。
注意:ViewModel绝对不要引用可能包含一个Context的任何对象,比如View,Lifecycle。
ViewModel对象被设计成生命周期比视图的生命周期更长。这种设计意味着你可以更简单地写测试,因为它不知道视图以及生命周期相关的东西。
ViewModel对象也可以包含LifecycleObservers,比如LiveData对象。然而,ViewModel对象不会观察到有生命周期感知的对象的改变,比如LiveData对象。如果ViewModel需要Context对象,例如需要找一个系统服务,那么可以继承AndroidViewModel类,这个类有一个含有Application的构造方法,Application继承于Context。
ViewModel的生命周期
当获取ViewModel的时候,ViewModel对象的生命周期交给了ViewModelProvider的Lifecycle来处理,ViewModel会一直存在在内存中直到下面这两种情况下会在生命周期中永久性地销毁:1.当在Activity中的时候,当Activity finish的时候。2.当在Fragment中的时候,当Fragment detached的时候。
下面的情景展示了当Activity经历了旋转,然后被关闭了后,ViewModel在各个生命周期中的状态。同时,下面的这个情景也适用于Fragment:
image.png
一般情况下,你在Activity中的OnCreate方法中生成一个ViewModel实例,有时候onCreate方法会在它的生命周期中被调用多次,比如屏幕旋转的时候,而ViewModel实例会在你第一次调用onCreate方法时创建,直到Activity被销毁。
在Fragment之间共享数据
在一个Activity中,两个Fragment或者多个Fragment之间进行数据共享是很常见的事情。想象一个普通场景,一个显示List列表的Fragment,当你点击其中的Item会跳转到详情Fragment。这么一个常见的场景,常用的做法是,这几个Fragment都需要定义一些接口Interface,并且它们的宿主Activity必须将它们绑定在一起。除此之外,所有的Fragment必须处理另外一个Fragment还未创建(Create)或者还不可见的情况。
然而,你可以通过使用ViewModel对象来解决这个痛点问题。这些Fragment可以通过共享一个Activity生命周期范围内的ViewModel对象来通信,就像下面这样:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}
我们注意到,两个Fragment都取回(getActivity())了它们的宿主Activity,它们通过ViewModelProvider取回了同一个Activity范围内的SharedViewModel实例。
这样做有下面这些好处:
- Activity不用做任何事情,或者知道任何它们需要通信的细节
- Fragment除了需要耦合SharedViewModel之外,它们不需要知道另外的Fragment的细节。如果一个Fragment消失了,另外一个Fragment还是会正常工作
- 每个Fragment都有它们自己的生命周期,不会影响到另外一个Fragment的生命周期,如果一个Fragment替代了另外一个,UI界面还是会继续正常工作
给ViewModel替换载入器(Loaders)
Loader类,比如像CursorLoader类频繁被用于保存 UI界面 和 数据库 中的同步数据。你可以使用ViewModel和一些其他类来替换载入器(Loader)。用一个ViewModel来分离UI控制器和载入数据操作,你可以在这些类之间保存更少的强引用。
举一个很常用的使用Loader的情景,App可能会使用CursorLoader来观察数据库的内容。当数据库中的一个值改变,Loader会被自动触发并且重新加载数据,并且更新UI界面:
image.png
ViewModel和Room以及LiveData一起可以代替Loader。ViewModel保证在配置改变的时候保存数据,Room数据库改变时通知你的LiveData,LiveData反过来更新你的修正后的数据。
image.png
网友评论