Composable 函数作用域内的 ViewModel

421 阅读16分钟

有时候, 你会遇到一些问题, 一方面你觉得这些问题是可以解决的, 但另一方面你又会觉得, 你之所以会遇到这样的问题, 完全是因为你自己陷入了这样的境地. 话不多说, 让我们开始讨论将 Jetpack ViewModel 架构与我们的 Composable 进行关联的问题!

Android 吉祥物

以正常方式对 ViewModel 进行范围设置

尽管你可以在对ViewModel进行寻址, 但通常情况下, 你需要将ViewModel寻址到以下其中之一:

  1. Activity
  2. Fragment (即使有了 Compose, 也不应再使用 Fragment )
  3. 导航图谱
  4. 导航目的地

Jetpack Compose 中的 ViewModel 通常依赖于 LocalViewModelStoreOwner: 一旦你使用了第一个 Composable, 它就已经在那里了, 等着你去使用!

class MainActivity: ComponentActivity {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyComposable()
    }
  }

}

@Composable
fun MyComposable() {
  val viewModelStoreOwner = LocalViewModelStoreOwner.current
  // viewModelStoreOwner == MainActivity
  
  // We can use the following helper function to simply get a ViewModel
  // Assuming it has no dependencies:
  val viewModel = viewModel<MyViewModel>()
}

现在, 如果我们以最简单的方式注入 ViewModel, 我们可以看到在 viewModel(..) 辅助函数中, 我们为 ViewModelStoreOwner 参数设置了一个默认值: LocalViewModelStoreOwner:

viewModel 函数.

使用 NavGraph 进行嵌套

我喜欢 NavGraph, 其中一个原因是: 它对 ViewModels 有很好的支持! 图中的每个目的地都有自己的 ViewModel 存储, 因为每个 NavBackStackEntry 都是 ViewModelStoreOwner.

@Composable
fun MyComposable() {
  println(LocalViewModelStoreOwner?.current?.javaClass?.simpleName)
  // prints "MainActivity"
  
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = "home",
  ) {
    composable("home") { homeBackstackEntry ->
      println(LocalViewModelStoreOwner.current == homeBackstackEntry)
      // prints "true"
    }
    composable("other") { otherBackstackEntry ->
      println(LocalViewModelStoreOwner.current == otherBackstackEntry)
      // prints "true"
    }
  }
}

这也意味着, 如果你从一个目的地移动到另一个目的地, 导航图谱将能够保存你的 ViewModel, 并在必要时销毁你的 ViewModel. 你会看到, 如果你从home移动到other, 然后弹出导航堆栈, 那么home目的地中的任何 ViewModel 都会神奇地重新出现, 因为它已被保存. 但是, 只要你弹出other目的地上的堆栈, 那里的任何 ViewModel 都会被直接清除!

将 ViewModelScope 扩展到 Composable

我想要的是将 ViewModelScope 指向它所在的 Composable ! 我希望在 Composable 进入组合时创建 ViewModel, 而在 Composable 离开组合时清除 ViewModel.

众所周知, ViewModel 存储在 ViewModelStoreOwner 内的 ViewModelStore 中. 因此, 我们需要做的基本上就是在我们的 Composable 中以某种方式记住一个 ViewModelStore, 用它来生成一个 ViewModel, 然后我们就可以开始了! 我的要求是很简单的:

  1. 我想让 ViewModelScope 指向我的 Composable 函数. 当我的 Composable 成为组合的一部分时, 它应该被创建; 如果 Composable 离开组合, 它应该被清除.
  2. 我希望 ViewModel 能在方向改变后继续存在.
  3. 我希望能够传递一个 Key , 如果该 Key 发生变化, 我的 ViewModel 应使用新 Key 重新初始化.
