案例:共享 ViewModel
如果两个页面都在同一个模块中,你也可以使用共享的 ViewModel 在页面间传递数据。具体步骤如下:
- 在共享的 ViewModel 中定义一个可变状态来保存 Lesson 对象。
class LessonViewModel : ViewModel() {
var currentLesson by mutableStateOf<Lesson?>(null)
}
- 在发送页面中,将 Lesson 对象存入 ViewModel。
val lessonViewModel: LessonViewModel = viewModel()
// 在点击时
lessonViewModel.currentLesson = lesson
navController.navigate("tutor_add_lesson")
- 在接收页面中,从 ViewModel 中获取 Lesson 对象。
fun AddLessonScreen(lessonViewModel: LessonViewModel = viewModel(), navController: NavHostController) {
val lesson = lessonViewModel.currentLesson
// 使用 lesson 对象
}
这种方式无需序列化、编码,数据在内存中共享,但要求两个页面使用同一个 ViewModel 作用域。
注:但是我们发现上面的代码其实不起作用,因为把ViewModel用在了不同页面,无法共享(也可能是因为没有使用 stateFlow)
注意:ViewModel 通常是与单个页面(Activity/Fragment 或对应的组合项)绑定的,默认只在一个页面中可用。
默认情况下,如果你在不同页面(或不同的 NavBackStackEntry)中分别使用 viewModel()
,它们会创建各自独立的 ViewModel 实例,从而无法共享状态。但如果你希望多个页面或 Compose 组件共享同一个 ViewModel,就需要确保它们处于同一个作用域。
关键点说明
默认行为 当你在每个页面中直接调用
viewModel()
时,系统会以当前页面(或当前 NavBackStackEntry)为作用域创建 ViewModel。这样,每个页面都会有一个独立的实例。共享同一个 ViewModel 要实现共享,可以指定相同的
ViewModelStoreOwner
。例如,在使用 Jetpack Navigation 时,可以通过如下方式确保 SenderScreen 和 ReceiverScreen 使用同一个 ViewModel 实例:kotlin// 在共享父级路由 "main" 下获取同一个 NavBackStackEntry val parentEntry = remember { navController.getBackStackEntry("main") } val sharedViewModel: SharedViewModel = viewModel(parentEntry)
这样,无论你在哪个页面中调用
viewModel(parentEntry)
,都会返回同一个实例,进而实现状态共享。
总结
- 默认情况下,ViewModel 的作用域是与单个页面绑定的,每个页面各自拥有独立的 ViewModel 实例。
- 如果需要多个页面共享状态,就必须明确指定相同的 ViewModelStoreOwner,比如通过相同的 Activity、Fragment 或 NavGraph 的父级 NavBackStackEntry。
共享的解决方案:
当需要在不同页面间共享 ViewModel 时,我们可以利用 NavBackStackEntry
来确定一个共同的 ViewModel 存储作用域。例如,如果两个页面都属于同一个父级路由,可以通过获取父级的 NavBackStackEntry
,确保它们获得相同的 ViewModel 实例:
val parentEntry = remember { navController.getBackStackEntry("main") }
val lessonViewModel: LessonViewModel = viewModel(parentEntry)
这里,通过传入相同的 NavBackStackEntry
,确保在 "main" 路由下的所有页面共享同一个 ViewModel。
ViewModel 的理解
我的个人理解:ViewModel其实可以说是一个单独管理 state 的地方,并且这个 state 可以多个组件去复用,相当于只写一次 state 即可!
在 Jetpack Compose 中,ViewModel 主要用于管理和保存与 UI 相关的数据,并且以一种生命周期感知(lifecycle-aware)的方式运作。具体来说,它解决了以下几个问题:
数据持久性和状态管理 ViewModel 能够在屏幕旋转、主题切换或其他配置更改时保持数据不丢失。这是因为它与 Activity 或 Fragment 的生命周期不同步,而是存活于整个 UI 组件的生命周期中,直到 UI 组件完全销毁为止。
详细解释:
当设备发生配置变化(如屏幕旋转、主题切换、语言变化等)时,系统通常会销毁当前的 Activity 或 Fragment,然后重新创建一个新的实例来适应新的配置。这种机制虽然保证了 UI 能够正确显示新的配置,但也会导致以下问题:
- 数据丢失风险 在重新创建过程中,这些数据会因为对象被销毁而丢失,迫使开发者需要手动保存和恢复数据。
- 重复网络请求或计算 如果数据没有得到妥善保存,可能会重复触发数据请求或复杂计算,这不仅影响用户体验,也浪费资源。
为了解决这些问题,ViewModel 提供了以下机制:
- 生命周期感知的设计
- 与配置变化无关ViewModel 的创建是通过
ViewModelProvider
进行的。当发生配置变化时(例如屏幕旋转),系统虽然会销毁旧的 Activity 或 Fragment,但在重新创建时,ViewModelProvider 会返回同一个 ViewModel 实例(如果在同一个作用域下),而不是重新创建新的 ViewModel。 这意味着,即使 UI 组件重建,存储在 ViewModel 中的数据依然保留,从而避免了数据丢失。 - 作用域管理 ViewModel 的生命周期绑定的是 UI 组件的作用域,但它与 Activity 或 Fragment 本身的生命周期不完全同步。具体来说,ViewModel 会一直存在,直到与之关联的 Activity 或 Fragment 完全销毁,不再需要时才会清理。这种机制确保了在配置变化期间数据能够被保留。
- 配置变化时的数据保留
- 节省开发者工作量开发者不再需要手动保存和恢复数据(例如在
onSaveInstanceState()
中存储数据),大大简化了代码复杂性和维护难度。ViewModel 自身处理了这一切,使得代码更加简洁和清晰。
- 实际应用场景举例
- 用户输入表单 当用户正在填写一个表单时,如果发生屏幕旋转,传统的 Activity 可能会因重建而使得表单中未提交的数据丢失。但如果使用 ViewModel 来存储用户输入的数据,旋转后新的 Activity 可以从 ViewModel 中获取之前保存的数据,恢复到用户操作前的状态。
- 数据加载与缓存 例如在一个列表页面中,数据从网络请求获得后保存在 ViewModel 中。如果屏幕旋转,虽然 Activity 重建,但列表数据依然存在于 ViewModel 中,从而避免了重复请求,提高了用户体验和应用效率。
分离关注点(Separation of Concerns) 通过将业务逻辑和 UI 数据管理从 UI 组件中分离出来,ViewModel 帮助开发者实现更清晰的架构。这使得 UI 层只关注界面的展示,而所有的数据操作、逻辑处理都由 ViewModel 来处理,从而提高了代码的可维护性和可测试性。
——> 所以可以用数据和 UI 分离的方式来写代码
简化状态同步 在 Jetpack Compose 中,通过使用 LiveData、StateFlow 或者 Compose 自带的 State,可以让 UI 自动响应数据变化。当 ViewModel 中的数据发生改变时,Compose 会自动重新组合(recompose)受影响的部分,更新界面而无需手动干预。——> 前提是在同一个作用域中使用这个ViewModel
概念明确:
Activity:应用中一个完整的屏幕,负责管理整个界面的生命周期。
Fragment:Activity 内的子模块,专注于部分 UI 展示和逻辑处理,方便构建多屏或动态 UI。
Compose 组件:Jetpack Compose 中的基本 UI 构建块,通过声明式编程方式构建界面,可以被嵌入到 Activity 或 Fragment 中。
UI 和数据分离的写法
下面给出一个简单的示例,展示如何将业务逻辑放在 ViewModel 中,而 UI 层只关注界面展示,两者完全分离。
1. 定义 ViewModel
在这个 ViewModel 中,我们使用 mutableStateOf
来存储状态,同时提供业务逻辑方法(如 increment()
):
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
// 使用 mutableStateOf 来存储状态
var count by mutableStateOf(0)
private set
// 业务逻辑:增加 count
fun increment() {
count++
}
}
2. 编写 UI 层
在 UI 层,我们通过 viewModel()
函数获取 CounterViewModel 的实例,然后直接引用 ViewModel 中的状态,并调用其业务逻辑方法。这样 UI 只负责展示数据,所有逻辑操作都在 ViewModel 内处理。
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
Column {
// 显示当前计数值
Text(text = "Count: ${viewModel.count}")
// 点击按钮时调用 ViewModel 中的逻辑,更新状态
Button(onClick = { viewModel.increment() }) {
Text(text = "Increment")
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewCounterScreen() {
CounterScreen()
}