文章目录
三、ViewModel 视图模型
ViewModel 介于 View(视图) 和 Model(数据模型) 之间,可以解耦分层,架构如下:
因为 ViewModel 的生命周期比 Activity 长,所以当手机旋转屏幕时,可通过 ViewModel 处理数据的存储和恢复,其生命周期示例如下:
首先,新建 Jetpack3ViewModelTest 项目,在项目添加 implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
依赖。
然后,新建 TimerViewModel 类。当 ViewModel 不再被需要后,其 onCleared() 方法就会被调用,可在其中做一些释放资源的操作。而屏幕旋转并不会销毁 ViewModel,代码如下:
package com.bignerdranch.android.jetpack3viewmodeltest
import androidx.lifecycle.ViewModel
class TimerViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
}
}
接下来,为了验证生命周期,在 ViewModel 内每隔1秒,通过 startTiming 启动定时器,再通过 OnTimeChangeListener 通知其调用者,代码如下:
package com.bignerdranch.android.jetpack3viewmodeltest
import android.util.Log
import androidx.lifecycle.ViewModel
import java.util.*
class TimerViewModel : ViewModel() {
private var timer: Timer? = null
private var currentSecond: Int = 0
private val TAG = this.javaClass.name
// 开始计时
fun startTiming() {
if (timer == null) {
currentSecond = 0
timer = Timer()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
currentSecond++
if (onTimeChangeListener != null) {
onTimeChangeListener!!.onTimeChanged(currentSecond)
}
}
}
timer?.schedule(timerTask, 1000, 1000) //延迟1秒执行, 间隔1秒
}
}
// 通过接口的方式,完成对调用者的通知,这种方式不是太好,更好的方式是通过LiveData组件来实现
interface OnTimeChangeListener {
fun onTimeChanged(second: Int)
}
private var onTimeChangeListener: OnTimeChangeListener? = null
fun setOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
this.onTimeChangeListener = onTimeChangeListener
}
// 由于屏幕旋转导致的Activity重建,该方法不会被调用
// 只有ViewModel已经没有任何Activity与之有关联,系统则会调用该方法,你可以在此清理资源
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared()");
timer?.cancel()
}
}
然后,设置 activity_main.xml 的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="40sp"
android:text="TIME"/>
</RelativeLayout>
activity_main.xml 的布局如下:
然后,在 MainActivity 中通过 ViewModelProvider 创建 timeViewModel,并监听 timerViewModel 的 OnTimeChangeListener() 回调函数传来的 second 参数,并据此更新 UI 界面的 textView,代码如下:
package com.bignerdranch.android.jetpack3viewmodeltest
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import com.bignerdranch.android.jetpack3viewmodeltest.TimerViewModel.OnTimeChangeListener
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
private fun iniComponent() {
val tvTime = findViewById<TextView>(R.id.tvTime)
// 通过ViewModelProviders得到ViewModel,如果ViewModel不存在就创建一个新的,如果已经存在就直接返回已经存在的
val timerViewModel = ViewModelProvider(this).get(TimerViewModel::class.java)
timerViewModel.setOnTimeChangeListener(object : OnTimeChangeListener {
override fun onTimeChanged(second: Int) {
runOnUiThread { tvTime.text = "TIME:$second" } // 更新UI界面
}
})
timerViewModel.startTiming()
}
}
运行后,当按 Home键、查看预览屏、旋转屏幕时,ViewModel 的 Timer 均继续运行(即使 Activity 被销毁,而 ViewModel 却一直长存),效果如下:
3.1 ViewModel 源码解析
通过 val timerViewModel = ViewModelProvider(this).get(TimerViewModel::class.java)
可获得 viewModel 对象。
源码的 androix.FragmentActivity 实现了 ViewModelStoreOwner 接口,
public class FragmentActivity extends ComponentActivity implements
ActivityCompat.OnRequestPermissionsResultCallback,
ActivityCompat.RequestPermissionsRequestCodeValidator {
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements
ViewModelStoreOwner,
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
FragmentOnAttachListener {
@Override
public ViewModelStore getViewModelStore() {
return FragmentActivity.this.getViewModelStore();
}
}
}
源码中 androidx.lifecycle.ViewModelStore 源码如下,其以 HashMap<String, ViewModel> 缓存了 ViewModel,当页面需要 ViewModel 时,其会向 ViewModelProvider 索要,若存在则返回,若不存在则实例化一个。Activity 的销毁重建并不影响 ViewModel。注意我们不要向 ViewModel 传入任何类型的 Context,这可能会导致页面无法被销毁,从而引发内存泄漏:
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.lifecycle;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* Class to store {@code ViewModels}.
* <p>
* An instance of {@code ViewModelStore} must be retained through configuration changes:
* if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration
* changes, new instance of an owner should still have the same old instance of
* {@code ViewModelStore}.
* <p>
* If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated,
* then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would
* be notified that they are no longer used.
* <p>
* Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for
* activities and fragments.
*/
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}
因为 androidx.lifecycle.ViewModelStoreOwner 接口,被 Fragment、FragmentActivity 等类实现,实现的类如下图所示,所以在这些类中均可使用 ViewModel:
3.2 ViewModel 和 AndroidViewModel
- 不能把任何类型的 Context,或含 Context 引用的对象,传入 ViewModel,否则会导致内存泄漏
- 但可将 Context 传入 AndroidViewModel 类,其继承自 ViewModel 类,其生命周期和 Application 一样,也就不存在内存泄漏的问题了
3.3 ViewModel 和 onSaveInstanceState() 的区别
二者都可以解决旋转屏幕带来的数据丢失问题,当各有特殊用途。
- onSaveInstanceState() 只能保存少量的、能序列化的数据;当页面被完全销毁时仍可持久化数据。
- ViewModel 可保存页面中所有的数据;当页面被完全销毁时,ViewModel 也会被销毁,故无法持久化数据。