@Composable
fun Example() {
  // If condition is true, the code block enters the composition
  // Ifcondition turns to false, the following code leaves the composition
  if (condition) {
    // Due to the ViewModelStoreOwner being some kind of root component
    // like an Activity or a NavGraph destination, the viewModel will
    // remain the same when we flip the condition multiple times.

    // I want this to be different: As soon as this piece of code leaves
    // the composition, it should clear any ViewModel it contains.
    // If it reappears, I want a fresh ViewModel again.
    val viewModel = viewModel<ExampleViewModel>()
    val text by viewModel.text.collectAsState()
    Text(text)
  }
}

使用案例

这是最难以解释的部分, 因为我需要说明为什么值得这样做, 以及其他工具如何才能发挥作用. 在一些用例中, 我们希望将组件嵌套到其他组件中.

  • 可重用的组件: 如果能创建 Composable 组件作为构建模块, 并在代码中使用这些 Composable 组件, 并在其中拥有自己的状态和 ViewModel, 那将会非常好. 这样可以避免出现 ViewModel 控制屏幕上过多内容的问题.
  • 底页: 在我目前工作的应用程序中, 我们的一个屏幕由多个BottomSheet支持, 如果按下按钮, BottomSheet就会弹出. 这些BottomSheet 可以帮助你更改某些数据, 它们位于一个 ModalBottomSheet 组件中. 这些独立的BottomSheet可以使用自己的 ViewModel 来封装内部逻辑, 当BottomSheet关闭或更改为其他BottomSheet 时, 最好能清除内部的 ViewModel.
  • 封装视图: 我们有一个显示计划表的屏幕. 计划表的顶部是一个日期选择器. 每当你选择一个日期(它提供明天/昨天), 屏幕就会根据你按下的按钮向左或向右滑动, 然后我们从后台加载数据并显示在新的屏幕上. 在主组件中封装一个处理日期选择的父 ViewModel 和一个处理该日期数据的子 ViewModel 是很好的.
  • 复杂的 ViewPager: 如上文所述, 如果你有一个 ViewModel 可以向 ViewPager 提供 ID 列表, 那么每个页面如果有自己的 ViewModel, 基本上都可以托管自己. 如果能在滚动到下一页后丢弃未使用的 ViewModel 就更好了.
  • 对话框: 在这里, 你或许可以使用 NavHost 来处理你的对话框, 但我将对话框放在了我的普通组件中, 而且我不想在取消对话框并重新打开后显示旧数据. 我还希望任何对话框都能自我维持: 如果它提供某些操作, 就应该有自己的 ViewModel 来处理业务.

第一次迭代

因此, 我们可以使用古老的 remember 函数来记忆一个值, 然后在 Key 值更改时清除旧值并创建新值!

@Composable
fun rememberViewModelStoreOwner(key: Any?): ViewModelStoreOwner() {
  val viewModelStoreOwner = remember(key) { 
    object: ViewModelStoreOwner {
      override val viewModelStore = ViewModelStore()
    }
  }
  DisposableEffect(viewModelStoreOwner) {
    onDispose {
      viewModelStoreOwner.clear()
    }
  }
  return viewModelStoreOwner
}

@Composable
fun WithViewModelStoreOwner(
  key: Any?,
  content: @Composable () -> Unit,
) {
  CompositionLocalProvider(
    value = LocalViewModelStoreOwner provides rememberViewModelStoreOwner(key),
    content = content,
  )
}

这就是你在这里看到的: 我们根据 Key 值记住 ViewModelStoreOwner. 然后使用 DisposableEffect, 每当 ViewModelStoreOwner 发生变化或完成可替代操作时, 我们都会清除它. 这样做是有效的. 但它做不到的是在方向发生变化时也能存活: 这是我们希望 ViewModel 做的主要事情! 此外, 如果你从屏幕上导航离开, 就会触发 onClear, 因此如果你弹回屏幕, ViewModel 也会消失.

近距离观察 NavHost

