From 52041f4bcd76a9b4182113d8cab3eac7ef5a9b1b Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:24:30 -0400 Subject: [PATCH 1/9] 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. --- .../main/kotlin/com/getcode/ui/components/SlideToConfirm.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt index de66eb614..8797ffb82 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/SlideToConfirm.kt @@ -162,6 +162,7 @@ fun SlideToConfirm( }, ) { val currentIsLoading by rememberUpdatedState(isLoading) + val currentOnConfirm by rememberUpdatedState(onConfirm) val hapticFeedback = LocalHapticFeedback.current val density = LocalDensity.current val thumbSize = Thumb.Size @@ -210,7 +211,7 @@ fun SlideToConfirm( .filter { it == 1f } .collect { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onConfirm() + currentOnConfirm() // Give the caller a moment to set isLoading = true. // If they don't, the confirmation was rejected — reset. delay(200) From 76e81c6e6d07ae07d26fb21fa78b38f483bb9c18 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:12 -0400 Subject: [PATCH 2/9] 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. --- .../app/bill/customization/features/TextureControls.kt | 9 ++++++--- .../ui/components/chat/messagecontents/Feedback.kt | 8 +++++--- .../chat/messagecontents/MessageTextContent.kt | 4 +++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt index f36709b95..a08a7b4ae 100644 --- a/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt +++ b/apps/flipcash/features/bill-customization/src/main/kotlin/com/flipcash/app/bill/customization/features/TextureControls.kt @@ -91,10 +91,13 @@ internal fun TextureControls( } } + val blendModes = remember(state.blendModes) { + state.blendModes.map { it.blendMode } + } + val selectedTabIndex by remember(state.selectedBlendMode) { derivedStateOf { - val option = state.selectedBlendMode.blendMode - state.blendModes.map { it.blendMode }.indexOf(option).takeIf { it >= 0 } ?: 0 + blendModes.indexOf(state.selectedBlendMode.blendMode).takeIf { it >= 0 } ?: 0 } } @@ -106,7 +109,7 @@ internal fun TextureControls( indicator = {}, // no indicator divider = {} // no divider ) { - state.blendModes.map { it.blendMode }.forEachIndexed { index, mode -> + blendModes.forEachIndexed { index, mode -> BlendModeTab( mode = mode, isSelected = index == selectedTabIndex, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt index 8ee49dcdf..855af1f55 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt @@ -67,9 +67,11 @@ internal fun Feedback( ) } - val rxns = reactions - .sortedBy { it.sentAt } - .groupBy { it.emoji } + val rxns = remember(reactions) { + reactions + .sortedBy { it.sentAt } + .groupBy { it.emoji } + } rxns.onEach { (emoji, occurrences) -> EmojiCounter( diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt index 2a35dccca..5b03d15de 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessageTextContent.kt @@ -352,7 +352,9 @@ private fun MarkupTextHandler( options.onMarkupClicked != null -> { val markupTextHelper = remember { MarkupTextHelper() } - val markups = options.markupsToResolve.map { Markup.create(it) } + val markups = remember(options.markupsToResolve) { + options.markupsToResolve.map { Markup.create(it) } + } val annotatedString = markupTextHelper.annotate(text.text, markups) val markupHoist = LocalTextLayoutResult.current From 28400b04a065c7a98b41df76be2dbcd8039cf3fa Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:28 -0400 Subject: [PATCH 3/9] fix(compose): use collectAsStateWithLifecycle in CashScreenContent Last remaining collectAsState() call in production code. Stops flow collection when the app is backgrounded. --- .../com/flipcash/app/cash/internal/CashScreenContent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt index 8de8bb958..0e8910a57 100644 --- a/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt +++ b/apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenContent.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -22,7 +22,7 @@ import com.getcode.ui.theme.CodeButton @Composable internal fun GiveScreenContent(viewModel: CashScreenViewModel) { val navigator = LocalCodeNavigator.current - val state by viewModel.stateFlow.collectAsState() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() Column( modifier = Modifier.fillMaxSize(), ) { From f592e6e20f82d7f53090afded91fc19b19b20e22 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:26:55 -0400 Subject: [PATCH 4/9] fix(compose): use mutableLongStateOf to avoid autoboxing Replace mutableStateOf(0L) with mutableLongStateOf(0L) to avoid Long autoboxing on every state read. --- .../src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt b/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt index d50a45d10..87ebe97bd 100644 --- a/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt +++ b/ui/biometrics/src/main/kotlin/com/getcode/ui/biometrics/Biometrics.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -62,7 +63,7 @@ fun rememberBiometricsState( } var lastAuthTimestamp by remember { - mutableStateOf(0L) + mutableLongStateOf(0L) } LaunchedEffect(checkBiometrics, requireBiometrics, canAuthenticate) { From 65fa033aefa910f4bf1a3fb05e3c86539ba9fc5e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:34:07 -0400 Subject: [PATCH 5/9] 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. --- .../com/flipcash/app/internal/ui/navigation/MainRoot.kt | 4 ++-- .../app/shareapp/internal/ShareAppScreenContent.kt | 6 +++--- .../com/getcode/ui/components/bars/BottomBarContainer.kt | 7 +++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt index 499192d8c..4392ab73d 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.navigation3.runtime.NavKey import com.flipcash.app.android.R @@ -79,7 +79,7 @@ internal fun MainRoot(deepLink: () -> DeepLink?) { label = "loading visibility" ) CodeCircularProgressIndicator( - modifier = Modifier.alpha(loadingAlpha) + modifier = Modifier.graphicsLayer { alpha = loadingAlpha } ) } } diff --git a/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt b/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt index 8397e5f10..defd0a3cc 100644 --- a/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt +++ b/apps/flipcash/features/shareapp/src/main/kotlin/com/flipcash/app/shareapp/internal/ShareAppScreenContent.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.boundsInWindow @@ -113,7 +113,7 @@ internal fun ShareAppScreenContent() { Text( modifier = Modifier .padding(bottom = CodeTheme.dimens.grid.x4) - .alpha(contentAlpha), + .graphicsLayer { alpha = contentAlpha }, text = stringResource(R.string.subtitle_scanToDownload), style = CodeTheme.typography.textLarge, textAlign = TextAlign.Center @@ -139,7 +139,7 @@ internal fun ShareAppScreenContent() { ) Row( - modifier = Modifier.alpha(contentAlpha), + modifier = Modifier.graphicsLayer { alpha = contentAlpha }, horizontalArrangement = Arrangement.spacedBy( space = CodeTheme.dimens.inset, alignment = Alignment.CenterHorizontally diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt index 2e25040e3..485128179 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/bars/BottomBarContainer.kt @@ -42,9 +42,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -121,7 +120,7 @@ fun BottomBarContainer(barMessages: BarMessages, onShown: (BottomBarManager.Bott Box( modifier = Modifier .fillMaxSize() - .alpha(scrimAlpha) + .graphicsLayer { alpha = scrimAlpha } .background(CodeTheme.colors.scrim) .rememberedClickable( indication = null, @@ -257,7 +256,7 @@ fun BottomBarView( color = LocalContentColor.current.copy(alpha = 0.8f), ) Icon( - modifier = Modifier.rotate(rotation), + modifier = Modifier.graphicsLayer { rotationZ = rotation }, imageVector = Icons.Rounded.ExpandMore, contentDescription = null, tint = LocalContentColor.current.copy(alpha = 0.8f), From a8b5cba5b457686578e8ecb28dc28a48894cad31 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:35:11 -0400 Subject: [PATCH 6/9] 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. --- .../app/directsend/internal/screens/ContactListScreen.kt | 4 ++-- .../internal/screens/ContactsPermissionGateScreen.kt | 2 +- .../kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt index cf04d946e..c43e33558 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt @@ -88,7 +88,7 @@ internal fun ContactListScreen() { val state by viewModel.stateFlow.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { event -> @@ -96,7 +96,7 @@ internal fun ContactListScreen() { } } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { event -> diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt index 23775602b..241097756 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactsPermissionGateScreen.kt @@ -50,7 +50,7 @@ internal fun ContactsPermissionGateScreen() { } } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .collect { flowNavigator.proceed() } diff --git a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt index 44776c399..d6ee1bcd3 100644 --- a/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt +++ b/apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/PhantomWalletScreens.kt @@ -47,7 +47,7 @@ internal fun PhantomConnectConfirmationScreen() { .launchIn(this) } - LaunchedEffect(Unit) { + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() .onEach { flowNavigator.navigateTo(SwapStep.PhantomConfirmTransaction) } From 640e1b52299715325af57dbc9109a2d6701dd275 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:35:55 -0400 Subject: [PATCH 7/9] 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. --- .../app/devicelogs/internal/DeviceLogsScreen.kt | 5 ++++- .../flipcash/app/userflags/internal/UserFlagsEditor.kt | 10 ++++++++-- .../main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt | 5 ++++- .../com/getcode/ui/emojis/FrequentlyUsedEmojis.kt | 5 ++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt b/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt index e2d5cd2e2..401642dd6 100644 --- a/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt +++ b/apps/flipcash/features/device-logs/src/main/kotlin/com/flipcash/app/devicelogs/internal/DeviceLogsScreen.kt @@ -196,7 +196,10 @@ private fun LogList( ), verticalArrangement = Arrangement.spacedBy(2.dp), ) { - items(lines.asReversed()) { line -> + items( + items = lines.asReversed(), + key = { it.hashCode() }, + ) { line -> LogLine(line = line, filter = filter) } } diff --git a/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt b/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt index d7abc992a..ada552e55 100644 --- a/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt +++ b/apps/flipcash/features/userflags/src/main/kotlin/com/flipcash/app/userflags/internal/UserFlagsEditor.kt @@ -75,13 +75,19 @@ private fun UserFlagsEditor( ) { // Read-only section item { SectionHeader("Read-Only") } - items(state.readOnlyEntries) { entry -> + items( + items = state.readOnlyEntries, + key = { it.label }, + ) { entry -> ReadOnlyRow(entry) } // Editable section item { SectionHeader("Overridable") } - items(state.editableEntries) { entry -> + items( + items = state.editableEntries, + key = { it.field.label }, + ) { entry -> EditableRow( modifier = Modifier.fillMaxWidth(), entry = entry, diff --git a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt index 8d429c651..7d033dfe2 100644 --- a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt +++ b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/EmojiSearch.kt @@ -51,7 +51,10 @@ fun EmojiSearchResults( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + CodeTheme.dimens.grid.x2 ) ) { - items(results.orEmpty()) { emoji -> + items( + items = results.orEmpty(), + key = { it.unicode }, + ) { emoji -> Row( modifier = Modifier .clickable { onSelected(emoji.unicode) }, diff --git a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt index c4082662d..2f3a7fc60 100644 --- a/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt +++ b/ui/emojis/src/main/kotlin/com/getcode/ui/emojis/FrequentlyUsedEmojis.kt @@ -49,7 +49,10 @@ fun FrequentlyUsedEmojis( .height(CodeTheme.dimens.grid.x9), contentPadding = PaddingValues(horizontal = CodeTheme.dimens.inset) ) { - items(frequentlyUsedEmojis) { emoji -> + items( + items = frequentlyUsedEmojis, + key = { it }, + ) { emoji -> EmojiRender( emoji = emoji, size = DpSize( From a01061299527f565e6a74c266eb3e3bba5096ab1 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:37:46 -0400 Subject: [PATCH 8/9] fix(compose): use rememberSaveable for user-driven UI flags Replace remember with rememberSaveable for isExportSeedRequested and isStoragePermissionGranted so these flags survive configuration changes. --- .../app/backupkey/internal/BackupKeyScreenContent.kt | 5 +++-- .../app/login/internal/screens/AccessKeyScreenContent.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt b/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt index 8226304cb..641a70e01 100644 --- a/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt +++ b/apps/flipcash/features/backupkey/src/main/kotlin/com/flipcash/app/backupkey/internal/BackupKeyScreenContent.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -59,8 +60,8 @@ internal fun BackupKeyScreenContent(viewModel: BackupKeyScreenViewModel) { val resources = LocalResources.current val dataState by viewModel.uiFlow.collectAsStateWithLifecycle() - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } + var isExportSeedRequested by rememberSaveable { mutableStateOf(false) } + var isStoragePermissionGranted by rememberSaveable { mutableStateOf(false) } val isAccessKeyVisible = remember { MutableTransitionState(false) } val onPermissionResult = { result: PermissionResult -> diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt index 5f347c7ff..0c741b026 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/internal/screens/AccessKeyScreenContent.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -78,8 +79,8 @@ internal fun AccessKeyScreen( val composeScope = rememberCoroutineScope() - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } + var isExportSeedRequested by rememberSaveable { mutableStateOf(false) } + var isStoragePermissionGranted by rememberSaveable { mutableStateOf(false) } val onPermissionResult = { result: PermissionResult -> isStoragePermissionGranted = result == PermissionResult.Granted From 3a1f8a1036c700134b8ca7e7a17103233714d3ad Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 10:38:17 -0400 Subject: [PATCH 9/9] 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. --- .../main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt | 4 ++-- .../kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt b/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt index 46a09b29e..59f9283f4 100644 --- a/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt +++ b/apps/flipcash/shared/appupdates/src/main/kotlin/com/flipcash/app/updates/LocalAppUpdater.kt @@ -1,10 +1,10 @@ package com.flipcash.app.updates -import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -val LocalAppUpdater = compositionLocalOf { StubAppUpdateController() } +val LocalAppUpdater = staticCompositionLocalOf { StubAppUpdateController() } class StubAppUpdateController(updateInfo: UpdateInfo? = null): AppUpdateController { override val availableUpdate: StateFlow = MutableStateFlow(updateInfo) diff --git a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt index 28ea59547..2490308fb 100644 --- a/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt +++ b/apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampLocal.kt @@ -1,6 +1,6 @@ package com.flipcash.app.onramp -import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf val LocalCoinbaseOnRampController = - compositionLocalOf { throw IllegalStateException() } + staticCompositionLocalOf { throw IllegalStateException() }