Jetpack 简介
Jetpack 是 Google 推出的一套库、工具和指南的集合,旨在帮助开发者轻松构建 Android 应用。它的主要目的是:
将 Android 开发的最佳实践内建在组件中;提供简洁的 API 减少重复性代码;提供向后兼容的组件,组件可以在任何 Android 系统版本上运行。
我们新建一个名为 JetpackTest 的 Empty Views Activity 项目,就可以开始学习了。
ViewModel
ViewModel 是 Jetpack 中最重要的组件之一。因为在传统的开发模式下,Activity 的任务太重了。既要负责逻辑的处理,又要控制 UI 的展示等。小型项目还好,但是如果在大型项目中还这样写,项目会变得非常臃肿且难以维护,因为它没有任何架构上的划分。
ViewModel 的一个作用就是帮助 Activity 分担任务,专门用于存放与界面相关的数据。也就是界面看得到的数据,它的相关变量需要放在 ViewModel 中,而不是 Activity 中。
另一个作用是在手机屏幕旋转后,保证界面的数据不会丢失。因为当手机发生横竖屏旋转后,Activity 会重新创建,导致存放在 Activity 中的数据丢失。而 ViewModel 的生命周期和 Activity 不同,它在手机屏幕旋转后并不会被重新创建。只有当该 Activity 被销毁,且被移出了任务栈之后(例如按下返回键或是调用 finish() 方法),ViewModel 实例才会跟着销毁(onCleared() 方法被调用)。
ViewModel 的生命周期:
我们来通过一个计数器示例学习 ViewModel 的基本用法。
ViewModel 的基本用法
使用 ViewModel 需要先添加依赖:
// build.gradle.kts (Module: app)
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1")
}
首先,我们为 MainActivity 创建一个对应的 MainViewModel 类,继承自 ViewModel。代码如下:
class MainViewModel : ViewModel() {
}
然后,将与计数相关的 counter 变量放到该 ViewModel 中:
class MainViewModel : ViewModel() {
var counter = 0
}
接着,在布局中放置一个 Button,用于触发计数器 +1。放置一个 TextView,用于显示当前的计数。activity_main.xml 文件中的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<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>
最后,在 MainActivity 中实现计数器的逻辑。
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 通过 ViewModelProvider 获取 ViewModel 实例
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
/**
* 更新当前的计数
*/
private fun refreshCounter() {
binding.infoText.text = "count: ${viewModel.counter}"
}
}
其中,我们使用了 ViewModelProvider(this)[MainViewModel::class.java] 这行代码来获取 ViewModel 实例,那为什么不能直接创建 ViewModel 的实例呢?
因为如果我们直接在 onCreate() 方法中创建 ViewModel 实例,那么每次当 onCreate() 方法被调用时(比如因屏幕旋转导致Activity 重新创建),都会获取一个新的 MainViewModel 实例。这样,上一次实例的数据就会丢失,使得 ViewModel 失去了在配置变更中保存数据的核心意义。
而 ViewModelProvider(this) 会创建一个与 Activity 生命周期绑定的 Provider。
-
如果你是第一次请求,它会创建一个新的 ViewModel 实例,并保存起来,以便后续使用;
-
如果不是第一次,它会返回存在的旧实例。
运行程序,并点击界面中的按钮,即可增加计数:
即使屏幕发生旋转,界面中的数据也不会丢失。
其实,我们可以使用 activity-ktx 库中的属性委托 by viewModels() 来简化上述代码。
首先,添加依赖:
// build.gradle.kts
dependencies {
implementation("androidx.activity:activity-ktx:1.10.1")
}
然后,修改 MainActivity。代码如下:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 使用属性委托声明
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
/**
* 更新当前的计数
*/
private fun refreshCounter() {
binding.infoText.text = "count: ${viewModel.counter}"
}
}
向 ViewModel 传递参数
怎么向 ViewModel 传递一些参数呢?这需要用到 ViewModelProvider.Factory。现在,我们来实现即使在退出应用后重新打开的情况下,数据也不会丢失。
首先,给 MainViewModel 添加一个主构造函数,并带有一个 countReserved 参数,表示之前保存的数据。
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = countReserved
}
然后,创建 MainViewModelFactory 类,实现 ViewModelProvider.Factory 接口。
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
这个 Factory 的 create() 方法是用于指定创建 ViewModel 实例的方式的。
另外,在布局中添加一个“清零”按钮,用于清零计数器。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<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_marginTop="24dp"
android:text="Plus One" />
<Button
android:id="@+id/clearBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Clear" />
</LinearLayout>
最后,修改 MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
private lateinit var sp: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 恢复之前的数据
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved", 0)
// 创建我们自定义的 Factory 实例
val factory = MainViewModelFactory(countReserved)
// 将 Factory 传给 ViewModelProvider 来获取 ViewModel 实例
viewModel = ViewModelProvider(this, factory)[MainViewModel::class.java]
binding.plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
binding.clearBtn.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
binding.infoText.text = "count: ${viewModel.counter}"
}
override fun onPause() {
super.onPause()
// 保存当前的数据
sp.edit {
putInt("count_reserved", viewModel.counter)
}
}
}
可以看到,当需要传递参数时,只需在创建 ViewModelProvider 时,将自定义的 Factory 实例传入第二个参数即可。它会使用这个 Factory 来创建 MainViewModel 实例。
运行程序,退出应用并重新打开,计数值是不会丢失的。
同样地,属性委托也支持 Factory 模式,如下:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels {
// 恢复之前的数据
sp = getPreferences(Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved", 0)
// 需要返回我们自定义的 Factory 实例
MainViewModelFactory(countReserved)
}
private lateinit var binding: ActivityMainBinding
private lateinit var sp: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
binding.clearBtn.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
binding.infoText.text = "count: ${viewModel.counter}"
}
override fun onPause() {
super.onPause()
// 保存当前的数据
sp.edit {
putInt("count_reserved", viewModel.counter)
}
}
}