ViewModel
应该算是Jetpack 中最重要的组件之一了。其实Android 平台上之所以会出现注入MVP、MVVM 之类的项目架构,就是因为在传统的开发模式下,Activity 的任务实在是太重了,既要负责逻辑处理,又要控制UI 提示,甚至还得处理网络回调,等等。在一个小项目中这样写或许没有什么问题,但是如果在大型项目中仍然使用这样写法的话,那么这个项目将会变得非常臃肿并且难以维护,因为没有任何架构上的划分。
而ViewModel
的一个重要作用就是可以帮助Activity 分担一部分工作,它是专门用于存放于界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel
中,而不是Activity 中,这样可以在一定程度上减少Activity 中的逻辑。
另外,ViewModel
还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候,Activity 会被重新创建,同时存放在Activity 中的数据也会丢失。而ViewModel
的生命周期和Activity 不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity 退出的时候才会跟着Activity 一起销毁。因此,将与界面相关的变量存放在ViewModel
当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。ViewModel
的生命周期如图所示:
由于Jetpack 中的组件通常是以 AndroidX 库的形式发布的,因此一些通常的Jetpack 组件会在创建AndroidX 项目时自动被包含进去。不过如果我们想要使用 ViewModel
组件,还需要在app/build.gradle 文件中添加如下依赖:
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
通常来讲,比较好的编程规范是给每一个 Activity 和 Fragment 都创建一个对应的ViewModel
,因此这里我们就位 MainActivity 创建一个对应的 MainViewModel 类,并让它继承自ViewModel
,代码如下所示:
import androidx.lifecycle.ViewModel
class MainViewModel: ViewModel() {
}
根据前面所学的知识,所有与界面相关的数据都应该放在ViewModel
中。那么这里我们要实现一个计数器的功能,就可以在 ViewModel
中加入一个 counter 变量用于计数,如下所示:
class MainViewModel : ViewModel() {
var counter: Int = 0
}
现在我们需要在界面上添加一个按钮,每点击一次按钮就让计数器加1,并且把最新的计数显示在界面上。修改activity_main.xml 中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/infoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="32sp" />
<Button
android:id="@+id/plusOneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Plus One" />
LinearLayout>
布局文件非常简单,一个TextView 用于显示当前的计数,一个Button 用于对计数器加1。
接着我们开始实现计数器的逻辑,修改MainActivity 中的代码,如下所示:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
plusOneBtn.setOnClickListener{
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter(){
infoText.text = viewModel.counter.toString()
}
}
注意:
我们绝对不可以直接去创建ViewModel
的实例,而是一定要通过ViewModelProvider
来获取 ViewModel
的实例,具体语法规则如下:
ViewModelProvider(<你的Activity 或 Fragment 实例>).get(<你的ViewModel>::class.java)
之所以这么写,是因为ViewModel
有其独立的生命周期,并且其生命周期要长于Activity 。如果我们在onCreate()
方法中创建一个ViewModel 的实例,那么每次onCreate()
方法执行的时候,ViewModel
都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了
除此之外的其他代码应该都是非常好理解的,我们提供了一个refreshCounter()
方法用来显示当前的计数,然后每次点击按钮的时候对计数器加1,并调用refreshCounter()
方法刷新计数
如果你尝试通过侧边工具栏旋转一下模拟器的屏幕,就会发现Activity 虽然被重新创建了,但是计数器的数据却没有消失
上一小节中创建的 MainViewModel 的构造函数中没有任何参数,但是思考一下,如果我们确实需要通过构造函数来传递一些参数,应该怎么办呢?由于所有ViewModel
的实例都是通过ViewModelProvider
来获取的,因此我们没有任何地方可以向ViewModel
的构造函数中传递参数
当然,这个问题也不难解决,只需要借助ViewModelProvider.Factory
就可以实现了
现在的计数器虽然在屏幕旋转的时候不会丢失数据,但是如果退出程序之后再重新打开,那么之前的计数就会被清零了。接下来我们就对这一功能进行升级,保证即使在退出程序后又重新打开的情况下,数据仍然不会丢失。
相信你已经猜到了,实现这个功能需要在退出程序的时候对当前的计数进行保存,然后在重新打开程序的时候读取之前保存的计数,传递给MainViewModel 。因此,这里修改 MainViewModel 中的代码,如下所示:
class MainViewModel(countReserved: Int) : ViewModel() {
var counter: Int = countReserved
}
现在我们给 MainViewModel 的构造函数添加了一个 countReserved 参数,这个参数用于记录之前保存的计数值,并在初始化的时候赋值给 counter 变量
前面已经说了需要借助 ViewModelProvider.Factory ,因此新建一个 MainViewModelFactory 类,并让它实现 ViewModelProviders.Factory 接口,代码如下所示:
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
可以看到,MainViewModelFactory 的构造函数中也接收了一个 countReserved 参数。另外 ViewModelProvider.Factory 接口要求我们必须实现create()
方法,因此这里在create()
方法中我们创建了 MainViewModel 实例,并将 countReserved 参数传了进去。为什么这里就可以创建 MainViewModel 的实例了呢?因为create()
方法的执行时机和 Activity 的生命周期无关,所以不会产生之前提到的问题
另外,我们还得在界面上添加一个清零按钮,方便用户手动将计数器清零。修改activity_main.xml 中的代码,(在原来基础上增加一个按钮即可)如下所示:
<Button
android:id="@+id/clearBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Clear"
/>
修改 MainActivity 中的代码
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var sp:SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved",0)
viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
plusOneBtn.setOnClickListener{
viewModel.counter++
refreshCounter()
}
clearBtn.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter(){
infoText.text = viewModel.counter.toString()
}
override fun onPause() {
super.onPause()
sp.edit{
putInt("count_reserved",viewModel.counter)
}
}
}
现在重新运行程序,点击数次”Plus One“ 按钮,然后退出程序并重新运行,你会发现,计数器的值是不会丢失的,只有点击”Clear“ 按钮,计数器的值才会被清零。如图所示:
本章内容源自 郭霖大神的《第一行代码 第三版》