Jetpack ViewModel

ViewModel 的一个重要作用就是可以帮助 Activity 分担一部分工作,以注重生命周期的方式存储和管理界面相关的数据。

另一个非常重要的特性是它可以保证手机屏幕发生旋转时不会被重新创建,因为它的生命周期和 Activity 不同,只有当 Activity 退出时才会跟着 Activity 一起销毁,这样就避免了当手机屏幕发生旋转时,存放在 Activity 中的数据丢失问题。

基本用法

首先要添加依赖:

1
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

1
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 比较好的编程规范,
* 是给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel。
*/
class MainViewModel:ViewModel() {

var counter = 0;
}


/**
* 如果想在 ViewModel 类中使用资源文件,则需要使用到 Context 上下文,这时不能将 Activity 的上下文传给 ViewModel,否则会存在内存泄漏的风险。
* 这种情况下需要将父类 ViewModel 修改为 AndroidViewModel 即可,拥有 Application 的实例后就可以访问资源文件了。
*/
class MainViewModel(application: Application) : AndroidViewModel(application) {

var counter = 0;
}
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
class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 绝对不可以直接创建 ViewModel 的实例,而是要通过 ViewModelProviders 来获取。

// 因为 ViewModel 有其独立的生命周期,并且比 Activity 长,(所以千万不要传递 Activity 过去)
// 如果在 onCreate() 中创建了 ViewModel 实例,那么每次 onCreate() 执行时都会创建一个新的 ViewModel,
// 这样当手机屏幕旋转时,就无法保留其中的数据了。
// 注:ViewMolder 查看官网:已弃用 ViewModelProviders.of() 。
// 您可以将 Fragment 或 FragmentActivity 传递给新 ViewModelProvider(ViewModelStoreOwner) 构造函数,以便在使用 Fragment 1.2.0 时实现相同的功能。
// 如下:通过 ViewModelProvider 来获取 ViewModel 的实例:
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

btnPlus.setOnClickListener{
viewModel.counter++
refreshCounter()
}
refreshCounter()
}

/**
* 用来显示当前的计数
*/
private fun refreshCounter() {
tvInfo.text = viewModel.counter.toString()
}
}

Kotlin 为开发者提供了许多扩展插件。在实际项目开发中,可以借助 KTX 更简单地创建 ViewModel 实例。以在 Activity 中创建 ViewModel 为例,在 build.gradle 添加 KTX 扩展:

1
implementation("androidx.fragment:fragment-ktx:1.5.0")

然后可通过如下代码获取 ViewModel:

1
val mainViewModel by viewModels<MainViewModel>()

生命周期

ViewModel 的生命周期要比 Activity 长很多,如果 ViewModel 引用 View 则会导致当视图被销毁时引用的资源得不到释放,从而导致内存泄漏。

在 onCreate 中创建了 ViewModel 对象,屏幕旋转后又重新创建了对象,为什么 ViewModel 对象会一直存在?从创建 ViewModel 对象的方法来看:

1
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
1
2
3
4
5
6
@MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
val canonicalName = modelClass.canonicalName
?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get("$DEFAULT_KEY:$canonicalName", modelClass)
}

在 modelClass 不为 null 的情况下,程序会走到 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
private val store: ViewModelStore,

@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
val viewModel = store[key]
if (modelClass.isInstance(viewModel)) {
(factory as? OnRequeryFactory)?.onRequery(viewModel)
return viewModel as T
} else {
@Suppress("ControlFlowWithEmptyBody")
if (viewModel != null) {
// TODO: log a warning.
}
}
val extras = MutableCreationExtras(defaultCreationExtras)
extras[VIEW_MODEL_KEY] = key
// AGP has some desugaring issues associated with compileOnly dependencies so we need to
// fall back to the other create method to keep from crashing.
return try {
factory.create(modelClass, extras)
} catch (e: AbstractMethodError) {
factory.create(modelClass)
}.also { store.put(key, it) }
}

创建 ViewModel 的方法采用的是工厂模式,创建好之后将其缓存在 ViewModelStore 中。

如果当前需要创建的 ViewModel 对象已经存在,则直接从 ViewModelStore 中取出。

所以在屏幕旋转前后使用的 ViewModel 是同一个对象。也就是说在创建 ViewModel 时,只要传入的 class 对象是一样的,那么获取到的 ViewModel 就是同一个对象,基于这一点可以很轻松地在同一个宿主 Activity 的不同 Fragment 之间进行数据共享。


向 ViewModel 传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 借助 ViewModelProvider.Factory 实现向 MainViewModel 的构造函数中传递数据。
*/
class MainViewModelFactory(private val countReserved:Int) : ViewModelProvider.Factory{

/**
* 这里的 onCreate() 执行时机和 Activity 的生命周期无关,
* 所以这里可直接创建 MainViewModel 实例。
*/
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
1
2
3
4
5
6
7
/**
* countReserved 参数用于记录之前保存的数值,并在初始化时赋值给 counter 变量。
*/
class MainViewModel(countReserved:Int):ViewModel() {

var counter = countReserved;
}
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
34
35
36
class MainActivity : AppCompatActivity() {

lateinit var viewModel: MainViewModel
lateinit var sp: SharedPreferences

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 获取 sp 实例
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved",0)
// 在 of() 中传入 MainViewModelFactory 参数,只能通过这种方式来向 ViewModel 传递参数。
viewModel = ViewModelProviders.of(this,MainViewModelFactory(countReserved))
.get(MainViewModel::class.java)
btnPlus.setOnClickListener{
viewModel.counter++
refreshCounter()
}
refreshCounter()
}

/**
* 用来显示当前的计数
*/
private fun refreshCounter() {
tvInfo.text = viewModel.counter.toString()
}

override fun onPause() {
super.onPause()
sp.edit {
putInt("count_reserved",viewModel.counter)
}
}
}

