Skip to content

案例:共享 ViewModel

如果两个页面都在同一个模块中,你也可以使用共享的 ViewModel 在页面间传递数据。具体步骤如下:

  1. 在共享的 ViewModel 中定义一个可变状态来保存 Lesson 对象。
kotlin
class LessonViewModel : ViewModel() {
    var currentLesson by mutableStateOf<Lesson?>(null)
}
  1. 在发送页面中,将 Lesson 对象存入 ViewModel。
kotlin
val lessonViewModel: LessonViewModel = viewModel()
// 在点击时
lessonViewModel.currentLesson = lesson
navController.navigate("tutor_add_lesson")
  1. 在接收页面中,从 ViewModel 中获取 Lesson 对象。
kotlin
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 实例:

kotlin
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)的方式运作。具体来说,它解决了以下几个问题:

  1. 数据持久性和状态管理 ViewModel 能够在屏幕旋转、主题切换或其他配置更改时保持数据不丢失。这是因为它与 Activity 或 Fragment 的生命周期不同步,而是存活于整个 UI 组件的生命周期中,直到 UI 组件完全销毁为止。

    详细解释:

    当设备发生配置变化(如屏幕旋转、主题切换、语言变化等)时,系统通常会销毁当前的 Activity 或 Fragment,然后重新创建一个新的实例来适应新的配置。这种机制虽然保证了 UI 能够正确显示新的配置,但也会导致以下问题:

    • 数据丢失风险 在重新创建过程中,这些数据会因为对象被销毁而丢失,迫使开发者需要手动保存和恢复数据。
    • 重复网络请求或计算 如果数据没有得到妥善保存,可能会重复触发数据请求或复杂计算,这不仅影响用户体验,也浪费资源。

    为了解决这些问题,ViewModel 提供了以下机制:

    1. 生命周期感知的设计
    • 与配置变化无关ViewModel 的创建是通过 ViewModelProvider 进行的。当发生配置变化时(例如屏幕旋转),系统虽然会销毁旧的 Activity 或 Fragment,但在重新创建时,ViewModelProvider 会返回同一个 ViewModel 实例(如果在同一个作用域下),而不是重新创建新的 ViewModel。 这意味着,即使 UI 组件重建,存储在 ViewModel 中的数据依然保留,从而避免了数据丢失。
    • 作用域管理 ViewModel 的生命周期绑定的是 UI 组件的作用域,但它与 Activity 或 Fragment 本身的生命周期不完全同步。具体来说,ViewModel 会一直存在,直到与之关联的 Activity 或 Fragment 完全销毁,不再需要时才会清理。这种机制确保了在配置变化期间数据能够被保留。
    1. 配置变化时的数据保留
    • 节省开发者工作量开发者不再需要手动保存和恢复数据(例如在 onSaveInstanceState() 中存储数据),大大简化了代码复杂性和维护难度。ViewModel 自身处理了这一切,使得代码更加简洁和清晰。
    1. 实际应用场景举例
    • 用户输入表单 当用户正在填写一个表单时,如果发生屏幕旋转,传统的 Activity 可能会因重建而使得表单中未提交的数据丢失。但如果使用 ViewModel 来存储用户输入的数据,旋转后新的 Activity 可以从 ViewModel 中获取之前保存的数据,恢复到用户操作前的状态。
    • 数据加载与缓存 例如在一个列表页面中,数据从网络请求获得后保存在 ViewModel 中。如果屏幕旋转,虽然 Activity 重建,但列表数据依然存在于 ViewModel 中,从而避免了重复请求,提高了用户体验和应用效率。
  2. 分离关注点(Separation of Concerns) 通过将业务逻辑和 UI 数据管理从 UI 组件中分离出来,ViewModel 帮助开发者实现更清晰的架构。这使得 UI 层只关注界面的展示,而所有的数据操作、逻辑处理都由 ViewModel 来处理,从而提高了代码的可维护性和可测试性。

    ——> 所以可以用数据和 UI 分离的方式来写代码

  3. 简化状态同步 在 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()):

kotlin
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 内处理。

kotlin
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()
}