我想知道他们是如何从 Jetpack 导航库中做到这一点的? 他们是如何在从一个目的地导航到另一个目的地时保持状态的? 答案很简单: 他们使用... ViewModel! 没错! 他们在自己内部的 NavHost 的 ViewModel 内有一个 ViewModelStore. 因此, 基本上, 导航图谱中的任何 ViewModel 都存储在另一个 ViewModel 中, 而导航组件足够聪明, 可以跟踪所有这些 ViewModel, 正如你在以下内部代码 Fragment 中看到的那样:

NavHost 内部如何使用 ViewModelStore

你可以看到, 每个 backStackEntryId 都会有一个 ViewModelStore 链接到它, 在弹出或者进行导航时, 它可以被清除或创建, 这取决于你给它的导航命令!

更新我们的代码

了解了这一点, 我还可以利用 ViewModel 的强大功能来存储我的 ViewModel! 在这种情况下, 我会让我的 ViewModel 负责根据给定的 Key 返回正确的 ViewModelStoreOwner. 如果 Key 发生变化, 我们会在返回之前清除 ViewModelStore.

@Composable
fun rememberViewModelStoreOwner(
  key: Any?,
): ViewModelStoreOwner {
  val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel>() {
    ViewModelStoreOwnerViewModel(key)
  }
  viewModelStoreOwnerViewModel.updateKey(key)

  DisposableEffect(Unit) {
    onDispose {
      viewModelStoreOwnerViewModel.resetViewModelStore()
    }
  }
  return remember(viewModelStoreOwnerViewModel) { 
    viewModelStoreOwnerViewModel.viewModelStore
  }
}

@Composable
fun WithViewModelStoreOwner(
  key: Any?,
  content: @Composable () -> Unit,
) {
  CompositionLocalProvider(
    value = LocalViewModelStoreOwner provides rememberViewModelStoreOwner(key),
    content = content,
  )
}

private class ViewModelStoreOwnerViewModel(
  private var key: Any?,
) : ViewModel() {

  private var viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner {
    override val viewModelStore = ViewModelStore()
  }

  fun get(key: Any?): ViewModelStoreOwner {
    if (key != this.key) {
      this.key = key
      resetViewModelStore()
    }
    return viewModelStore
  }

  fun resetViewModelStore() {
    viewModelStore.viewModelStore.clear()
  }

  override fun onCleared() {
    resetViewModelStore()
  }

}

