Android 架构组件之 ViewModel

ViewModel 是用来保存应用 UI 数据的类,它会在配置变更后继续存在。本文将从以下几个方面来介绍 ViewModel。

  • 使用 ViewModel 有哪些优势?
  • 如何使用 ViewModel。
  • 分析 ViewModel 的组成及原理。
  • 总结 ViewModel 的使用及注意事项。

1. 使用 ViewModel 有哪些优势?

ViewModel 是用来保存应用 UI 数据的类,它会在配置变更后继续存在。让我们来看看它具有的特点。

手机屏幕旋转是配置变更的一种,当旋转屏幕时,Activity 会被重新创建,如果数据没有被正确的保存和恢复,就有可能丢失,从而导致明明奇妙的 UI 错误,甚至出现应用的崩溃,相反的,ViewModel 会在配置变更后继续存在。

Google 推荐的架构设计:
将应用所有的 UI 数据保存在 ViewModel 中,而不是 Activity 中,这样能确保数据不会收到配置变更带来的影响。

Android 开发中,一个常见的坑,就是把很多变量,逻辑和数据放在 Activity 或 Fragment 中,这样的代码比较混乱和难以维护。这种开发模式也违反了单一责任的原则。

ViewModel 可以有效地划分责任。具体的,它可以用来保存 Activity 中的所有数据,然后 Activity 仅负责了解如何在屏幕上显示该数据和接收用户互动,但是它不会处理这些互动。

如果你的应用加载和存储数据,建议创建一个 Repository 的存储区类,另外,应该确保 ViewModel 不会因为承担过多的责任而变得臃肿。要避免这种情况,可以创建 Presenter 类,或者实现一种更成熟的架构。

了解了 ViewModel 在架构设计中的地位,再来看一下 ViewModel 的生命周期:

从图中可以看出 ViewModel 在屏幕旋转后,ViewModel 继续存在,并没有出现重建。

2. 如何使用 ViewModel

要创建一个 ViewModel,首先需要扩展 ViewModel 类,然后将 Activity 中之前与 UI相关的实例变量,摆放在这个 ViewModel 中。

1
2
3
class ChronometerViewModel : ViewModel() {
var mStartTime: Long = 0L
}

接着在 Activity 的 onCreate()方法中,从 ViewModel Provider 的框架实用类再获取 ViewModel,请注意:ViewModelProvider 将获取一个 Activity 实例,这种机制让你在旋转屏幕时,获取一个新的 Activity 实例,不过,请确保它始终与同一个 ViewModel 关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.chrono_activity_2)

// The ViewModelStore provides a new ViewModel or one previously created.
val chronometerViewModel = ViewModelProvider(this).get(ChronometerViewModel::class.java)

if (chronometerViewModel.mStartTime == 0L) {
// If the start date is not defined, it's a new ViewModel so set it.
val startTime = SystemClock.elapsedRealtime()
chronometerViewModel.mStartTime = startTime
chronometer.base = startTime

} else {
// Otherwise the ViewModel has been retained, set the chronometer's base to the original
// starting time.
chronometer.base = chronometerViewModel.mStartTime ?: 0
}
chronometer.start()
}

对于 ViewModel实例,你可以使用 getter()方法,从 Activity 直接获取 UI数据,ViewModel 的默认构造函数是没有任何参数的,如果想要修改,可以使用 ViewModelProvider.Factory 创造一个自定义函数。

2.1 通过 ViewModel 和 LiveData 实现 Fragment 之间数据共享

上面介绍的,是 ViewModel 最简单的用例。ViewModel 类也可以很友好地与 LiveData和 DataBinding互相搭配使用。使用 ViewModel 和 LiveData 可以创建反应式界面,也就是说当底层数据被更新时,UI 也会相应的自动更新。关于 LiveData 的介绍,查看Android 架构组件之 LiveData

通过 ViewModel 与LiveData的组合,实现 Fragment 之间数据的共享,先看一下示例运行的效果图。

界面中包含两个 Fragment,每一个 Fragment 中都有一个 SeekBar 控件,通过 ViewModel 与LiveData 实现 SeekBar 进度值的共享。

首先定义 ViewModel

1
2
3
class SeekBarViewModel : ViewModel() {
val seekbarValue = MutableLiveData<Int>()
}

在 Fragment 的 onCreateView()方法中,完成 viewModel 实例的获取,并完成注册监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_share_data, container, false)
mSeekBar = root.findViewById(R.id.seekBar)
mSeekBarViewModel = ViewModelProvider(requireActivity()).get(
SeekBarViewModel::class.java
)
subscribeSeekBar()
return root
}

完整的示例代码,在文末给出 GitHub 地址。

3. 分析 ViewModel 的组成及原理

我们用官方最新的lifecycle-viewmodel:2.2.0-alpha02 版本来分析,先来看一张类图:

