《Jetpack Compose系列学习》-23 Compose中使用ViewModel

2,741 阅读5分钟

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。看看效果:

image.png

点了两次后TextView的值修改成了4,这时如果旋转屏幕,由于重新走了Activity的生命周期方法,所以TextView的值又变成了0,如图:

image.png

这种结果并不是我们想要的,我们希望系统配置修改的时候不影响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的值了。我们旋转屏幕后发现该值并没有发生改变。

image.png

image.png

我们一起回顾下ViewModel的生命周期:

image.png

在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值发生改变的时候可组合项会发生重组来显示最新数据。看下效果:

image.png

image.png

横竖屏切换后数据也没有发生改变,符合预期。

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")
    }
}

看下运行效果:

image.png

我们杀死程序进程后再次进入,显示的依然是上次退出的值6

image.png

好了,今天就学习到这里,后续会讲到和其他jetpack库的用法。