Skip to content

Commit 0684ddc

Browse files
bmc08gtclaude
andauthored
fix(compose): lazy list keys, stale remember, redundant derivedStateOf (#851)
* fix(compose): prevent stale onConfirm capture in SlideToConfirm onConfirm was captured by a LaunchedEffect(Unit) without rememberUpdatedState. If the caller recomposes with a new lambda, the stale version executes on swipe-to-confirm. * fix(compose): cache collection transforms in per-item composables Wrap sortedBy/groupBy, map, and forEachIndexed transforms in remember(key) so they do not re-run on every recomposition inside LazyColumn item factories. * fix(compose): use collectAsStateWithLifecycle in CashScreenContent Last remaining collectAsState() call in production code. Stops flow collection when the app is backgrounded. * fix(compose): use mutableLongStateOf to avoid autoboxing Replace mutableStateOf(0L) with mutableLongStateOf(0L) to avoid Long autoboxing on every state read. * fix(compose): defer animation reads to draw phase via graphicsLayer Replace Modifier.alpha(animatedFloat) and Modifier.rotate(animatedFloat) with graphicsLayer equivalents so animated values are read in the draw phase instead of triggering recomposition every frame. * fix(compose): use viewModel as LaunchedEffect key for event flow collection Replace LaunchedEffect(Unit) with LaunchedEffect(viewModel) so the effect restarts if the ViewModel instance changes, and is consistent across composables. * fix(compose): add stable keys to lazy lists Add key parameters to items() calls in DeviceLogsScreen, UserFlagsEditor, FrequentlyUsedEmojis, and EmojiSearch so Compose can diff items correctly during recomposition instead of recomposing every item. * fix(compose): use rememberSaveable for user-driven UI flags Replace remember with rememberSaveable for isExportSeedRequested and isStoragePermissionGranted so these flags survive configuration changes. * fix(compose): use staticCompositionLocalOf for singleton controllers LocalAppUpdater and LocalCoinbaseOnRampController are provided once at app startup and never change. Using staticCompositionLocalOf avoids unnecessary recomposition tracking overhead. * fix(compose): correct parameter ordering in shared components Reorder parameters to follow Compose API guidelines: required params first, then modifier, then optional params, then trailing lambdas. Also fix modifier chain ordering in Pill and CodeToggleSwitch so the caller modifier is applied outermost. * fix(compose): add modifier parameter to ConnectionStatus Shared reusable component was missing a modifier parameter, preventing callers from controlling its layout. * fix(compose): key BillCustomizationScreen effect on changing state LaunchedEffect(Unit) was reading state.customizations and state.purchaseAmount but only running once. Key on these values so the effect re-fires when they change. * refactor(compose): remove rememberedClickable/rememberedLongClickable wrappers With Strong Skipping Mode enabled, the remember-based lambda caching in rememberedClickable is redundant — Compose auto-stabilizes lambdas. The Modifier.composed wrapper also allocates a sub-composition per element per recomposition. - Delete both rememberedClickable overloads from Modifier.kt - Rename rememberedLongClickable to longClickable (no composed) - Simplify noRippleClickable (no composed, null interactionSource) - Keep unboundedClickable (needs composed for @composable ripple()) - Migrate 38 call sites across the codebase: - Simple .rememberedClickable -> .clickable (standard foundation) - No-ripple pattern -> .noRippleClickable - Long click -> .longClickable - Remove unused MutableInteractionSource imports * fix(compose): add lazy list keys, fix stale remember, remove redundant derivedStateOf - Add stable `key` to lazy list items in TokenDetails and BackgroundControls - Remove redundant `derivedStateOf` wrappers where the block reads no snapshot State (NavigationBar, TransactionReceipt, TokenList) - Add missing `remember` keys to prevent stale cached values (LabsScreen, TokenDiscoveryScreen, Editors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4f51f44 commit 0684ddc

8 files changed

Lines changed: 15 additions & 28 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import androidx.compose.foundation.layout.size
2020
import androidx.compose.foundation.layout.width
2121
import androidx.compose.material.Text
2222
import androidx.compose.runtime.Composable
23-
import androidx.compose.runtime.derivedStateOf
24-
import androidx.compose.runtime.getValue
2523
import androidx.compose.runtime.remember
2624
import androidx.compose.ui.Alignment
2725
import androidx.compose.ui.Modifier
@@ -112,11 +110,8 @@ fun NavigationBar(
112110
fadeOut(animationSpec = tween(500, 100))
113111
else fadeOut(animationSpec = tween(0)),
114112
) {
115-
val toastText by remember(state.toastText) {
116-
derivedStateOf { state.toastText }
117-
}
118113
Pill(
119-
text = toastText.orEmpty(),
114+
text = state.toastText.orEmpty(),
120115
textStyle = CodeTheme.typography.textSmall.copy(
121116
fontWeight = FontWeight.Bold
122117
),

apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/BackgroundControls.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ internal fun BackgroundControls(
126126
}
127127
}
128128

129-
items(state.colorOptions.size) { index ->
129+
items(state.colorOptions.size, key = { state.colorOptions[it].colorHex }) { index ->
130130
ColorOptionItem(state.colorOptions, index) { event ->
131131
dispatchEvent(event)
132132
}

apps/flipcash/features/discovery/src/main/kotlin/com/flipcash/app/discovery/TokenDiscoveryScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.onEach
2727
fun TokenDiscoveryScreen() {
2828
val navigator = LocalCodeNavigator.current
2929
val viewModel = hiltViewModel<TokenDiscoveryViewModel>()
30-
val isSheetRoot = remember { navigator.backStack.size <= 1 }
30+
val isSheetRoot = remember(navigator) { navigator.backStack.size <= 1 }
3131

3232
Column(
3333
modifier = Modifier.fillMaxSize(),

apps/flipcash/features/lab/src/main/kotlin/com/flipcash/app/lab/LabsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import com.getcode.ui.components.AppBarWithTitle
1818
@Composable
1919
fun LabsScreen() {
2020
val navigator = LocalCodeNavigator.current
21-
val isSheetRoot = remember { navigator.backStack.size <= 1 }
21+
val isSheetRoot = remember(navigator) { navigator.backStack.size <= 1 }
2222
Column(
2323
modifier = Modifier.fillMaxSize(),
2424
horizontalAlignment = Alignment.CenterHorizontally,

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/internal/components/info/TokenDetails.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ internal fun TokenDetailsSection(
9595
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2),
9696
contentPadding = PaddingValues(horizontal = CodeTheme.dimens.inset)
9797
) {
98-
items(links) { link ->
98+
items(links, key = { it.uri }) { link ->
9999
SocialChip(link)
100100
}
101101
}

apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/components/Editors.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ internal fun <T> TextInputContent(
3737
dispatch: (UserFlagsViewModel.Event) -> Unit,
3838
onDismiss: () -> Unit,
3939
) {
40-
val textFieldState = remember {
40+
val textFieldState = remember(entry) {
4141
TextFieldState(initialText = entry.formattedEditValue())
4242
}
4343

@@ -80,7 +80,7 @@ internal fun <T> MultiSelectContent(
8080
dispatch: (UserFlagsViewModel.Event) -> Unit,
8181
onDismiss: () -> Unit,
8282
) {
83-
val selected = remember {
83+
val selected = remember(entry) {
8484
mutableStateListOf<T>().apply {
8585
addAll(entry.resolved.effectiveValue)
8686
}
@@ -129,7 +129,7 @@ internal fun <T> SingleSelectContent(
129129
dispatch: (UserFlagsViewModel.Event) -> Unit,
130130
onDismiss: () -> Unit,
131131
) {
132-
var selected by remember { mutableStateOf(entry.resolved.effectiveValue) }
132+
var selected by remember(entry) { mutableStateOf(entry.resolved.effectiveValue) }
133133

134134
editor.options.forEach { (label, value) ->
135135
Row(

apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/internal/components/TransactionReceipt.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import androidx.compose.foundation.layout.padding
1010
import androidx.compose.material3.Text
1111
import androidx.compose.runtime.Composable
1212
import androidx.compose.runtime.CompositionLocalProvider
13-
import androidx.compose.runtime.derivedStateOf
14-
import androidx.compose.runtime.getValue
1513
import androidx.compose.runtime.remember
1614
import androidx.compose.ui.Alignment
1715
import androidx.compose.ui.Modifier
@@ -68,10 +66,8 @@ internal fun TransactionReceipt(
6866
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x4),
6967
horizontalAlignment = Alignment.CenterHorizontally
7068
) {
71-
val netTransferAmount by remember(tokenWithBalance, fee) {
72-
derivedStateOf {
73-
tokenWithBalance.balance - (fee ?: 0.toFiat(tokenWithBalance.balance.currencyCode))
74-
}
69+
val netTransferAmount = remember(tokenWithBalance, fee) {
70+
tokenWithBalance.balance - (fee ?: 0.toFiat(tokenWithBalance.balance.currencyCode))
7571
}
7672

7773
LineItems(

apps/flipcash/shared/tokens/src/main/kotlin/com/flipcash/app/tokens/ui/TokenList.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,12 @@ fun TokenList(
5757
) {
5858
val listState = rememberLazyListState()
5959

60-
val cashReserves by remember(tokens) {
61-
derivedStateOf {
62-
tokens?.find { it.token.address == Mint.usdf }?.balance ?: LocalFiat.Zero
63-
}
60+
val cashReserves = remember(tokens) {
61+
tokens?.find { it.token.address == Mint.usdf }?.balance ?: LocalFiat.Zero
6462
}
65-
val filteredTokens by remember(tokens, includeReserves) {
66-
derivedStateOf {
67-
if (includeReserves) tokens
68-
else tokens?.filterNot { it.token.address == Mint.usdf }
69-
}
63+
val filteredTokens = remember(tokens, includeReserves) {
64+
if (includeReserves) tokens
65+
else tokens?.filterNot { it.token.address == Mint.usdf }
7066
}
7167

7268
val footerSettled by remember {

0 commit comments

Comments
 (0)