从上面的类图我们可以看到:

  • ViewModelProviders 提供了多个 of() 重载的静态方法用来获取 ViewModelProvider 对象。

  • ViewModelProvider 中定义了一个接口 Factory 和两个内部类 NewInstanceFactory 和 AndroidViewModelFactory,NewInstanceFactory实现了 Factory 接口,AndroidViewModelFactory 是 NewInstanceFactory 的子类。Factory 接口中只有一个 create() 方法,接收一个实现了 ViewModel 类的 Class对象,返回该 Class对象 newInstance() 后的实例,可以看出 create()的实现都是通过反射完成。

  • 在 ViewModelProvider 中有两个成员变量 mFactory 和 mViewModelStore。mFactory 可以是通过 ViewModelProviders 的 of()传递进来的 Factory 对象,如果 of() 方法没有传进来 Factory 对象,会创建一个AndroidViewModelFactory对象。

  • 在 ViewModelStore 中维护着一个 Map 用来存储创建的 ViewModel 对象。

  • ViewModelProvider 的一个构造方法中需要一个实现了 ViewModelStoreOwner 接口的对象,最新的 androidx 包中的 Activity 和 Fragment 都实现了这个接口。这个接口只有一个 getViewModelStore()的方法,所以我们可以在 Activity 和 Fragment 中通过 getViewModelStore()方法获得 ViewModelStore 对象,通过 ViewModelStore维护的 Map 最终得到 ViewModel 实例。使用这种方法,我们可以实现 Fragment 之间的通信。

了解了跟 ViewModel 有关的类,我们再通过时序图来了解一下这些类之间的调用关系,在时序图中,我们重点关注几个过程:ViewModelStore 和 ViewModel 实例的创建过程,页面销毁时 onCleared()方法的执行过程。

3.1 ViewModelStore 和 ViewModel 实例的创建过程

首先我们在 Fragment(或Activity)中调用 ViewModelProviders 的 of() 方法,这个方法最终会返回一个 ViewModelProvider 对象。

在 of() 方法中主要完成了以下几个工作:

  • 确定 application,通过 checkApplication() 方法;
  • 确定 factory 对象,如果没有通过 of() 传入,调用ViewModelProvider 中的 AndroidViewModelFactory 创建;
  • 创建 ViewModelProvider 对象。
1
2
3
4
5
6
7
8
9
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
Application application = checkApplication(checkActivity(fragment));
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
return new ViewModelProvider(fragment.getViewModelStore(), factory);
}

ViewModelStore 的创建过程,最新的 androidx 包中的 Activity 和 Fragment 都实现了 ViewModelStoreOwner 接口,这个接口只有一个 getViewModelStore() 的方法。在 Activity 和 Fragment 中通过 getViewModelStore() 方法获得 ViewModelStore 对象。而 ViewModelStore 中维护着的 Map 保存着 ViewModel 实例。

1
2
3
4
5
public interface ViewModelStoreOwner {

@NonNull
ViewModelStore getViewModelStore();
}

通过 of() 方法创建了 ViewModelProvider 对象后,再来看一下 ViewModelProvider 类中的 get() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

private static final String DEFAULT_KEY =
"androidx.lifecycle.ViewModelProvider.DefaultKey";

对上面的代码中进一步跟踪中,我们可以分析出:

  • ViewModel 实例的创建过程是通过反射完成的,获取的 ViewModel 实例保存在 ViewModelStore 中的一个 Map 成员变量中;

  • ViewModelStore 中 Map 的 Key 是 DEFAULT_KEY 与 ViewModel 完整包路径拼接而成;

  • 如果 Map 中包含了这个 ViewModel 的实例,直接返回。如果没有,则通过调用 factory 的 create() 方法,创建 ViewModel 实例,并将这个实例保存到 Map 中。

3.2 onCleared()方法的执行过程

分析过了 ViewModel 的创建过程,再来看看 ViewModel 的销毁过程。

在 ComponentActivity 中,通过注册一个匿名的 LifecycleEventObserver 对象,来监听界面的是否被销毁。当收到页面被销毁的通知后,会调用 ViewModelStore 中的 clear() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ComponentActivity() {
....
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}

在 ViewModelStore 中,调用 Map 中每个 ViewModel 实例的 clear()方法,最后将 map 清空,至此,完成了 ViewModel 的销毁工作。

1
2
3
4
5
6
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}

4. 总结 ViewModel 的使用及注意事项

最后说一些使用 ViewModel的注意事项:

首先,任何时候,都不应该将 Context 传入 ViewModel。也就是说,Fragment、Activity 和 View 都不能被传入,正如此前介绍的一样,ViewModel 可以比相联的 Activity 和 Fragment 的生命周期更长。假设在 ViewModel 中存储了一个 Activity,那么当你旋转屏幕时,那个 Activity 将被销毁,但是 ViewModel 还存储着已经被销毁的 Activity 的引用,这就是一种内存泄露。

如果你需要比 ViewModel 的生命周期更长的 Application 类,可以使用 AndroidViewModel 的子类,通过这个子类,就可以直接使用 Application 的引用了。

第二点,ViewModel 并不应该取代 onSaveInstanceState的使用,它们两个是相辅相成的,当进程被关闭时,ViewModel 将被销毁,但是 onSaveInstanceState 将不会受到影响。

另外,ViewModel 可以用来存储大量数据,而 onSaveInstanceState 就只可以用来存储有限的数据。我们尽可能把多一点的 UI数据往 ViewModel 内存储,以便在配置变更时不需要重新加载或生成数据,另一方面,如果进程被 Framework 关闭,我们应该用 onSaveInstanceState 来存储,足以还原 UI 状态的最少量数据。比如用户的数据库 ID。


参考文献

ViewModel Overview


评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×