使用 ViewModel 实现数据共享

在实际开发中,经常会遇到两个 Fragment 之间有通信的需求,常见的解决办法如:

  • 通过数据存取的方式,记录需要的通信信息。
  • 通过为宿主 Activity 增加实现接口的方式进行通信。

这里通过 ViewModel 的特性可以更简单的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class EntertainmentViewModel : ViewModel() {

// 如果将类的属性设为公共属性暴露给外部,则无法保证数据的统一性和完整性。
// 所以设为私有变量,并单独提供设置和获取的方法,将操作放在类的内部。
private var currentPosition:Int = 0

fun getCurrentPosition():Int{
return currentPosition
}

fun positionChanged(currentPosition:Int){
this.currentPosition = currentPosition
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class OneFragment : BaseFragment() {

lateinit var entertainmentViewModel:EntertainmentViewModel

override fun init(view: View) {
// requireActivity() 获取的是宿主 Activity 对应的 ViewModel 对象,此种获取方式可以用来实现数据共享。
// 如果参数是 this,则获取的是 Fragment 各自对应的 ViewModel 对象,此种方式不能用来实现数据共享功能。
entertainmentViewModel = ViewModelProvider(requireActivity()).get(EntertainmentViewModel::class.java)
entertainmentViewModel.positionChanged(1)
Log.d(TAG,"${entertainmentViewModel.getCurrentPosition()}")
}

}
1
2
3
4
5
6
7
8
9
10
11
class TwoFragment : BaseFragment() {

lateinit var entertainmentViewModel:EntertainmentViewModel

override fun init(view: View) {
entertainmentViewModel = ViewModelProvider(requireActivity()).get(EntertainmentViewModel::class.java)
entertainmentViewModel.positionChanged(2)
Log.d(OneFragment.TAG,"${entertainmentViewModel.getCurrentPosition()}")
}

}

源码分析

ViewModelProvider(this) 中 this 的参数类型是 ViewModelStoreOwner

ViewModelProvider
1
2
3
public constructor(
owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))

在 androidx.activity.ComponentActivity 中实现了 ViewModelStoreOwner 接口并实现了 getViewModelStore 方法,因此可以直接使用当前 Activity 的上下文 this。

1
2
3
4
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ViewModelStoreOwner{
...
}
1
2
3
4
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
ComponentActivity
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
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
ensureViewModelStore();
return mViewModelStore;
}

@SuppressWarnings("WeakerAccess") /* synthetic access */
void ensureViewModelStore() {
if (mViewModelStore == null) {
// 获取上一次的配置信息
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// 如果上一次配置信息不为空,则直接使用上一次的 viewModelStore
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
// 如果为空则新建 viewModelStore
mViewModelStore = new ViewModelStore();
}
}
}

// NonConfigurationInstances 中有 viewModelStore 对象
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}

在 Activity 旋转屏幕被销毁时,不仅会调用 onSaveInstanceState 方法,而且会调用 onRetainNonConfigurationInstance 方法

ComponentActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final Object onRetainNonConfigurationInstance() {
// Maintain backward compatibility.
Object custom = onRetainCustomNonConfigurationInstance();

ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
// No one called getViewModelStore(), so see if there was an existing
// ViewModelStore from our last NonConfigurationInstance
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}

if (viewModelStore == null && custom == null) {
return null;
}
// 对 viewModelStore 进行了存储
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
1
2
3
4
// 当屏幕旋转恢复时通过 getLastNonConfigurationInstance 方法进行恢复
public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;

所以在 Activity 旋转屏幕的整个过程中,ViewModelStore 对象保留了下来,通过 ViewModelProvider(this).get 方法获取到的是同一个 ViewModel 实例,从而避免了由于屏幕旋转而导致数据丢失的问题。

ViewModel 虽然可以防止屏幕旋转引起的数据丢失,但并能代替 onSaveInstanceState 方法,主要原因有两点:

  • onSaveInstanceState 方法可以存储少量的序列化数据,ViewModel 可以存储任意数据,只是使用时的限制不同。
  • onSaveInstanceState 可以达到数据持久化的目的,但是 ViewModel 不可以,使用场景不同。

ViewModel 不能达到数据持久化的目的是因为当 Activity 被真正销毁时,ViewModel 会将资源进行回收

1
2
3
4
5
6
7
8
9
10
11
12
13
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
...
// 当对应的 Activity 被真正销毁时,即不是屏幕旋转导致页面被销毁时,ViewModelStore 将会调用 clear 方法清理数据,所以 ViewModel 并不能达到数据持久化的目的。
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});

备注

参考资料

第一行代码(第3版)

《Android Jetpack开发 原理解析与应用实战》