2018年,Google在I/O大会上发布了一系列辅助Android开发者的实用工具,合称为Jetpack,以辅助开发者构建出色的Android应用程序。Jetpack中的很多库非常好用,比如ViewModel、Navigation、Hilt、paging等。之前说过,Compose也是Jetpack中的一员,理所当然地可以使用Jetpack中的其他库,Jetpack中的其他库也都对Compose做了相应的适配。我们先看看在Compose中如何使用ViewModel。
Android中使用ViewModel
ViewModel是Jetpack中比较重要的一个库,也是实现MVVM架构的重要一环,可以有效减少Activity或Fragment和数据之间的耦合。ViewModel以生命周期的方式存储及管理UI相关数据。众所周知,Activity在配置改变的时候会重新走遍生命周期方法,当前,Activity中的数据也会随着Activity的重新创建而重新创建。
下面看一个经典的例子:屏幕上有一个TextView和一个Button,点击Button的时候修改TextView中的值,修改之后旋转屏幕再次查看TextView中的值是否改变。我们先创建一个布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/white">
<TextView
android:id="@+id/oneTvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="20dp"
android:textSize="30sp" />
<Button
android:id="@+id/oneBtnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Add Count"/>
</LinearLayout>
复制代码
一个垂直方向的线性布局,一个TextView和一个Button。再创建一个Activity:
class ViewModelActivity : AppCompatActivity() {
private lateinit var binding: AcViewmodelBinding
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AcViewmodelBinding.inflate(layoutInflater)
setTheme(androidx.appcompat.R.style.Theme_AppCompat_NoActionBar)
setContentView(binding.root)
initView();
}
private fun initView() {
binding.oneTvCount.text = count.toString()
binding.oneBtnAdd.setOnClickListener {
count += 2
binding.oneTvCount.text = count.toString()
}
}
}
复制代码
我们开启了ViewBinding功能,在点击Button时对count值每次加2。看看效果:
点了两次后TextView的值修改成了4,这时如果旋转屏幕,由于重新走了Activity的生命周期方法,所以TextView的值又变成了0,如图:
这种结果并不是我们想要的,我们希望系统配置修改的时候不影响Activity中的数据。当然在Activity中重写onSaveInstanceState方法并在方法内保存count值也是可以实现的,但有一些麻烦。我们可以直接使用ViewModel来实现。在Button的点击事件中必须为TextView重新赋值,并不能在数据发生改变的时候直接进行修改,而LiveData解决了这个问题。来看看怎么构建ViewModel:
class CViewModel(defaultCount: Int = 0) : ViewModel() {
private val _count = MutableLiveData(defaultCount)
val count: LiveData<Int>
get() = _count
fun onCountChanged(count: Int) {
_count.postValue(count)
}
}
复制代码
定义了一个CViewModel,继承自ViewModel。如果在ViewModel中需要使用Context,可以继承自AndroidViewModel,而不能直接将Activity或Fragment中的Context传入,因为ViewModel的生命周期要不Activity或Fragment长,会造成内存泄露。我们在CViewModel中将count值修改为LiveData,这样就可以观察count值的改变了。下面看看Activity中的代码需要做哪些修改:
private val viewModel by viewModels<CViewModel>() // 创建viewModel
private fun initView() {
viewModel.count.observe(this) {
binding.oneTvCount.text = it.toString() // 监听值变化,刷新UI
}
binding.oneBtnAdd.setOnClickListener {
val count = viewModel.count.value ?: 0
viewModel.onCountChanged(count + 2)
}
}
复制代码
将ViewModel定义一个全局变量,当按钮被点击的时候通过viewModel中定义的onCountChanged方法来修改count的值,然后通过观察viewModel中的count值来刷新TextView。这样就不需要修改count值的时候再主动修改TextView的值了。我们旋转屏幕后发现该值并没有发生改变。
我们一起回顾下ViewModel的生命周期:
在Compose中使用ViewModel
我们上面介绍了在Android中ViewModel的基本使用方式。现在看看在Compose中如何使用。首先我们添加相关的依赖库:
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07"
复制代码
然后按照上面Android View中的布局用Compose实现一遍:
@Composable
fun TestViewModel() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("0", modifier = Modifier.padding(10.dp))
Button(onClick = {
// 点击事件
}) {
Text("Add Count")
}
}
}
复制代码
好了,布局写完了,下面我们在Compose中应用ViewModel和LiveData:
@Composable
fun TestViewModel() {
val viewModel: CViewModel = viewModel()
val count by viewModel.count.observeAsState(initial = 0)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(count.toString(), modifier = Modifier.padding(10.dp))
Button(onClick = {
// 点击事件
viewModel.onCountChanged(count + 2)
}) {
Text("Add Count")
}
}
}
复制代码
使用Compose为我们定义好的可组合项ViewModel就可以构建好ViewModel,然后通过LiveData的扩展方法observeAsState将LiveData转为Compose中的可以观察的State数据。点击Button的时候会通过viewModel的onCountChanged方法修改count值,而Text中直接使用State数据count即可。count值发生改变的时候可组合项会发生重组来显示最新数据。看下效果:
横竖屏切换后数据也没有发生改变,符合预期。
Compose中ViewModel的进阶用法
上面例子也有一些问题,当把应用程序杀掉再重启的时候,之前保存的数据会清零。如果不想清零,就需要将数值保存下来,然后在初始化ViewModel的时候将保存下来的数据传入。这时就需要使用ViewModel.Factory了。看看它如何使用:
class CViewModelFactory(private val defaultCount: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return CViewModel(defaultCount) as T
}
}
复制代码
上面代码我们新建了一个CViewModelFactory类,让它实现ViewModelProvider.Factory接口,构造函数中接收保存下来的count值,然后在初始化CViewModel的时候通过构造方法传入,CViewModel中将传入的count值设置为默认值。
ViewModelProvider.Factory已经创建完成,那么在Compose中如何使用呢?看一下Compose中viewModel可组合项源码:
@Suppress("MissingJvmstatic")
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)
复制代码
一个参数key;一个参数factory,类型为ViewModelProvider.Factory。所以这里我们可以直接将ViewModelProvider.Factory当作参数传入。看下具体实例代码:
val context = LocalContext.current
val sp = context.getSharedPreferences("count_file", Context.MODE_PRIVATE)
val defaultCount = sp.getInt("DEFAULT_COUNT", 0)
val viewModel: CViewModel = viewModel(factory = CViewModelFactory(defaultCount))
val count by viewModel.count.observeAsState(defaultCount)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(count.toString(), modifier = Modifier.padding(10.dp))
Button(onClick = {
val counts = count + 2
viewModel.onCountChanged(counts)
sp.edit {
putInt("DEFAULT_COUNT", counts) // 保存值到本地sp
}
}) {
Text("Add Count")
}
Button(onClick = {
sp.edit().clear().apply()
viewModel.onCountChanged(0)
}, modifier = Modifier.padding(10.dp)) {
Text("Clear Count")
}
}
复制代码
看下运行效果:
我们杀死程序进程后再次进入,显示的依然是上次退出的值6
好了,今天就学习到这里,后续会讲到和其他jetpack库的用法。