0

I have a composable with with a remembered value called imageWidth

var imageWidth = remember { 0f }

I want to calculated the image width once (unless rotation has changed). This is the only place that writes to that variable.

LaunchedEffect(key1 = currentRotation) {
    val ratio = imageBitmap.getRatio(currentRotation)
    imageWidth = //some calculation
}

The imageWidth is accessed by multiple places in the app. Using some log prints, I have made sure that this side effect is being called and correct value is set to imageWidth for the first time I enter the screen.

For some reason, by clicking on some button, the value is being reset to 0.

What are the cases that a remembered value can be forgotten/reset? If the reason is the disposal of the composable, why isn't the LaunchedEffect called again and calculate the value when entering the composition?

TareK Khoury
  • 12,721
  • 16
  • 55
  • 78
  • 2
    Per [documentation][1]: > Note: remember stores objects in the Composition, and forgets the object when the composable that called remember is removed from the Composition. You might want to try lifting the state to a composable higher up the tree. Also, I think you might be using the syntax for immutable values, but I'm not sure. : var imageWidth = remember { mutableStateOf(0f) } [1]: https://developer.android.com/jetpack/compose/state – eimmer Jun 20 '22 at 19:12

1 Answers1

0

With your current set up what supposed to happen is imageWidth to be set to 0f after each recomposition because remember is run on composition or any of its key are changed.

/**
 * Remember the value returned by [calculation] if all values of [keys] are equal to the previous
 * composition, otherwise produce and remember a new value by calling [calculation].
 */
@Composable
inline fun <T> remember(
    vararg keys: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    var invalid = false
    for (key in keys) invalid = invalid or currentComposer.changed(key)
    return currentComposer.cache(invalid, calculation)
}

In LaunchedEffect you set value but when another recomposition happens it's reset to value in remember block.

However something i might have missed here is by rotation if you mean rotating device and recreating Activity, the answer below doesn't work, you can move your value to ViewModel to store latest value or rememberSavable.

If your Activity is not recreated but you want to recalculate block inside remember add keys to check if there has to be new calculation.

You need to add currentRotation as key for it to be set only when rotation changes

var imageWidth = remember(currentRotation) {
     val ratio = imageBitmap.getRatio(currentRotation)
     //some calculation result as float 
} 

remember with keys is commonly used in default Composable source codes, but i wonder why it' not mentioned in official documents.

Slider for instance use it as

@Composable
fun Slider(
    value: Float,
    onValueChange: (Float) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    /*@IntRange(from = 0)*/
    steps: Int = 0,
    onValueChangeFinished: (() -> Unit)? = null,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    colors: SliderColors = SliderDefaults.colors()
) {
    require(steps >= 0) { "steps should be >= 0" }
    val onValueChangeState = rememberUpdatedState(onValueChange)
    val tickFractions = remember(steps) {
        stepsToTickFractions(steps)
    }

   // rest of the code
}

And in painterResource code

@Composable
@ComposableOpenTarget(-1)
fun rememberVectorPainter(
    defaultWidth: Dp,
    defaultHeight: Dp,
    viewportWidth: Float = Float.NaN,
    viewportHeight: Float = Float.NaN,
    name: String = RootGroupName,
    tintColor: Color = Color.Unspecified,
    tintBlendMode: BlendMode = BlendMode.SrcIn,
    autoMirror: Boolean = false,
    content: @Composable @VectorComposable (viewportWidth: Float, viewportHeight: Float) -> Unit
): VectorPainter {
    val density = LocalDensity.current
    val widthPx = with(density) { defaultWidth.toPx() }
    val heightPx = with(density) { defaultHeight.toPx() }

    val vpWidth = if (viewportWidth.isNaN()) widthPx else viewportWidth
    val vpHeight = if (viewportHeight.isNaN()) heightPx else viewportHeight

    val intrinsicColorFilter = remember(tintColor, tintBlendMode) {
        if (tintColor != Color.Unspecified) {
            ColorFilter.tint(tintColor, tintBlendMode)
        } else {
            null
        }
    }

    return remember { VectorPainter() }.apply {
        // These assignments are thread safe as parameters are backed by a mutableState object
        size = Size(widthPx, heightPx)
        this.autoMirror = autoMirror
        this.intrinsicColorFilter = intrinsicColorFilter
        RenderVector(name, vpWidth, vpHeight, content)
    }
}

This is a very common approach in many default Composables and the most common one is LaunchedEffect

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

which runs code block on first composition or when at least one of they keys change.

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 1
    Thanks for the answer. Isn't the whole point of remember to survive recomposition? From the Android docs I read that `A value computed by remember is stored in the Composition during initial composition, and the stored value is returned during recomposition` – TareK Khoury Jun 21 '22 at 10:43
  • Yes, but under some conditions you want it to be recalculated. I use it a lot. For instance [here](https://github.com/SmartToolFactory/Compose-Image-Cropper/blob/a05b1b1c8d254259a49639fae835124ad05dae37/imagecropper/src/main/java/com/smarttoolfactory/imagecropper/ImageScope.kt#L107) – Thracian Jun 21 '22 at 10:46
  • For the remember in the link, i crop bitmap when the bitmap user picks another bitmap, width or height of layout bitmap is drawn changes, rectangle is which section of bitmap should be drawn and contentScale is the same as Compose image uses. If you want your calculation to run when any of the conditions or parameters change you set keys for `remember`. `LaunchedEffect` under the hood is also a remember function which runs when a key changes. – Thracian Jun 21 '22 at 10:51
  • You can check this [question](https://stackoverflow.com/questions/70144298/compose-remember-with-keys-vs-derivedstateof), it mentions about derived state too, but that part is not relevant with your question. – Thracian Jun 21 '22 at 11:03
  • I wanted to calculate image width once (image won't change). this is why I didn't think of using any keys. For some reason they are resetting and not getting calculated back. Like the person from the first comment on my questions quoted: `forgets the object when the composable that called remember is removed from the Composition`. I can understand this. But I did not understand is that after removal we should have initial composition and re-calculation triggered which does not happen – TareK Khoury Jun 21 '22 at 11:09
  • Thanks for the samples though, will go over them. – TareK Khoury Jun 21 '22 at 11:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/245788/discussion-between-thracian-and-tarek-khoury). – Thracian Jun 21 '22 at 11:13