然而, 我们仍然没有做到这一点. 我们仍然依赖 DisposableEffect 来清除 ViewModel, 而 onDispose { } 肯定会在配置更改时被调用! 此外, 如果我多次使用 rememberViewModelStoreOwner, 就会得到相同的实例. 这可能意味着, 如果 Composable A 被处置, 我们可能会丢失 Composable B 中的 ViewModel :( . 此外, 如果我们来回浏览, 由于 onDispose 的原因, 我们会丢失数据.

每个实例的唯一 Key

让我们先解决唯一性问题: 我们希望为每个 Composable 函数创建一个 ViewModelStoreOwner. 幸运的是, Compose 会通过特定的哈希值: currentCompositeKeyHash 来跟踪 Composable 的位置. 实际上, 它在 rememberSaveable 的内部实现中使用了这个哈希值. 当你调用 rememberSaveable 时, Composable 程序的当前哈希值将被用作保存状态时存储数据的 Key . 如果你重新创建一个 Composable 程序, 它会生成相同的哈希值, 然后你就可以根据哈希值恢复任何值. 由于我们的 Composable元素将依靠这个字段获得相当稳定的 Key , 因此我们可以为每个 Key 创建一个唯一的 ViewModelStoreOwnerViewModel:

@Composable
fun rememberViewModelStoreOwner(
  key: Any?,
): ViewModelStoreOwner {
  val viewModelKey = "rememberViewModelStoreOwner#" + currentCompositeKeyHash.toString(36)
  val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel>(
    key = viewModelKey,
  ) {
    ViewModelStoreOwnerViewModel(key)
  }
  viewModelStoreOwnerViewModel.updateKey(key)

  DisposableEffect(Unit) {
    onDispose {
      viewModelStoreOwnerViewModel.resetViewModelStore()
    }
  }
  return remember(viewModelStoreOwnerViewModel) { 
    viewModelStoreOwnerViewModel.viewModelStore
  }
}

@Composable
fun Example() {
  // All these will have their own unique ViewModelStoreOwner
  val viewModelStoreOwner1 = rememberViewModelStoreOwner(Unit)
  val viewModelStoreOwner2 = rememberViewModelStoreOwner(Unit)
  if (condition) {
    // This means that whenever condition turns into false, only
    // viewModelStoreOwner3 will be cleared
    val viewModelStoreOwner3 = rememberViewModelStoreOwner(Unit)
  }

天哪! 问题真多啊!

现在, 让我们来解决配置更改后的生存问题. 当配置发生变化时, Activity 将重新创建自己. 在此之前, 它会触发 onSaveInstanceState. 这反过来又会触发其所有子 Fragment 、视图和组件中的保存方法! 我们对此非常感兴趣, 因为如果我们能检测到正在保存的更改, 我们就会知道, 我们要么会被...:

  • 导航离开, 但可以选择稍后恢复状态
  • 应用程序在后台被系统销毁, 以节省资源
  • 由于配置更改(如旋转)而重新创建应用程序

我开始想写一个完整的解决方案, 使用LocalSavedStateRegisterOwner来检测我们保存状态的时刻, 但我发现这并不可靠. 问题在于, 保存状态并非在所有情况下都会在我们导航离开时触发, 而且当应用程序仅仅处于后台时也会触发. 我们需要更可靠的方法!

Lifecycle 拯救我们(一如既往...)

因此, 在浪费了大量代码和时间之后, 我想到了以下条件, 根据 Composable 模型所处的 Lifecycle 来保留或销毁 ViewModel. Lifecycle 很有意思, 它可以是 Activity 的 Lifecycle , 也可以是导航后栈入口的 Lifecycle . 有趣的是, 如果你从 NavHost 内的当前目的地导航离开, BackStackEntry 不会被销毁. 它将返回到创建状态. 只有当处理 BackStackEntry 时, Lifecycle 才会变为 Destroyed.

  1. 如果我们的 Composable 组件被移除, 而我们又处于恢复状态, 我们就会知道持有 Lifecycle 的组件仍然存活, 只是我们的 Composable 组件被移除了. 因此在这种情况下, 我们要清除 ViewModelStoreOwner. 下一次再次添加 Composable 组件时, 它将获得一个新的 ViewModel!
  2. 如果我们的 Lifecycle 没有恢复, Composable 被移除, 我们将保留数据. 这可能是我们导航到了另一个位置(因此我们当前的位置将被停止, 但不会被销毁), 也可能是我们的配置发生了变化, Lifecycle 将被重新创建. 如果我们的导航目的地实际上已被销毁, 其内部的 ViewModelStore 将被清除, 我们自己的 ViewModelStoreOwnerViewModel 也将像变魔术一样被清除.
  3. 如果我们的 Composable 元素被移除, 而我们的 Lifecycle 到达 RESUMED, 并且在此期间我们的 Composable 元素没有被重新创建, 这意味着我们的 Composable 元素实际上是在重新创建过程中或应用程序处于后台时被移除的. 在这种情况下, 我们会在恢复时清除 ViewModel.

保持正确的状态

我们应该在 ViewModel 中存储最后已知的 Lifecycle , 以及我们是否处于连接状态. 现在, 我们的 ViewModel 将决定是否清除 ViewModelStore. 因此, 我们的 rememberViewModelStoreOwner 变成了一个更简单的实现:

@Composable
fun rememberViewModelStoreOwner(
  key: Any?,
): ViewModelStoreOwner {
  val viewModelKey = "rememberViewModelStoreOwner#" + currentCompositeKeyHash.toString(36)
  val localLifecycle = LocalLifecycleOwner.current.lifecycle
  val viewModelStoreOwnerViewModel = viewModel<ViewModelStoreOwnerViewModel>(key = viewModelKey) {
    ViewModelStoreOwnerViewModel(key, localLifecycle)
  }
  viewModelStoreOwnerViewModel.update(key, localLifecycle)

  DisposableEffect(Unit) {
    viewModelStoreOwnerViewModel.attachComposable()
    onDispose {
      viewModelStoreOwnerViewModel.detachComposable()
    }
  }
  return remember(viewModelStoreOwnerViewModel) {
    viewModelStoreOwnerViewModel.viewModelStore
  }
}

你可以看到, 我们向 ViewModel 传递了三样东西:

  1. 我们决定返回新 ViewModelStoreOwner 的 Key
  2. Composable 当前所处的 Lifecycle (这可能会比 Composable 的 Lifecycle 更长) !.
  3. 通过检查 Composable 元素的处置时间, 确定 Composable 元素是否被附加.

更新 ViewModel

现在, 我们只需在 ViewModel 中执行前面所述的 3 条规则. 因此, 我们在这里清除了这 3 种情况下存储的 ViewModel :

  1. 我们的 Key 改变了
  2. 我们脱离了, 而且 Lifecycle 状态变更为 RESUMED
  3. 我们稍后进入恢复状态, 但不再处于连接状态

如果我们被附加或我们的 Lifecycle 被破坏(可能发生在配置更改时的重现过程中), 那么我们什么也不做. 因此, 我们可以简单地组合一些状态, 并用 ViewModelScope 来收集这些状态.

@Stable
private class ViewModelStoreOwnerViewModel(
  private var key: Any?,
  initialLifecycle: Lifecycle,
) : ViewModel() {

  private val attachedLifecycle = MutableStateFlow<Lifecycle?>(initialLifecycle)
  private val isAttachedToComposable = MutableSharedFlow<Boolean>()

  val viewModelStore: ViewModelStoreOwner = object : ViewModelStoreOwner {
    override val viewModelStore = ViewModelStore()
  }

  init {
    viewModelScope.launch {
      attachedLifecycle
        .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() }
        .collectLatest {
          // Make sure we release the lifecycle once it is destroyed
          if (it == Lifecycle.Event.ON_DESTROY) {
            attachedLifecycle.update { null }
          }
        }
    }
    viewModelScope.launch {
      isAttachedToComposable.collectLatest { isAttached ->
        when {
          // If we are attached or we are destroyed, we do not need to do anything
          isAttached || attachedLifecycle.value == null -> return@collectLatest
          // If we are detached and the lifecycle state is resumed, we should reset the view model store
          attachedLifecycle.value?.currentState == Lifecycle.State.RESUMED -> resetViewModelStore()
          else -> {
            // We wait for the lifecycle event ON_RESUME to be triggered before resetting the ViewModelStore
            // If in the mean time we are attached again, this work is cancelled
            attachedLifecycle
              .flatMapLatest { lifecycle -> lifecycle?.eventFlow ?: emptyFlow() }
              // Wait for first event that matches ON_RESUME.
              .firstOrNull { it == Lifecycle.Event.ON_RESUME } ?: return@collectLatest
            resetViewModelStore()
          }
        }
      }
    }
  }

  fun update(key: Any?, lifecycle: Lifecycle) {
    if (key != this.key) {
      this.key = key
      resetViewModelStore()
    }
    attachedLifecycle.update { lifecycle }
  }

  fun attachComposable() {
    viewModelScope.launch { isAttachedToComposable.emit(true) }
  }

  fun detachComposable() {
    viewModelScope.launch { isAttachedToComposable.emit(false) }
  }

  override fun onCleared() {
    super.onCleared()
    resetViewModelStore()
  }

  private fun resetViewModelStore() {
    viewModelStore.viewModelStore.clear()
  }

}

这样做似乎非常有效! 不过, 如果我们有大量的 Composable元素来来去去, 那么最好就不要生成数百万个 ViewModelStoreOwnerViewModels, 每个都有唯一的 Key . 我们可以将其全部封装在一个且仅有一个 ViewModel 实例中, 并跟踪其中不同哈希 Key 的不同 ViewModelStore, 将其存储在一个映射中. 这样一来, 只要 Composable 离开屏幕, 我们就可以清空 ViewModelStore 并从内存中删除所有内容.

要点

使用映射来存储 ViewModelStoreOwner 和 Composable 的完整实现可以在这里中找到.

示例时间到了!

现在是展示成果的时候了. 在下面的 ContrivedExample 中, 你可以看到我们可以通过哪些方式使用 ViewModelStoreOwner:

@Composable
fun ContrivedExample(
  state: State,
  someCondition: Boolean,
  dialogState: SomeDialogState?,
) {
  when(state) {
    State.Something -> {
      WithViewModelStoreOwner(state.originalData) {
      // Cleared when state turns into State.Something, 
      // Recreated when originalData is changing
      val viewModel = viewModel {
        SomethingViewModel(initialValue = state.originalData)
      }
        SomethingComponent(state.originalData)
      }
    }
    State.SomethingElse -> {
      WithViewModelStoreOwner(state.otherData) {
        // Cleared when state turns into State.Something, 
        // recreated when otherData is changing
        val viewModel = viewModel {
          SomethingElseViewModel(initialValue = state.otherData)
        }
        SomethingElseComponent()
      }
    }
  }
  if (someCondition) {
    WithViewModelStoreOwner(Unit) {
      // If someCondition turns false, this viewModel is cleared magically
      val viewModel = viewModel<SpecialViewModel>()
      ComposableShownWithSomeCondition(viewModel = viewModel)
    }
  }
  dialogState?.let {
    WithViewModelStoreOwner(it) {
      // If our dialog state changes, view model will reinitialize
      // If our dialog state turns to null, view model is cleared
      val viewModel = viewModel<SpecialViewModel>()
      ComposableShownWithSomeCondition(viewModel = viewModel)
    }
  }
}

好了, 这些都是很棒的示例! 但现在是时候展示一些真正的动态图片了. 我就不多说 ViewModel 的实现了, 下面的内容应该能让你大致了解我们现在如何构建屏幕:

  • 我们有一个名为 MyScreen 的容器.
  • 顶部是一个日期选择器, 它有两个图标按钮:<>, 用于将日期更改为前一天或后一天.
  • 屏幕的其余部分是一个组件, 根据所选日期显示工作日. 它假装从某个地方加载这个工作日, 并且它有一个针对所选日期的 ViewModel.
@Composable
fun MyScreen() {
  val viewModel = viewModel<MyViewModel>()
  val selectedDate by viewModel.selectedDate.collectAsState()
  Column {
    DateSelector(
      selectedDate = selectedDate,
      onDateChanged = viewModel::onDateChanged
    )

    HorizontalDivider()

    AnimatedContent(
      targetState = selectedDate,
      transitionSpec = TransitionSpec,
    ) { date ->
      WithViewModelStoreOwner(date) {
        val viewModel = viewModel() {
          WeekDayForDateViewModel(date)
        }
        WeekDayForDate(viewModel)
      }
      // Also possible:
      // val viewModel = viewModel(rememberViewModelStoreOwner(date) {
      //   WeekDayForDateViewModel(date)
      // }
      // WeekDayForDate(viewModel)
    }
  }
}

/**
  * Note that this component is not aware of the special ViewModel handling.
  * This is because we simply wrap it in WithViewModelStoreOwner {}.
  * It is always good for children components to not be aware of where
  * they are positioned and how to manage their scoping.
  */
@Composable
fun WeekDayForDate(viewModel: WeekDayForDateViewModel) {
  val viewState by viewModel.viewState
  LoadingResultScreen(
    onRefresh = viewModel::refresh,
    loadingResult = viewState,
  ) { state, _ ->
    Text(state.weekDay)
  }
}

private val TransitionSpec: AnimatedContentTransitionScope<LocalDate>.() -> ContentTransform = {
  val direction = initialState.compareTo(targetState)
  (fadeIn() + slideIn(initialOffset = { IntOffset(it.width * direction, 0) })) togetherWith 
    (fadeOut() + slideOut(targetOffset = { IntOffset(-it.width * direction, 0) }))
}

想法很简单: 如果你选择了一个日期, 我们就会使用动画滑动到相应的日期屏幕, 然后从其他地方“加载”一些数据并显示出来. 动画内容中的日期屏幕有自己的 ViewModel, 一旦 Composable 滑出视图, 这些 ViewModel 就会被清除. 此外, 我还加入了一些随机化功能, 以显示屏幕可以进行适当的错误处理. 所有的错误处理等都由内部的 ViewModel 处理. 下面是拍摄的 GIF 效果:

日期选择器在上, 内容在下

我可以向你保证, 每次切换日期时, 旧屏幕都会滑出, 其 ViewModel 也会被清除. 你相信我, 对吗? 为了让你相信我没有说谎: 每次 ViewModel 被清除时, 下面的 GIF 都会显示一条小吃店信息! (是的, 这些都是由 ViewModel 的 onCleared 方法触发的, 而不是伪造的).

处理 ViewModel 时显示消息

那么配置变化又是怎么回事呢? 在这里你会看到, 当我们旋转时, 原来的 Composable 将保持其状态, 而不会再次变成一个加载旋转器.

旋转时保持状态

最后但并非最不重要的一点: 如果我们将所有内容都转储到 NavHost 中, 那么即使旋转屏幕, 我们也可以导航离开, 导航回来并保持状态!

我们可以前后导航、旋转并保持正确的实例

结论

事实上还不错

我发现很难检测 Composable 是否真的因为重新创建而离开组合, 或者仅仅因为不再需要它. 通过使用 ViewModel 和 Lifecycle 的强大功能, 我很高兴找到了一种非笨拙的方法来达到预期效果. 重要的是要认识到一个限制条件, 即停止 Lifecycle 和分离 Composable 并不意味着我们应该删除数据. 我们只能通过检查其 Lifecycle 是否处于 Resumed 状态, 并检查我们是否处于连接状态, 来确定我们是否可以清除任何 Composable元素. 这是我唯一感到不安的地方, 因为同步依赖于 Composable 达到 Resumed 状态, 而这可能需要一段时间. 不过, 要想更精确地解决这个问题, 还需要以某种方式与父 Composable 函数挂钩, 以保持更紧密的控制, 而这就失去了简单地将此解决方案投放到位的意义. 这个解决方案可以放在任何地方, 比如 ViewPager 中, 甚至是 LazyColumn 中, 每个项目在从 View 中滚动出来时都会清空它的 ViewModel.

生产中的使用

实际上, 我会在生产中使用这段代码. 原因很简单: 我已经有了一个糟糕的解决方案(参见本文中提到的 “第一次迭代”:), 它不可能变得更糟. 我已经有可能在更改配置时丢失 ViewModel 状态, 所以这是一种改进. 此外, 最终一切都将与已经存在的 LocalViewModelStoreOwner 绑定. 因此, 只要清除了这个存储空间, 它就会清理我的实现所留下的任何混乱. 如果某些未被发现的边缘情况决定在以后出现并导致问题, 我完全可以承担任何责任.

最后的话

写这篇文章是一次相当漫长的旅程. 你能做到, 并不意味着你应该做到. 我从多个方向入手, 最终找到了一个不需要以奇怪或笨拙的方式与任何框架挂钩的解决方案. 它建立在我对 Android Lifecycle 内部工作原理的理解之上, 并设法加以利用.

今天的内容就分享到这里啦!

一家之言, 欢迎斧正!

Happy Coding! Stay GOLDEN!