Merge pull request #2857 from vitorpamplona/claude/fix-notecompose-scroll-jitter-8lw3R

Optimize Compose state management and flow subscriptions
This commit is contained in:
Vitor Pamplona
2026-05-12 09:24:25 -04:00
committed by GitHub
15 changed files with 276 additions and 139 deletions
@@ -22,35 +22,47 @@ package com.vitorpamplona.amethyst.commons.compose
import androidx.collection.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// On a cache hit, short-circuit with a remembered State<V?> and skip the produceState coroutine.
// Only on a miss do we launch the suspending update.
@Composable
fun <K : Any, V : Any> produceCachedStateAsync(
cache: AsyncCachedState<K, V>,
key: K,
): State<V?> =
@Suppress("ProduceStateDoesNotAssignValue")
produceState(initialValue = cache.cached(key), key1 = key) {
val newValue = cache.update(key)
if (newValue != value) {
value = newValue
): State<V?> {
val state = remember(key) { mutableStateOf(cache.cached(key)) }
if (state.value == null) {
LaunchedEffect(key) {
val newValue = cache.update(key)
if (state.value != newValue) {
state.value = newValue
}
}
}
return state
}
@Composable
fun <K : Any, V : Any> produceCachedStateAsync(
cache: AsyncCachedState<K, V>,
key: String,
updateValue: K,
): State<V?> =
@Suppress("ProduceStateDoesNotAssignValue")
produceState(initialValue = cache.cached(updateValue), key1 = key) {
val newValue = cache.update(updateValue)
if (newValue != value) {
value = newValue
): State<V?> {
val state = remember(key) { mutableStateOf(cache.cached(updateValue)) }
if (state.value == null) {
LaunchedEffect(key) {
val newValue = cache.update(updateValue)
if (state.value != newValue) {
state.value = newValue
}
}
}
return state
}
interface AsyncCachedState<K : Any, V : Any> {
fun cached(k: K): V?
@@ -22,35 +22,48 @@ package com.vitorpamplona.amethyst.commons.compose
import androidx.collection.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// On a cache hit (the common case during scroll for things like Bech32 link previews),
// short-circuit with a remembered State<V?> and skip the produceState coroutine entirely.
// Only on a miss do we launch the suspending update.
@Composable
fun <K : Any, V : Any> produceCachedState(
cache: CachedState<K, V>,
key: K,
): State<V?> =
@Suppress("ProduceStateDoesNotAssignValue")
produceState(initialValue = cache.cached(key), key1 = key) {
val newValue = cache.update(key)
if (value != newValue) {
value = newValue
): State<V?> {
val state = remember(key) { mutableStateOf(cache.cached(key)) }
if (state.value == null) {
LaunchedEffect(key) {
val newValue = cache.update(key)
if (state.value != newValue) {
state.value = newValue
}
}
}
return state
}
@Composable
fun <K : Any, V : Any> produceCachedState(
cache: CachedState<K, V>,
key: String,
updateValue: K,
): State<V?> =
@Suppress("ProduceStateDoesNotAssignValue")
produceState(initialValue = cache.cached(updateValue), key1 = key) {
val newValue = cache.update(updateValue)
if (value != newValue) {
value = newValue
): State<V?> {
val state = remember(key) { mutableStateOf(cache.cached(updateValue)) }
if (state.value == null) {
LaunchedEffect(key) {
val newValue = cache.update(updateValue)
if (state.value != newValue) {
state.value = newValue
}
}
}
return state
}
interface CachedState<K : Any, V : Any> {
fun cached(k: K): V?