Jetpack Compose中MVVM的实现及ViewModel和remember对比

前言

ViewModelremember 是 Jetpack Compose 中用于管理数据的两种不同机制。

它们有以下区别:

生命周期管理:

  • ViewModel: ViewModel 是一个用于存储和管理与界面相关的数据的类,其生命周期与其关联的 ViewModelStoreOwner 相关联(通常是 ActivityFragment)。

    这意味着 ViewModel 中的数据会在相关联的 ViewModelStoreOwner 存在时保持状态,直到它们的生命周期结束。

  • remember: remember 是一个用于存储短暂数据的 Compose 状态管理器。

    它的生命周期与调用它的组件相关联,通常是函数组件。当组件被重新创建时,remember 中存储的数据会丢失。

数据共享:

  • ViewModel: ViewModel 通常用于存储与界面相关的持久性数据,它可以在多个组件之间共享,比如在同一个 Activity 中的不同 Fragment 之间共享数据。
  • remember: remember 主要用于存储临时性的局部状态,例如 UI 状态、临时缓存等,它的作用范围通常限制在调用它的组件内部。

数据持久性:

  • ViewModel: ViewModel 中的数据通常具有较长的生命周期,并且在配置更改(如屏幕旋转)时会被保留。
  • remember: remember 中的数据通常是临时性的,不会在配置更改后保留。

用法:

  • ViewModel: 通常通过在 ActivityFragment 中使用 ViewModelProvider 获取 ViewModel 实例,并在需要时观察 ViewModel 中的 LiveData 或使用它提供的数据。
  • remember: remember 可以直接在 Compose 组件内部使用,通过调用 remember { }rememberSaveable { } 来创建和存储状态。

总的来说:

  • ViewModel 适合用于管理持久性数据和在不同组件之间共享数据。
  • remember 则适合用于管理短暂的 UI 状态和局部状态。

remember/rememberSaveable

在Compose中,rememberrememberSaveable都是用于保存可组合函数的状态的方法,但它们在如何保存状态以及在什么情况下会重新计算状态上有所不同。

remember: 这个函数在组合函数的生命周期内始终保持相同的状态。这意味着,每次组合函数重新调用时,它都会使用先前保存的状态值,而不会重新计算它。这对于静态数据或者不会因用户交互而改变的数据很有用。如果状态的改变不需要在组件生命周期之外持久化,remember是一个更轻量级的选择。

1
var password by remember { mutableStateOf("") }

rememberSaveable: 这个函数也会保存状态,但它还会将状态持久化,以便在应用程序进入后台或被销毁后,能够恢复该状态。这对于需要跨配置更改(例如旋转屏幕)或者应用程序生命周期的状态非常有用。它会将状态保存在Bundle中,以确保状态的持久化。

1
var password by rememberSaveable { mutableStateOf("") }

因此,rememberSaveable提供了对状态的持久化支持,而remember则仅在组件生命周期内保存状态。

选择使用哪种取决于您需要的状态是否需要在应用程序重新启动后保持不变。

mutableStateOf/mutableStateListOf

mutableStateOf 是 Jetpack Compose 中的一个函数,用于创建可变的状态。

它的作用是创建一个可以被修改的状态,并且当状态发生改变时,Compose 会重新计算并更新相关的 UI。

具体来说,mutableStateOf 函数接受一个初始值作为参数,并返回一个包含该初始值的 MutableState 对象。

MutableState 对象具有 value 属性,可以读取和修改该状态的值。

MutableState 对象的值发生改变时,Compose 会根据新的状态重新计算 UI,以确保 UI 反映最新的状态。

例如,假设我们有一个 mutableStateOf 对象来表示一个计数器的值:

1
val countState = remember { mutableStateOf(0) }

然后我们可以通过修改 countState.value 的值来更新计数器的状态:

1
countState.value += 1

每当 countState.value 的值发生改变时,与该状态相关联的 UI 将会重新计算并更新,从而反映最新的计数器值。

总的来说:

mutableStateOf 的作用是在 Jetpack Compose 中创建可变的状态,以便动态更新 UI,并确保 UI 反映最新的状态值。

ViewModel

ViewModel 类是一种业务逻辑或屏幕级状态容器。它用于将状态公开给界面,以及封装相关的业务逻辑。

它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。这意味着在 activity 之间导航时或进行配置更改后(例如旋转屏幕时),界面将无需重新提取数据。

添加类

1
2
3
class MyViewModel : ViewModel() {
val listItems by mutableStateOf(listOf("张三", "李四", "王五"))
}

这时候我们每次初始化的时候都会是一个新的对象

1
2
val mainViewModel:MyViewModel = MyViewModel()
Log.i("ZLog","对象的Code:"+mainViewModel.hashCode())

这样自定义组件时使用数据的时候复用的时候就不方便,能不能让我们的ViewModel的实例在一个类中是同一个实例呢?

是可以的。

添加引用

1
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")

这时候我们使用实例的时候这样写

1
2
val mainViewModel:MyViewModel = viewModel()
Log.i("ZLog","对象的Code:"+mainViewModel.hashCode())

注意 :

viewModel()方法会根据类型和所在的ViewModelStoreOwner自动生成和缓存实例。

viewModel 的实例是与 ViewModelStoreOwner(通常是一个 ActivityFragment)相关联的,这意味着 ViewModel 的生命周期跟它的 ViewModelStoreOwner 是相匹配的。

列表示例

ViewModel

1
2
3
4
5
6
7
class MyViewModel : ViewModel() {
val listItems = mutableStateListOf<String>()
fun loadMore(){
Log.i("ZLog","添加数据")
listItems.add((Math.random()*100).toString())
}
}

数据初始化也可以

1
val listItems = mutableStateListOf("张三","李四","王五")

组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun MyList(mList: List<String>) {
SideEffect {
Log.i("ZLog","SideEffect")
}
LazyColumn {
items(mList.size) {
ListItem(mList[it])
}
}
}

@Composable
fun ListItem(text: String) {
// 构建列表项的 UI
Text(text = text)
}

数据更新的时候SideEffect并没有触发。

SideEffect 中的回调函数只在 MyList 组件第一次创建时被调用,因为 SideEffect 会在组件创建时运行其代码块,并在每次组件重新组合时运行。

但是在这种情况下,MyList 组件在 mList 改变时并不会重新组合,因为 Compose 无法检测到列表数据的更改。

要使 SideEffect 在列表数据改变时被调用,可以考虑将列表数据作为 key 参数传递给 MyList 组件,这样当列表数据改变时,MyList 组件将会重新创建,触发 SideEffect 的回调函数。

例如:

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun MyList(mList: List<String>, key: Any = mList) {
SideEffect {
Log.i("ZLog","SideEffect")
}
LazyColumn {
items(mList.size) {
ListItem(mList[it])
}
}
}

在这个修改后的 MyList 中,我们使用了 key 参数将列表数据传递给组件。这样,当列表数据发生变化时,key 值也会变化,从而触发 MyList 的重新组合,使 SideEffect 得以再次执行。

使用

1
2
3
4
5
6
7
8
9
Box() {
val mainViewModel: MyViewModel = viewModel()
MyList(mainViewModel.listItems)
Button(modifier=Modifier.align(Alignment.BottomEnd).padding(10.dp),onClick = {
mainViewModel.loadMore()
}) {
Text("加载数据")
}
}

注意

ViewModel中的数据必须在UI线程中更新才会出发页面刷新。

常见错误

接口请求

Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied

更新数据在LaunchedEffect

1
2
3
4
val appViewModel: AppViewModel = viewModel()
LaunchedEffect(Unit){
appViewModel.loadInit()
}