All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.slack.circuit.retained.RememberRetained.kt Maven / Gradle / Ivy

There is a newer version: 0.25.0
Show newest version
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuit.retained

import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.remember

/**
 * Remember the value produced by [init].
 *
 * It behaves similarly to [remember], but the stored value will survive configuration changes, such
 * as a screen rotation.
 *
 * You can use it with a value stored inside [androidx.compose.runtime.mutableStateOf].
 *
 * This differs from `rememberSaveable` by not being tied to Android bundles or parcelable. You
 * should take care to ensure that the state computed by [init] does not capture anything that is
 * not safe to persist across reconfiguration, such as Navigators. The same caveats of
 * `rememberSaveable` also still apply (i.e. do not retain Android Contexts, Views, etc).
 *
 * However, it does not participate in saved instance state either, so care should be taken to
 * choose the right retention mechanism for your use case. Consider the below two examples.
 *
 * The first case will retain `state` across configuration changes but will _not_ survive process
 * death.
 *
 * ```kotlin
 * @Composable
 * override fun present(): CounterState {
 *   var state by rememberRetained { mutableStateOf(CounterState(0)) }
 *
 *   return CounterState(count) { event ->
 *     when (event) {
 *       is CounterEvent.Increment -> state = state.copy(count = state.count + 1)
 *       is CounterEvent.Decrement -> state = state.copy(count = state.count - 1)
 *     }
 *   }
 * }
 * ```
 *
 * This second case will retain `count` across configuration changes _and_ survive process death.
 * However, it only works with primitives or `Parcelable` state types.
 *
 * ```kotlin
 * @Composable
 * override fun present(): CounterState {
 *   var count by rememberSaveable { mutableStateOf(0) }
 *
 *   return CounterState(count) { event ->
 *     when (event) {
 *       is CounterEvent.Increment -> state = count++
 *       is CounterEvent.Decrement -> state = count--
 *     }
 *   }
 * }
 * ```
 *
 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
 *   reset and [init] to be rerun
 * @param key An optional key to be used as a key for the saved value. If not provided we use the
 *   automatically generated by the Compose runtime which is unique for the every exact code
 *   location in the composition tree
 * @param init A factory function to create the initial value of this state
 */
@Composable
public fun  rememberRetained(vararg inputs: Any?, key: String? = null, init: () -> T): T {
  val registry = LocalRetainedStateRegistry.current
  // Short-circuit no-ops
  if (registry === NoOpRetainedStateRegistry) {
    return when (inputs.size) {
      0 -> remember(init)
      1 -> remember(inputs[0], init)
      2 -> remember(inputs[0], inputs[1], init)
      3 -> remember(inputs[0], inputs[1], inputs[2], init)
      else -> remember(keys = inputs, init)
    }
  }

  val compositeKey = currentCompositeKeyHash
  // key is the one provided by the user or the one generated by the compose runtime
  val finalKey =
    if (!key.isNullOrEmpty()) {
      key
    } else {
      compositeKey.toString(MaxSupportedRadix)
    }

  val canRetainChecker = LocalCanRetainChecker.current ?: rememberCanRetainChecker()
  val holder =
    remember(canRetainChecker) {
      // value is restored using the registry or created via [init] lambda
      val restored = registry.consumeValue(finalKey)
      val finalValue = restored ?: init()
      RetainableHolder(registry, canRetainChecker, finalKey, finalValue, inputs)
    }
  val value = holder.getValueIfInputsAreEqual(inputs) ?: init()
  SideEffect { holder.update(registry, finalKey, value, inputs) }
  @Suppress("UNCHECKED_CAST") return value as T
}

/** The maximum radix available for conversion to and from strings. */
private const val MaxSupportedRadix = 36

private class RetainableHolder(
  private var registry: RetainedStateRegistry?,
  private var canRetainChecker: CanRetainChecker,
  private var key: String,
  private var value: T,
  private var inputs: Array
) : RetainedValueProvider, RememberObserver {
  private var entry: RetainedStateRegistry.Entry? = null

  fun update(registry: RetainedStateRegistry?, key: String, value: T, inputs: Array) {
    var entryIsOutdated = false
    if (this.registry !== registry) {
      this.registry = registry
      entryIsOutdated = true
    }
    if (this.key != key) {
      this.key = key
      entryIsOutdated = true
    }
    this.value = value
    this.inputs = inputs
    if (entry != null && entryIsOutdated) {
      entry?.unregister()
      entry = null
      register()
    }
  }

  private fun register() {
    val registry = registry
    require(entry == null) { "entry($entry) is not null" }
    if (registry != null) {
      entry = registry.registerValue(key, this)
    }
  }

  /** Value provider called by the registry. */
  override fun invoke(): Any = requireNotNull(value) { "Value should be initialized" }

  fun saveIfRetainable() {
    // If the value is a RetainedStateRegistry, we need to take care to retain it.
    // First we tell it to saveAll, to retain it's values. Then we need to tell the host
    // registry to retain the child registry.
    if (value is RetainedStateRegistry) {
      (value as RetainedStateRegistry).saveAll()
      registry?.saveValue(key)
    }

    if (registry != null && !canRetainChecker.canRetain(registry!!)) {
      entry?.unregister()
    }
  }

  override fun onRemembered() {
    register()
  }

  override fun onForgotten() {
    saveIfRetainable()
  }

  override fun onAbandoned() {
    saveIfRetainable()
  }

  fun getValueIfInputsAreEqual(inputs: Array): T? {
    return value.takeIf { inputs.contentEquals(this.inputs) }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy