diff --git a/AGENTS.md b/AGENTS.md index 1600ddb18..886ce0c29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,6 +231,10 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS prefer `when (subject)` with Kotlin guard conditions (`if`) over condition-based `when {}` with `is` type checks, e.g. `when (event) { is Foo if event.x == y -> ... }` instead of `when { event is Foo && event.x == y -> ... }` - ALWAYS prefer `sealed interface` over `sealed class` when no shared state or constructor is needed - NEVER duplicate error logging in `.onFailure {}` if the called method already logs the same error internally +- ALWAYS use `ImmutableList`/`ImmutableMap`/`ImmutableSet` instead of `List`/`Map`/`Set` for composable function parameters and UiState data class fields +- ALWAYS annotate UiState data classes with `@Immutable`; use `@Stable` instead when any field holds a non-immutable type (e.g. `Throwable`, external library types from `bitkitcore`/`ldknode`/`vssclient`, or types containing plain `List`/`Map`/`Set`) +- ALWAYS use `.toImmutableList()`, `.toImmutableMap()`, `.toImmutableSet()` when producing collections for UI state +- ALWAYS use `persistentListOf()`, `persistentMapOf()`, `persistentSetOf()` for default values in UiState fields ### Device Debugging (adb) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77cd5453c..ec309661e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -225,6 +225,7 @@ dependencies { implementation(libs.material) implementation(libs.datastore.preferences) implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.collections.immutable) implementation(libs.biometric) implementation(libs.zxing) implementation(libs.barcode.scanning) diff --git a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt index 7fb9a5b85..51df37d20 100644 --- a/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/ActivityRepo.kt @@ -1,6 +1,7 @@ package to.bitkit.repositories import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Immutable import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.ActivityTags @@ -11,6 +12,9 @@ import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.SortDirection +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async @@ -600,7 +604,7 @@ class ActivityRepo @Inject constructor( runCatching { coreService.activity.allPossibleTags() }.onSuccess { tags -> - _state.update { it.copy(tags = tags) } + _state.update { it.copy(tags = tags.toImmutableList()) } }.onFailure { Logger.error("getAllAvailableTags error", it, context = TAG) } @@ -677,6 +681,7 @@ class ActivityRepo @Inject constructor( } } +@Immutable data class ActivityState( - val tags: List = emptyList(), + val tags: ImmutableList = persistentListOf(), ) diff --git a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt index 6107e60d9..43f123ab4 100644 --- a/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt @@ -1,5 +1,6 @@ package to.bitkit.repositories +import androidx.compose.runtime.Stable import com.synonym.bitkitcore.BtOrderState2 import com.synonym.bitkitcore.ChannelLiquidityOptions import com.synonym.bitkitcore.ChannelLiquidityParams @@ -14,6 +15,9 @@ import com.synonym.bitkitcore.calculateChannelLiquidityOptions import com.synonym.bitkitcore.getDefaultLspBalance import com.synonym.bitkitcore.giftOrder import com.synonym.bitkitcore.giftPay +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -104,7 +108,7 @@ class BlocktankRepo @Inject constructor( .collect { paidOrderIds -> _blocktankState.update { state -> state.copy( - paidOrders = state.orders.filter { order -> order.id in paidOrderIds }, + paidOrders = state.orders.filter { order -> order.id in paidOrderIds }.toImmutableList(), ) } } @@ -148,9 +152,9 @@ class BlocktankRepo @Inject constructor( val cachedCjitEntries = coreService.blocktank.cjitEntries(refresh = false) _blocktankState.update { state -> state.copy( - orders = cachedOrders, - cjitEntries = cachedCjitEntries, - paidOrders = cachedOrders.filter { order -> order.id in paidOrderIds }, + orders = cachedOrders.toImmutableList(), + cjitEntries = cachedCjitEntries.toImmutableList(), + paidOrders = cachedOrders.filter { order -> order.id in paidOrderIds }.toImmutableList(), ) } @@ -159,9 +163,9 @@ class BlocktankRepo @Inject constructor( val cjitEntries = coreService.blocktank.cjitEntries(refresh = true) _blocktankState.update { state -> state.copy( - orders = orders, - cjitEntries = cjitEntries, - paidOrders = orders.filter { order -> order.id in paidOrderIds }, + orders = orders.toImmutableList(), + cjitEntries = cjitEntries.toImmutableList(), + paidOrders = orders.filter { order -> order.id in paidOrderIds }.toImmutableList(), ) } @@ -287,7 +291,7 @@ class BlocktankRepo @Inject constructor( updatedOrders[index] = order } - _blocktankState.update { state -> state.copy(orders = updatedOrders) } + _blocktankState.update { state -> state.copy(orders = updatedOrders.toImmutableList()) } return@runCatching order }.onFailure { @@ -406,8 +410,8 @@ class BlocktankRepo @Inject constructor( _blocktankState.update { it.copy( - orders = payload.orders, - cjitEntries = payload.cjitEntries, + orders = payload.orders.toImmutableList(), + cjitEntries = payload.cjitEntries.toImmutableList(), info = payload.info, ) } @@ -516,10 +520,11 @@ class BlocktankRepo @Inject constructor( } } +@Stable data class BlocktankState( - val orders: List = emptyList(), - val paidOrders: List = emptyList(), - val cjitEntries: List = emptyList(), + val orders: ImmutableList = persistentListOf(), + val paidOrders: ImmutableList = persistentListOf(), + val cjitEntries: ImmutableList = persistentListOf(), val info: IBtInfo? = null, val minCjitSats: Int? = null, ) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 57d347abc..42a792943 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -1,5 +1,9 @@ package to.bitkit.repositories +import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -112,7 +116,7 @@ class CurrencyRepo @Inject constructor( rate.quote == settings.selectedCurrency } _currencyState.value.copy( - rates = cachedData.cachedRates, + rates = cachedData.cachedRates.toImmutableList(), selectedCurrency = settings.selectedCurrency, displayUnit = settings.displayUnit, primaryDisplay = settings.primaryDisplay, @@ -248,8 +252,9 @@ class CurrencyRepo @Inject constructor( override fun convertSatsToFiatString(sats: Long): String = convertSatsToFiatPair(sats).getOrNull()?.second ?: "" } +@Stable data class CurrencyState( - val rates: List = emptyList(), + val rates: ImmutableList = persistentListOf(), val error: Throwable? = null, val hasStaleData: Boolean = false, val selectedCurrency: String = "USD", diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 3c9ecdc05..04bde8c44 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -1,5 +1,6 @@ package to.bitkit.repositories +import androidx.compose.runtime.Stable import com.google.firebase.messaging.FirebaseMessaging import com.synonym.bitkitcore.AddressType import com.synonym.bitkitcore.ClosedChannelDetails @@ -10,6 +11,9 @@ import com.synonym.bitkitcore.Scanner import com.synonym.bitkitcore.createChannelRequestUrl import com.synonym.bitkitcore.createWithdrawCallbackUrl import com.synonym.bitkitcore.lnurlAuth +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -1170,8 +1174,8 @@ class LightningRepo @Inject constructor( it.copy( nodeId = getNodeId().orEmpty(), nodeStatus = getStatus(), - peers = getPeers().orEmpty(), - channels = getChannels().orEmpty(), + peers = getPeers().orEmpty().toImmutableList(), + channels = getChannels().orEmpty().toImmutableList(), balances = getBalances(), ) } @@ -1388,12 +1392,13 @@ class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node t class GetPaymentsError : AppError("It wasn't possible get the payments") class SyncUnhealthyError : AppError("Wallet sync failed before send") +@Stable data class LightningState( val nodeId: String = "", val nodeStatus: NodeStatus? = null, val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped, - val peers: List = emptyList(), - val channels: List = emptyList(), + val peers: ImmutableList = persistentListOf(), + val channels: ImmutableList = persistentListOf(), val balances: BalanceDetails? = null, val isSyncingWallet: Boolean = false, val isGeoBlocked: Boolean = false, diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 12392f2c7..78a990867 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -1,8 +1,12 @@ package to.bitkit.repositories +import androidx.compose.runtime.Immutable import com.synonym.bitkitcore.AddressType import com.synonym.bitkitcore.PreActivityMetadata import com.synonym.bitkitcore.Scanner +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -434,7 +438,7 @@ class WalletRepo @Inject constructor( _walletState.update { it.copy( bip21 = "", - selectedTags = if (clearTags) emptyList() else it.selectedTags, + selectedTags = if (clearTags) persistentListOf() else it.selectedTags, bip21AmountSats = null, bip21Description = "", ) @@ -474,7 +478,7 @@ class WalletRepo @Inject constructor( preActivityMetadataRepo.addPreActivityMetadataTags(paymentId, listOf(newTag)).onSuccess { _walletState.update { it.copy( - selectedTags = (it.selectedTags + newTag).distinct() + selectedTags = (it.selectedTags + newTag).distinct().toImmutableList() ) } settingsStore.addLastUsedTag(newTag) @@ -495,7 +499,7 @@ class WalletRepo @Inject constructor( .onSuccess { _walletState.update { it.copy( - selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag } + selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag }.toImmutableList() ) } }.onFailure { @@ -508,7 +512,7 @@ class WalletRepo @Inject constructor( if (paymentId == null || paymentId.isEmpty()) return@withContext preActivityMetadataRepo.resetPreActivityMetadataTags(paymentId).onSuccess { - _walletState.update { it.copy(selectedTags = emptyList()) } + _walletState.update { it.copy(selectedTags = persistentListOf()) } }.onFailure { Logger.error("Failed to reset tags for pre-activity metadata", it, context = TAG) } @@ -588,13 +592,14 @@ class WalletRepo @Inject constructor( } } +@Immutable data class WalletState( val onchainAddress: String = "", val bolt11: String = "", val bip21: String = "", val bip21AmountSats: ULong? = null, val bip21Description: String = "", - val selectedTags: List = listOf(), + val selectedTags: ImmutableList = persistentListOf(), val walletExists: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 4c5ca717b..d15c2875e 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -41,6 +40,7 @@ import androidx.navigation.toRoute import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -339,7 +339,7 @@ fun ContentView( } val balance by walletViewModel.balanceState.collectAsStateWithLifecycle() - val currencies by currencyViewModel.uiState.collectAsState() + val currencies by currencyViewModel.uiState.collectAsStateWithLifecycle() // Keep backups in sync LaunchedEffect(backupsViewModel) { backupsViewModel.observeAndSyncBackups() } @@ -381,7 +381,7 @@ fun ContentView( } is Sheet.Receive -> { - val walletState by walletViewModel.walletState.collectAsState() + val walletState by walletViewModel.walletState.collectAsStateWithLifecycle() ReceiveSheet( walletState = walletState, navigateToExternalConnection = { @@ -658,7 +658,7 @@ private fun RootNavHost( ) } composableWithDefaultTransitions { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState() + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() FundingScreen( @@ -796,7 +796,7 @@ private fun NavGraphBuilder.home( SavingsWalletScreen( isGeoBlocked = isGeoBlocked, - onchainActivities = onchainActivities.orEmpty(), + onchainActivities = onchainActivities ?: persistentListOf(), onAllActivityButtonClick = { navController.navigateToAllActivity() }, onActivityItemClick = { navController.navigateToActivityItem(it) }, onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, @@ -821,7 +821,7 @@ private fun NavGraphBuilder.home( SpendingWalletScreen( channels = lightningState.channels, - lightningActivities = lightningActivities.orEmpty(), + lightningActivities = lightningActivities ?: persistentListOf(), onAllActivityButtonClick = { navController.navigateToAllActivity() }, onActivityItemClick = { navController.navigateToActivityItem(it) }, onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index e9f40bb29..bcda79c0d 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -32,6 +32,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.synonym.bitkitcore.ILspNode +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BalanceSource import org.lightningdevkit.ldknode.BestBlock @@ -104,7 +107,7 @@ fun NodeInfoScreen( private fun Content( lightningState: LightningState, isRefreshing: Boolean = false, - peers: List = emptyList(), + peers: ImmutableList = persistentListOf(), onBack: () -> Unit = {}, onRefresh: () -> Unit = {}, onDisconnectPeer: (PeerDetails) -> Unit = {}, @@ -137,7 +140,7 @@ private fun Content( WalletBalancesSection(balanceDetails = details) if (details.lightningBalances.isNotEmpty()) { - LightningBalancesSection(balances = details.lightningBalances) + LightningBalancesSection(balances = details.lightningBalances.toImmutableList()) } } if (lightningState.channels.isNotEmpty()) { @@ -266,7 +269,7 @@ private fun WalletBalancesSection(balanceDetails: BalanceDetails) { } @Composable -private fun LightningBalancesSection(balances: List) { +private fun LightningBalancesSection(balances: ImmutableList) { Column(modifier = Modifier.fillMaxWidth()) { SectionHeader(stringResource(R.string.lightning__lightning_balances)) balances.forEach { balance -> @@ -306,7 +309,7 @@ private fun LightningBalancesSection(balances: List) { @Composable private fun ChannelsSection( - channels: List, + channels: ImmutableList, onCopy: (String) -> Unit = {}, ) { Column(modifier = Modifier.fillMaxWidth()) { @@ -383,7 +386,7 @@ private fun ChannelsSection( @Composable private fun PeersSection( - peers: List, + peers: ImmutableList, onDisconnectPeer: (PeerDetails) -> Unit = {}, onCopy: (String) -> Unit = {}, ) { @@ -466,7 +469,7 @@ private fun ChannelDetailRow( } } -private fun previewPeers() = listOf( +private fun previewPeers() = persistentListOf( NodePeer( peerDetails = Peers.stag, lspNode = ILspNode( @@ -524,7 +527,7 @@ private fun Preview() { latestPathfindingScoresSyncTimestamp = null, ), nodeId = "0348a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9", - peers = listOf(Peers.stag), + peers = listOf(Peers.stag).toImmutableList(), channels = listOf( createChannelDetails().copy( channelId = "abc123def456789012345678901234567890123456789012345678901234567890", @@ -553,7 +556,7 @@ private fun Preview() { inboundHtlcMinimumMsat = 1000UL, inboundHtlcMaximumMsat = 200000000UL, ), - ), + ).toImmutableList(), balances = BalanceDetails( totalOnchainBalanceSats = 1000000UL, spendableOnchainBalanceSats = 900000UL, diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoViewModel.kt b/app/src/main/java/to/bitkit/ui/NodeInfoViewModel.kt index ed8cd88f0..46b40e4b3 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoViewModel.kt @@ -5,6 +5,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -29,7 +32,7 @@ class NodeInfoViewModel @Inject constructor( blocktankRepo: BlocktankRepo, private val lightningRepo: LightningRepo, ) : ViewModel() { - val peers: StateFlow> = combine( + val peers: StateFlow> = combine( lightningRepo.lightningState.map { it.peers }, blocktankRepo.blocktankState.map { it.info?.nodes }, ) { peers, lspNodes -> @@ -39,10 +42,10 @@ class NodeInfoViewModel @Inject constructor( lspNode = lspNodes?.firstOrNull { it.pubkey == peer.nodeId }, name = Peers.Known.find(peer)?.name, ) - }.sortedBy { it.alias() } + }.sortedBy { it.alias() }.toImmutableList() } .distinctUntilChanged() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf()) fun disconnectPeer(peer: PeerDetails) { viewModelScope.launch { diff --git a/app/src/main/java/to/bitkit/ui/components/MnemonicWordsGrid.kt b/app/src/main/java/to/bitkit/ui/components/MnemonicWordsGrid.kt index 8ac9fd1b5..25d906bf6 100644 --- a/app/src/main/java/to/bitkit/ui/components/MnemonicWordsGrid.kt +++ b/app/src/main/java/to/bitkit/ui/components/MnemonicWordsGrid.kt @@ -21,12 +21,14 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.blur import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun MnemonicWordsGrid( - actualWords: List, + actualWords: ImmutableList, showMnemonic: Boolean, modifier: Modifier = Modifier, blurDurationMs: Int = 800, @@ -97,7 +99,7 @@ private fun WordItem( } } -private val previewWords = List(8) { "word${it + 1}" } +private val previewWords = List(8) { "word${it + 1}" }.toImmutableList() @Preview @Composable diff --git a/app/src/main/java/to/bitkit/ui/components/Slider.kt b/app/src/main/java/to/bitkit/ui/components/Slider.kt index e1b5d9749..39229c354 100644 --- a/app/src/main/java/to/bitkit/ui/components/Slider.kt +++ b/app/src/main/java/to/bitkit/ui/components/Slider.kt @@ -35,6 +35,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -50,7 +52,7 @@ private const val STEP_MARKER_HEIGHT_DP = 16 @Composable fun StepSlider( value: Int, - steps: List, + steps: ImmutableList, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -246,7 +248,7 @@ private fun Preview() { Column(modifier = Modifier.padding(32.dp)) { StepSlider( value = value, - steps = listOf(1, 5, 10, 20, 50), + steps = persistentListOf(1, 5, 10, 20, 50), onValueChange = { value = it }, ) } @@ -260,7 +262,7 @@ private fun Preview2() { Column(modifier = Modifier.padding(32.dp)) { StepSlider( value = 5, - steps = listOf(1, 2, 5, 10), + steps = persistentListOf(1, 2, 5, 10), onValueChange = {}, ) } diff --git a/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt b/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt index 321947503..f58f18439 100644 --- a/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/onboarding/RestoreWalletScreen.kt @@ -55,6 +55,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS @@ -311,7 +313,7 @@ private fun Content( @Composable private fun BoxScope.SuggestionsRow( - suggestions: List, + suggestions: ImmutableList, onSelect: (String) -> Unit, ) { AnimatedVisibility( @@ -452,7 +454,7 @@ private fun PreviewValid() { AppThemeSurface { Content( uiState = RestoreWalletUiState( - words = List(12) { if (it % 2 == 0) "abandon" else "ability" }, + words = List(12) { if (it % 2 == 0) "abandon" else "ability" }.toImmutableList(), is24Words = false, ), checksumErrorVisible = false, @@ -467,7 +469,7 @@ private fun PreviewInvalid() { AppThemeSurface { Content( uiState = RestoreWalletUiState( - words = List(12) { if (it % 2 == 0) "rock" else "roll" }, + words = List(12) { if (it % 2 == 0) "rock" else "roll" }.toImmutableList(), is24Words = false, ), checksumErrorVisible = true, @@ -484,7 +486,7 @@ private fun Preview24Words() { Content( uiState = RestoreWalletUiState( is24Words = true, - words = List(24) { "word${it + 1}" } + words = List(24) { "word${it + 1}" }.toImmutableList() ), checksumErrorVisible = false, areButtonsEnabled = false, diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt index 23087bc99..6c18473ad 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,6 +20,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.persistentListOf import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.FillHeight @@ -41,7 +42,7 @@ fun RecoveryMnemonicScreen( ) { BlockScreenshots() - val uiState by recoveryMnemonicViewModel.uiState.collectAsState() + val uiState by recoveryMnemonicViewModel.uiState.collectAsStateWithLifecycle() Content( uiState = uiState, @@ -161,7 +162,7 @@ private fun ContentPreview12Words() { Content( uiState = RecoveryMnemonicUiState( isLoading = false, - mnemonicWords = listOf( + mnemonicWords = persistentListOf( "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", ), @@ -179,7 +180,7 @@ private fun ContentPreview24Words() { Content( uiState = RecoveryMnemonicUiState( isLoading = false, - mnemonicWords = listOf( + mnemonicWords = persistentListOf( "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index fd53d23a9..1cc5b04a8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -1,10 +1,14 @@ package to.bitkit.ui.screens.recovery import android.content.Context +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -50,7 +54,7 @@ class RecoveryMnemonicViewModel @Inject constructor( return@launch } - val mnemonicWords = mnemonic.split(" ").filter { it.isNotBlank() } + val mnemonicWords = mnemonic.split(" ").filter { it.isNotBlank() }.toImmutableList() _uiState.update { it.copy( @@ -76,8 +80,9 @@ class RecoveryMnemonicViewModel @Inject constructor( } } +@Immutable data class RecoveryMnemonicUiState( val isLoading: Boolean = true, - val mnemonicWords: List = emptyList(), + val mnemonicWords: ImmutableList = persistentListOf(), val passphrase: String = "", ) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/VssDebugScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/VssDebugScreen.kt index 1a9709403..f16e9e5f8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/VssDebugScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/VssDebugScreen.kt @@ -36,6 +36,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.synonym.vssclient.LdkNamespace +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.env.Env import to.bitkit.models.BackupCategory @@ -117,7 +118,7 @@ private fun VssDebugContent( .verticalScroll(rememberScrollState()) ) { CustomTabRowWithSpacing( - tabs = VssTab.entries, + tabs = VssTab.entries.toImmutableList(), currentTabIndex = selectedVssTab, onTabChange = { selectedVssTab = it.ordinal }, ) @@ -320,7 +321,7 @@ private fun PreviewApp() { com.synonym.vssclient.KeyVersion(key.name, (i + 1).toLong()) } var uiState by remember { - mutableStateOf(VssDebugUiState(vssKeys = vssKeys)) + mutableStateOf(VssDebugUiState(vssKeys = vssKeys.toImmutableList())) } AppThemeSurface { @@ -328,7 +329,7 @@ private fun PreviewApp() { uiState = uiState, onBackClick = {}, onRefresh = {}, - onListVssKeys = { uiState = uiState.copy(vssKeys = vssKeys) }, + onListVssKeys = { uiState = uiState.copy(vssKeys = vssKeys.toImmutableList()) }, onDeleteVssKey = {}, onDeleteAllVssKeys = {}, onListVssLdkKeys = {}, @@ -360,7 +361,7 @@ private fun PreviewLdk() { ), ) var uiState by remember { - mutableStateOf(VssDebugUiState(vssLdkKeys = vssLdkKeys)) + mutableStateOf(VssDebugUiState(vssLdkKeys = vssLdkKeys.toImmutableList())) } AppThemeSurface { @@ -371,7 +372,7 @@ private fun PreviewLdk() { onListVssKeys = {}, onDeleteVssKey = {}, onDeleteAllVssKeys = {}, - onListVssLdkKeys = { uiState = uiState.copy(vssLdkKeys = vssLdkKeys) }, + onListVssLdkKeys = { uiState = uiState.copy(vssLdkKeys = vssLdkKeys.toImmutableList()) }, onDeleteVssLdkKey = { _, _ -> }, onShareVssLdkKey = { _, _, _ -> }, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt index b72503af6..c0ab49e83 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Switch import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,6 +25,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ext.amountOnClose import to.bitkit.ext.filterOpen @@ -78,7 +82,7 @@ fun SavingsAdvancedScreen( balance = it.amountOnClose, isSelected = selectedChannelIds.contains(it.channelId), ) - } + }.toImmutableList() } SavingsAdvancedContent( @@ -97,7 +101,7 @@ fun SavingsAdvancedScreen( @Composable private fun SavingsAdvancedContent( - channelItems: List, + channelItems: ImmutableList, onChannelItemClick: (String) -> Unit = {}, onAmountClick: () -> Unit = {}, onContinueClick: () -> Unit = {}, @@ -192,6 +196,7 @@ fun ChannelItem( } } +@Immutable private data class TransferChannelUiState( val channelId: String, val balance: ULong, @@ -203,7 +208,7 @@ private data class TransferChannelUiState( private fun SavingsAdvancedScreenPreview() { AppThemeSurface { SavingsAdvancedContent( - channelItems = listOf( + channelItems = persistentListOf( TransferChannelUiState( channelId = "channelId_1", balance = 45_000u, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt index eacc494f9..f0a120c3a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.regtestMine +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import org.lightningdevkit.ldknode.Network import to.bitkit.R @@ -141,7 +142,7 @@ private fun SettingUpScreen( modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.height(16.dp)) - val steps = listOf( + val steps = persistentListOf( stringResource(R.string.lightning__setting_up_step1), stringResource(R.string.lightning__setting_up_step2), stringResource(R.string.lightning__setting_up_step3), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/components/ProgressSteps.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/components/ProgressSteps.kt index b3c53885d..776ab54fd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/components/ProgressSteps.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/components/ProgressSteps.kt @@ -26,13 +26,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import to.bitkit.ui.components.BodySSB import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun ProgressSteps( - steps: List, + steps: ImmutableList, activeStepIndex: Int, modifier: Modifier = Modifier, ) { @@ -113,7 +115,7 @@ fun ProgressSteps( @Composable private fun ProgressStepsPreview() { AppThemeSurface { - val steps = listOf( + val steps = persistentListOf( "Step 1", "Step 2", "Step 3", diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 1543b821d..ed57e674f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -62,6 +62,8 @@ import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import to.bitkit.R @@ -271,7 +273,7 @@ private fun Content( walletNavController: NavController, drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), - latestActivities: List?, + latestActivities: ImmutableList?, onRefresh: () -> Unit = {}, onRemoveSuggestion: (Suggestion) -> Unit = {}, onClickSuggestion: (Suggestion) -> Unit = {}, @@ -680,7 +682,7 @@ private fun Preview() { rootNavController = rememberNavController(), walletNavController = rememberNavController(), drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), - latestActivities = previewActivityItems.take(3), + latestActivities = previewActivityItems.take(3).toImmutableList(), balances = BalanceState( totalOnchainSats = 165_000u, totalLightningSats = 45_000u, @@ -704,7 +706,7 @@ private fun PreviewEmpty() { rootNavController = rememberNavController(), walletNavController = rememberNavController(), drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), - latestActivities = previewActivityItems.take(3), + latestActivities = previewActivityItems.take(3).toImmutableList(), balances = BalanceState() ) TabBar() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt index bebb01e6e..9eb075421 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt @@ -1,6 +1,8 @@ package to.bitkit.ui.screens.wallets import androidx.compose.runtime.Stable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import to.bitkit.data.dto.price.PriceDTO import to.bitkit.models.BannerItem import to.bitkit.models.Suggestion @@ -17,16 +19,16 @@ import to.bitkit.ui.screens.widgets.blocks.WeatherModel @Stable data class HomeUiState( - val suggestions: List = listOf(), - val banners: List = listOf(), + val suggestions: ImmutableList = persistentListOf(), + val banners: ImmutableList = persistentListOf(), val showWidgets: Boolean = false, val showWidgetTitles: Boolean = false, - val widgetsWithPosition: List = emptyList(), + val widgetsWithPosition: ImmutableList = persistentListOf(), val headlinePreferences: HeadlinePreferences = HeadlinePreferences(), val currentArticle: ArticleModel? = null, val currentFact: String? = null, val factsPreferences: FactsPreferences = FactsPreferences(), - val facts: List = listOf(), + val facts: ImmutableList = persistentListOf(), val blocksPreferences: BlocksPreferences = BlocksPreferences(), val currentBlock: BlockModel? = null, val weatherPreferences: WeatherPreferences = WeatherPreferences(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt index a2e6e563d..6c91d5e65 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -62,10 +63,10 @@ class HomeViewModel @Inject constructor( _currentFact, ) { suggestions, settings, widgetsData, currentArticle, currentFact -> _uiState.value.copy( - suggestions = suggestions, + suggestions = suggestions.toImmutableList(), showWidgets = settings.showWidgets, showWidgetTitles = settings.showWidgetTitles, - widgetsWithPosition = widgetsData.widgets, + widgetsWithPosition = widgetsData.widgets.toImmutableList(), headlinePreferences = widgetsData.headlinePreferences, factsPreferences = widgetsData.factsPreferences, blocksPreferences = widgetsData.blocksPreferences, @@ -176,7 +177,7 @@ class HomeViewModel @Inject constructor( widget.copy(position = index) } - _uiState.update { it.copy(widgetsWithPosition = updatedWidgets) } + _uiState.update { it.copy(widgetsWithPosition = updatedWidgets.toImmutableList()) } } } @@ -240,7 +241,7 @@ class HomeViewModel @Inject constructor( ).takeIf { balanceState.balanceInTransferToSavings > 0uL }, ) }.collect { banners -> - _uiState.update { it.copy(banners = banners) } + _uiState.update { it.copy(banners = banners.toImmutableList()) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt index 7b7e32433..0d85aded5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt @@ -26,6 +26,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.models.BalanceState import to.bitkit.ui.LocalBalances @@ -46,7 +49,7 @@ import to.bitkit.ui.utils.withAccent @Composable fun SavingsWalletScreen( isGeoBlocked: Boolean, - onchainActivities: List, + onchainActivities: ImmutableList, onAllActivityButtonClick: () -> Unit, onEmptyActivityRowClick: () -> Unit, onActivityItemClick: (String) -> Unit, @@ -154,7 +157,7 @@ private fun Preview() { Box { SavingsWalletScreen( isGeoBlocked = false, - onchainActivities = previewOnchainActivityItems(), + onchainActivities = previewOnchainActivityItems().toImmutableList(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -174,7 +177,7 @@ private fun PreviewTransfer() { Box { SavingsWalletScreen( isGeoBlocked = false, - onchainActivities = previewOnchainActivityItems(), + onchainActivities = previewOnchainActivityItems().toImmutableList(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -197,7 +200,7 @@ private fun PreviewNoActivity() { Box { SavingsWalletScreen( isGeoBlocked = false, - onchainActivities = emptyList(), + onchainActivities = persistentListOf(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -217,7 +220,7 @@ private fun PreviewGeoBlocked() { Box { SavingsWalletScreen( isGeoBlocked = true, - onchainActivities = previewOnchainActivityItems(), + onchainActivities = previewOnchainActivityItems().toImmutableList(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -237,7 +240,7 @@ private fun PreviewEmpty() { Box { SavingsWalletScreen( isGeoBlocked = false, - onchainActivities = emptyList(), + onchainActivities = persistentListOf(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index 5aeac50d9..127b72b02 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -26,6 +26,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R import to.bitkit.ext.createChannelDetails @@ -47,8 +50,8 @@ import to.bitkit.ui.utils.withAccent @Composable fun SpendingWalletScreen( - channels: List, - lightningActivities: List, + channels: ImmutableList, + lightningActivities: ImmutableList, onAllActivityButtonClick: () -> Unit, onActivityItemClick: (String) -> Unit, onEmptyActivityRowClick: () -> Unit, @@ -153,8 +156,8 @@ private fun Preview() { AppThemeSurface { Box { SpendingWalletScreen( - channels = listOf(createChannelDetails()), - lightningActivities = previewLightningActivityItems(), + channels = persistentListOf(createChannelDetails()), + lightningActivities = previewLightningActivityItems().toImmutableList(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -173,8 +176,8 @@ private fun PreviewTransfer() { AppThemeSurface { Box { SpendingWalletScreen( - channels = listOf(createChannelDetails()), - lightningActivities = previewLightningActivityItems(), + channels = persistentListOf(createChannelDetails()), + lightningActivities = previewLightningActivityItems().toImmutableList(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -196,8 +199,8 @@ private fun PreviewNoActivity() { AppThemeSurface { Box { SpendingWalletScreen( - channels = listOf(createChannelDetails()), - lightningActivities = emptyList(), + channels = persistentListOf(createChannelDetails()), + lightningActivities = persistentListOf(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -216,8 +219,8 @@ private fun PreviewEmpty() { AppThemeSurface { Box { SpendingWalletScreen( - channels = emptyList(), - lightningActivities = emptyList(), + channels = persistentListOf(), + lightningActivities = persistentListOf(), onAllActivityButtonClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index b0d1f7233..7d5cd7267 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -48,6 +48,12 @@ import com.synonym.bitkitcore.LightningActivity import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import to.bitkit.R import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle @@ -177,26 +183,26 @@ fun ActivityDetailScreen( var showAddTagSheet by remember { mutableStateOf(false) } var showAssignSheet by remember { mutableStateOf(false) } var isCpfpChild by remember { mutableStateOf(false) } - var boostTxDoesExist by remember { mutableStateOf>(emptyMap()) } + var boostTxDoesExist by remember { mutableStateOf>(persistentMapOf()) } LaunchedEffect(item) { if (item is Activity.Onchain) { isCpfpChild = detailViewModel.isCpfpChildTransaction(item.v1.txId) boostTxDoesExist = if (item.v1.boostTxIds.isNotEmpty()) { - detailViewModel.getBoostTxDoesExist(item.v1.boostTxIds) + detailViewModel.getBoostTxDoesExist(item.v1.boostTxIds).toImmutableMap() } else { - emptyMap() + persistentMapOf() } } else { isCpfpChild = false - boostTxDoesExist = emptyMap() + boostTxDoesExist = persistentMapOf() } } // Update boostTxDoesExist when boostTxIds change LaunchedEffect(if (item is Activity.Onchain) item.v1.boostTxIds else emptyList()) { if (item is Activity.Onchain && item.v1.boostTxIds.isNotEmpty()) { - boostTxDoesExist = detailViewModel.getBoostTxDoesExist(item.v1.boostTxIds) + boostTxDoesExist = detailViewModel.getBoostTxDoesExist(item.v1.boostTxIds).toImmutableMap() } } @@ -222,7 +228,7 @@ fun ActivityDetailScreen( ) ActivityDetailContent( item = item, - tags = tags, + tags = tags.toImmutableList(), onRemoveTag = { detailViewModel.removeTag(it) }, onAddTagClick = { showAddTagSheet = true }, onAssignClick = { showAssignSheet = true }, @@ -308,7 +314,7 @@ fun ActivityDetailScreen( @Composable private fun ActivityDetailContent( item: Activity, - tags: List, + tags: ImmutableList, onRemoveTag: (String) -> Unit, onAddTagClick: () -> Unit, onAssignClick: () -> Unit, @@ -317,7 +323,7 @@ private fun ActivityDetailContent( onChannelClick: ((String) -> Unit)?, detailViewModel: ActivityDetailViewModel? = null, isCpfpChild: Boolean = false, - boostTxDoesExist: Map = emptyMap(), + boostTxDoesExist: ImmutableMap = persistentMapOf(), onCopy: (String) -> Unit, hideBalance: Boolean = false, feeRates: FeeRates? = null, @@ -885,7 +891,7 @@ private fun PreviewLightningSent() { message = "Thanks for paying at the bar. Here's my share.", ) ), - tags = listOf("Lunch", "Drinks"), + tags = persistentListOf("Lunch", "Drinks"), onRemoveTag = {}, onAddTagClick = {}, onAssignClick = {}, @@ -916,7 +922,7 @@ private fun PreviewOnchain() { confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), ) ), - tags = emptyList(), + tags = persistentListOf(), onRemoveTag = {}, onAddTagClick = {}, onAssignClick = {}, @@ -948,7 +954,7 @@ private fun PreviewSheetSmallScreen() { message = "Thanks for paying at the bar. Here's my share.", ) ), - tags = listOf("Lunch", "Drinks"), + tags = persistentListOf("Lunch", "Drinks"), onRemoveTag = {}, onAddTagClick = {}, onAssignClick = {}, @@ -966,7 +972,7 @@ private fun PreviewSheetSmallScreen() { private fun shouldEnableBoostButton( item: Activity, isCpfpChild: Boolean, - boostTxDoesExist: Map, + boostTxDoesExist: ImmutableMap, ): Boolean { if (item !is Activity.Onchain) return false @@ -986,7 +992,7 @@ private fun shouldEnableBoostButton( @Composable private fun isBoostCompleted( activity: OnchainActivity, - boostTxDoesExist: Map, + boostTxDoesExist: ImmutableMap, ): Boolean { if (activity.boostTxIds.isEmpty()) return true diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index f6b8e16f0..286d4b3a9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -40,6 +40,7 @@ import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentState import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.TransactionDetails +import kotlinx.collections.immutable.persistentMapOf import to.bitkit.R import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle @@ -298,7 +299,7 @@ private fun ColumnScope.OnchainDetails( ) if (txDetails != null) { Section( - title = localizedPlural(R.string.wallet__activity_input, mapOf("count" to txDetails.inputs.size)), + title = localizedPlural(R.string.wallet__activity_input, persistentMapOf("count" to txDetails.inputs.size)), valueContent = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { txDetails.inputs.forEach { input -> @@ -309,7 +310,10 @@ private fun ColumnScope.OnchainDetails( }, ) Section( - title = localizedPlural(R.string.wallet__activity_output, mapOf("count" to txDetails.outputs.size)), + title = localizedPlural( + R.string.wallet__activity_output, + persistentMapOf("count" to txDetails.outputs.size), + ), valueContent = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { txDetails.outputs.forEach { output -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt index a6558b341..5a1439e98 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -19,6 +20,12 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import to.bitkit.R import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Sheet @@ -48,7 +55,7 @@ fun AllActivityScreen( val startDate by viewModel.startDate.collectAsStateWithLifecycle() val selectedTab by viewModel.selectedTab.collectAsStateWithLifecycle() - val tabs = ActivityTab.entries + val tabs = remember { ActivityTab.entries.toImmutableList() } val currentTabIndex = tabs.indexOf(selectedTab) BackHandler { onBack() } @@ -58,7 +65,7 @@ fun AllActivityScreen( searchText = searchText, onSearchTextChange = { viewModel.setSearchText(it) }, hasTagFilter = selectedTags.isNotEmpty(), - selectedTags = selectedTags, + selectedTags = selectedTags.toImmutableSet(), hasDateRangeFilter = startDate != null, tabs = tabs, currentTabIndex = currentTabIndex, @@ -75,13 +82,13 @@ fun AllActivityScreen( @Composable @OptIn(ExperimentalHazeMaterialsApi::class) private fun AllActivityScreenContent( - filteredActivities: List?, + filteredActivities: ImmutableList?, searchText: String, onSearchTextChange: (String) -> Unit, hasTagFilter: Boolean, - selectedTags: Set, + selectedTags: ImmutableSet, hasDateRangeFilter: Boolean, - tabs: List, + tabs: ImmutableList, currentTabIndex: Int, onRemoveTag: (String) -> Unit, onTabChange: (Int) -> Unit, @@ -147,13 +154,13 @@ private fun AllActivityScreenContent( private fun Preview() { AppThemeSurface { AllActivityScreenContent( - filteredActivities = previewActivityItems, + filteredActivities = previewActivityItems.toImmutableList(), searchText = "", onSearchTextChange = {}, hasTagFilter = false, - selectedTags = setOf(), + selectedTags = persistentSetOf(), hasDateRangeFilter = false, - tabs = ActivityTab.entries, + tabs = ActivityTab.entries.toImmutableList(), currentTabIndex = 0, onTabChange = {}, onBackClick = {}, @@ -171,13 +178,13 @@ private fun Preview() { private fun PreviewEmpty() { AppThemeSurface { AllActivityScreenContent( - filteredActivities = emptyList(), + filteredActivities = persistentListOf(), searchText = "", onSearchTextChange = {}, hasTagFilter = false, - selectedTags = setOf("tag1", "tag2"), + selectedTags = persistentSetOf("tag1", "tag2"), hasDateRangeFilter = false, - tabs = ActivityTab.entries, + tabs = ActivityTab.entries.toImmutableList(), currentTabIndex = 0, onTabChange = {}, onBackClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt index 3d178f85a..7b0bf254d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt @@ -18,6 +18,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet import to.bitkit.R import to.bitkit.ui.activityListViewModel import to.bitkit.ui.appViewModel @@ -44,7 +49,7 @@ fun TagSelectorSheet() { Content( availableTags = availableTags, - selectedTags = selectedTags, + selectedTags = selectedTags.toImmutableSet(), onTagClick = { activity.toggleTag(it) app.hideSheet() @@ -54,8 +59,8 @@ fun TagSelectorSheet() { @Composable private fun Content( - availableTags: List, - selectedTags: Set, + availableTags: ImmutableList, + selectedTags: ImmutableSet, onTagClick: (String) -> Unit = {}, ) { Column( @@ -103,8 +108,8 @@ private fun Preview() { AppThemeSurface { BottomSheetPreview { Content( - availableTags = listOf("Bitcoin", "Lightning", "Sent", "Received"), - selectedTags = setOf("Bitcoin", "Received"), + availableTags = persistentListOf("Bitcoin", "Lightning", "Sent", "Received"), + selectedTags = persistentSetOf("Bitcoin", "Received"), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt index d78bbbb46..dbd6f64ac 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt @@ -19,6 +19,10 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ui.components.SearchInput import to.bitkit.ui.components.SearchInputIconButton @@ -33,11 +37,11 @@ fun ActivityListFilter( hasDateRangeFilter: Boolean, onTagClick: () -> Unit, onDateRangeClick: () -> Unit, - tabs: List, + tabs: ImmutableList, currentTabIndex: Int, onTabChange: (ActivityTab) -> Unit, modifier: Modifier = Modifier, - selectedTags: Set = emptySet(), + selectedTags: ImmutableSet = persistentSetOf(), onRemoveTag: (String) -> Unit = {}, ) { val focusManager = LocalFocusManager.current @@ -124,7 +128,7 @@ private fun Preview() { onTagClick = {}, hasDateRangeFilter = false, onDateRangeClick = {}, - tabs = ActivityTab.entries, + tabs = ActivityTab.entries.toImmutableList(), currentTabIndex = 0, onTabChange = {}, modifier = Modifier.padding(16.dp) @@ -143,10 +147,10 @@ private fun PreviewWithTags() { onTagClick = {}, hasDateRangeFilter = false, onDateRangeClick = {}, - tabs = ActivityTab.entries, + tabs = ActivityTab.entries.toImmutableList(), currentTabIndex = 0, onTabChange = {}, - selectedTags = setOf("Tag1", "Tag2"), + selectedTags = persistentSetOf("Tag1", "Tag2"), modifier = Modifier.padding(16.dp) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt index 06cf97711..e4637ef28 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt @@ -18,6 +18,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ext.rawId import to.bitkit.ui.components.BodyM @@ -36,7 +39,7 @@ import java.util.Locale @Composable fun ActivityListGrouped( - items: List?, + items: ImmutableList?, onActivityItemClick: (String) -> Unit, onEmptyActivityRowClick: () -> Unit, modifier: Modifier = Modifier, @@ -204,7 +207,7 @@ private fun Preview() { AppThemeSurface { Column(modifier = Modifier.padding(horizontal = 16.dp)) { ActivityListGrouped( - items = previewActivityItems, + items = previewActivityItems.toImmutableList(), onActivityItemClick = {}, onEmptyActivityRowClick = {}, ) @@ -217,7 +220,7 @@ private fun Preview() { private fun PreviewEmpty() { AppThemeSurface { ActivityListGrouped( - items = emptyList(), + items = persistentListOf(), onActivityItemClick = {}, onEmptyActivityRowClick = {}, ) @@ -229,7 +232,7 @@ private fun PreviewEmpty() { private fun PreviewEmptyWithFooter() { AppThemeSurface { ActivityListGrouped( - items = emptyList(), + items = persistentListOf(), onActivityItemClick = {}, onEmptyActivityRowClick = {}, showFooter = true, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt index cc476b42a..225032df2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt @@ -12,6 +12,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.synonym.bitkitcore.Activity +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ui.components.TertiaryButton import to.bitkit.ui.components.VerticalSpacer @@ -20,7 +23,7 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun ActivityListSimple( - items: List?, + items: ImmutableList?, onAllActivityClick: () -> Unit, onActivityItemClick: (String) -> Unit, onEmptyActivityRowClick: () -> Unit, @@ -53,7 +56,7 @@ fun ActivityListSimple( private fun Preview() { AppThemeSurface { ActivityListSimple( - items = previewActivityItems, + items = previewActivityItems.toImmutableList(), onAllActivityClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, @@ -66,7 +69,7 @@ private fun Preview() { private fun PreviewEmpty() { AppThemeSurface { ActivityListSimple( - items = emptyList(), + items = persistentListOf(), onAllActivityClick = {}, onActivityItemClick = {}, onEmptyActivityRowClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 55a1b4fd9..1c20627a0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -22,13 +22,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList import to.bitkit.ui.components.CaptionB import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.Colors @Composable fun CustomTabRowWithSpacing( - tabs: List, + tabs: ImmutableList, currentTabIndex: Int, onTabChange: (T) -> Unit, modifier: Modifier = Modifier, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index f77c53a47..860f0e51b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -40,6 +40,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import to.bitkit.R import to.bitkit.repositories.CurrencyState import to.bitkit.repositories.WalletState @@ -153,7 +155,7 @@ fun EditInvoiceContent( noteText: String, isSoftKeyboardVisible: Boolean, keyboardVisible: Boolean, - tags: List, + tags: ImmutableList, onBack: () -> Unit, onContinueKeyboard: () -> Unit, onClickBalance: () -> Unit, @@ -361,7 +363,7 @@ private fun Preview() { onClickBalance = {}, onContinueGeneral = {}, onContinueKeyboard = {}, - tags = listOf(), + tags = persistentListOf(), onClickAddTag = {}, onClickTag = {}, isSoftKeyboardVisible = false, @@ -385,7 +387,7 @@ private fun PreviewWithTags() { onClickBalance = {}, onContinueGeneral = {}, onContinueKeyboard = {}, - tags = listOf("Team", "Dinner", "Home", "Work"), + tags = persistentListOf("Team", "Dinner", "Home", "Work"), onClickAddTag = {}, onClickTag = {}, isSoftKeyboardVisible = false, @@ -409,7 +411,7 @@ private fun PreviewWithKeyboard() { onClickBalance = {}, onContinueGeneral = {}, onContinueKeyboard = {}, - tags = listOf("Team", "Dinner", "Home"), + tags = persistentListOf("Team", "Dinner", "Home"), onClickAddTag = {}, onClickTag = {}, isSoftKeyboardVisible = false, @@ -433,7 +435,7 @@ private fun PreviewSmallScreen() { onClickBalance = {}, onContinueGeneral = {}, onContinueKeyboard = {}, - tags = listOf("Team", "Dinner", "Home"), + tags = persistentListOf("Team", "Dinner", "Home"), onClickAddTag = {}, onClickTag = {}, isSoftKeyboardVisible = false, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index a35a59b6f..9d4325b18 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -48,6 +48,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -106,7 +108,7 @@ fun ReceiveQrScreen( add(ReceiveTab.AUTO) } add(ReceiveTab.SPENDING) - } + }.toImmutableList() } val invoicesByTab = remember( @@ -657,7 +659,7 @@ private fun PreviewSavingsMode() { ), lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = emptyList() + channels = persistentListOf() ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), @@ -729,7 +731,7 @@ private fun PreviewAutoMode() { ), lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = listOf(mockChannel), + channels = listOf(mockChannel).toImmutableList(), ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), @@ -796,7 +798,7 @@ private fun PreviewSpendingMode() { ), lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = listOf(mockChannel), + channels = listOf(mockChannel).toImmutableList(), ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt index 1de166d58..585d9c64d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.ui.components.BottomSheetPreview @@ -156,7 +157,7 @@ private fun Preview() { BottomSheetPreview { AddTagContent( uiState = AddTagUiState( - tagsSuggestions = listOf("Lunch", "Mom", "Dad", "Dinner", "Tip", "Gift") + tagsSuggestions = listOf("Lunch", "Mom", "Dad", "Dinner", "Tip", "Gift").toImmutableList() ), onTagSelected = {}, onInputUpdated = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt index c27f20d2d..ffd725a7f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionScreen.kt @@ -26,6 +26,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList import org.lightningdevkit.ldknode.OutPoint import org.lightningdevkit.ldknode.SpendableUtxo import to.bitkit.R @@ -85,7 +90,7 @@ fun SendCoinSelectionScreen( private fun Content( uiState: CoinSelectionUiState, modifier: Modifier = Modifier, - tagsByTxId: Map> = emptyMap(), + tagsByTxId: ImmutableMap> = persistentMapOf(), onBack: () -> Unit = {}, onContinue: () -> Unit = {}, onClickAuto: () -> Unit = {}, @@ -133,7 +138,7 @@ private fun Content( UtxoRow( utxo = utxo, isSelected = uiState.selectedUtxos.any { it.outpoint == utxo.outpoint }, - tags = tagsByTxId[utxo.outpoint.txid] ?: emptyList(), + tags = tagsByTxId[utxo.outpoint.txid] ?: persistentListOf(), onTap = { onClickUtxo(utxo) }, onRender = onRenderUtxo, ) @@ -183,7 +188,7 @@ private fun Content( private fun UtxoRow( utxo: SpendableUtxo, isSelected: Boolean, - tags: List, + tags: ImmutableList, onTap: () -> Unit, onRender: (String) -> Unit = {}, ) { @@ -251,18 +256,18 @@ private fun Preview() { SpendableUtxo(outpoint = OutPoint(txid = "abc123", vout = 0u), valueSats = 50000uL), SpendableUtxo(outpoint = OutPoint(txid = "def456", vout = 1u), valueSats = 25000uL), SpendableUtxo(outpoint = OutPoint(txid = "ghi789", vout = 0u), valueSats = 10000uL) - ), + ).toImmutableList(), selectedUtxos = listOf( SpendableUtxo(outpoint = OutPoint(txid = "abc123", vout = 0u), valueSats = 50000uL), - ), + ).toImmutableList(), autoSelectCoinsOn = false, totalRequiredSat = 30000uL, totalSelectedSat = 50000uL, isSelectionValid = true, ), - tagsByTxId = mapOf( - "abc123" to listOf("coffee", "work"), - "def456" to listOf("shopping", "groceries", "food"), + tagsByTxId = persistentMapOf( + "abc123" to persistentListOf("coffee", "work"), + "def456" to persistentListOf("shopping", "groceries", "food"), ), modifier = Modifier.sheetHeight(), ) @@ -277,13 +282,13 @@ private fun PreviewAuto() { BottomSheetPreview { Content( uiState = CoinSelectionUiState( - availableUtxos = emptyList(), + availableUtxos = persistentListOf(), autoSelectCoinsOn = true, totalRequiredSat = 1000uL, totalSelectedSat = 0uL, isSelectionValid = false ), - tagsByTxId = emptyMap(), + tagsByTxId = persistentMapOf(), modifier = Modifier.sheetHeight(), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index 851027e26..0d771fb13 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -1,10 +1,17 @@ package to.bitkit.ui.screens.wallets.send +import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.Activity.Onchain import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -33,7 +40,7 @@ class SendCoinSelectionViewModel @Inject constructor( private val _uiState = MutableStateFlow(CoinSelectionUiState()) val uiState = _uiState.asStateFlow() - private val _tagsByTxId = MutableStateFlow>>(emptyMap()) + private val _tagsByTxId = MutableStateFlow>>(persistentMapOf()) val tagsByTxId = _tagsByTxId.asStateFlow() private var onchainActivities: List = emptyList() @@ -57,8 +64,8 @@ class SendCoinSelectionViewModel @Inject constructor( _uiState.update { state -> state.copy( - availableUtxos = sortedUtxos, - selectedUtxos = sortedUtxos, + availableUtxos = sortedUtxos.toImmutableList(), + selectedUtxos = sortedUtxos.toImmutableList(), autoSelectCoinsOn = true, totalRequiredSat = totalRequired, totalSelectedSat = totalSelected, @@ -82,7 +89,9 @@ class SendCoinSelectionViewModel @Inject constructor( .onSuccess { tags -> if (tags.isNotEmpty()) { // add map entry linking tags to utxo.outpoint.txid - _tagsByTxId.update { currentMap -> currentMap + (txId to tags) } + _tagsByTxId.update { + (it + (txId to tags.toImmutableList())).toImmutableMap() + } } } .onFailure { @@ -105,7 +114,7 @@ class SendCoinSelectionViewModel @Inject constructor( state.copy( autoSelectCoinsOn = true, - selectedUtxos = allSelected, + selectedUtxos = allSelected.toImmutableList(), totalSelectedSat = newTotalSat, isSelectionValid = validateCoinSelection(newTotalSat, state.totalRequiredSat) ) @@ -125,7 +134,7 @@ class SendCoinSelectionViewModel @Inject constructor( val newTotal = newSelection.sumOf { it.valueSats } state.copy( - selectedUtxos = newSelection, + selectedUtxos = newSelection.toImmutableList(), totalSelectedSat = newTotal, autoSelectCoinsOn = false, isSelectionValid = validateCoinSelection(newTotal, state.totalRequiredSat) @@ -155,9 +164,10 @@ class SendCoinSelectionViewModel @Inject constructor( } } +@Stable data class CoinSelectionUiState( - val availableUtxos: List = emptyList(), - val selectedUtxos: List = emptyList(), + val availableUtxos: ImmutableList = persistentListOf(), + val selectedUtxos: ImmutableList = persistentListOf(), val autoSelectCoinsOn: Boolean = true, val totalRequiredSat: ULong = 0u, val totalSelectedSat: ULong = 0u, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 0d369b08e..e65076a24 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -44,6 +44,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.LightningInvoice import com.synonym.bitkitcore.LnurlPayData import com.synonym.bitkitcore.NetworkType +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -609,7 +610,7 @@ private fun PreviewOnChain() { BottomSheetPreview { Content( uiState = sendUiState().copy( - selectedTags = listOf("car", "house", "uber"), + selectedTags = persistentListOf("car", "house", "uber"), fee = SendFee.OnChain(1_234), speed = TransactionSpeed.Medium, ), @@ -631,7 +632,7 @@ private fun PreviewOnChainLongFeeSmallScreen() { Content( uiState = sendUiState().copy( amount = 2_345_678u, - selectedTags = listOf("car", "house", "uber"), + selectedTags = persistentListOf("car", "house", "uber"), fee = SendFee.OnChain(654_321), speed = TransactionSpeed.Custom(12_345u), ), @@ -651,7 +652,7 @@ private fun PreviewOnChainFeeLoading() { BottomSheetPreview { Content( uiState = sendUiState().copy( - selectedTags = listOf("car", "house", "uber"), + selectedTags = persistentListOf("car", "house", "uber"), fee = null, ), isNodeRunning = true, @@ -673,7 +674,7 @@ private fun PreviewLightning() { uiState = sendUiState().copy( amount = 6_543u, payMethod = SendMethod.LIGHTNING, - selectedTags = emptyList(), + selectedTags = persistentListOf(), fee = SendFee.Lightning(43), ), isNodeRunning = true, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index 32a0633ef..fe3326848 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.persistentMapOf import to.bitkit.R import to.bitkit.models.FeeRate import to.bitkit.models.PrimaryDisplay @@ -187,7 +188,7 @@ private fun Preview() { BottomSheetPreview { Content( uiState = SendFeeUiState( - fees = mapOf( + fees = persistentMapOf( FeeRate.FAST to 4000L, FeeRate.NORMAL to 3000L, FeeRate.SLOW to 2000L, @@ -209,7 +210,7 @@ private fun PreviewCustom() { BottomSheetPreview { Content( uiState = SendFeeUiState( - fees = mapOf( + fees = persistentMapOf( FeeRate.FAST to 4000L, FeeRate.NORMAL to 3000L, FeeRate.SLOW to 2000L, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index cdd4fac8f..fd3e4cf6e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -1,10 +1,16 @@ package to.bitkit.ui.screens.wallets.send import android.content.Context +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -54,7 +60,7 @@ class SendFeeViewModel @Inject constructor( } } calculateMaxSatPerVByte() - val disabledRates = fees.filter { it.value.toULong() > maxFee }.keys.toSet() + val disabledRates = fees.filter { it.value.toULong() > maxFee }.keys.toImmutableSet() _uiState.update { it.copy( selected = selected, @@ -175,12 +181,13 @@ class SendFeeViewModel @Inject constructor( } } +@Immutable data class SendFeeUiState( - val fees: Map = emptyMap(), + val fees: ImmutableMap = persistentMapOf(), val selected: FeeRate? = null, val custom: TransactionSpeed.Custom? = null, val input: String = "", val totalFeeText: String = "", - val disabledRates: Set = emptySet(), + val disabledRates: ImmutableSet = persistentSetOf(), val shouldContinue: Boolean? = null, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/DragDropColumn.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/DragDropColumn.kt index 92b0dd4e2..1949ab5a3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/DragDropColumn.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/DragDropColumn.kt @@ -18,11 +18,12 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import kotlinx.collections.immutable.ImmutableList import to.bitkit.models.WidgetWithPosition @Composable fun DragDropColumn( - items: List, + items: ImmutableList, onMove: (Int, Int) -> Unit, modifier: Modifier = Modifier, itemContent: @Composable (WidgetWithPosition, Boolean) -> Unit, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt index d6f28fc13..92b98f47d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceEditScreen.kt @@ -28,6 +28,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import to.bitkit.R import to.bitkit.data.dto.price.Change import to.bitkit.data.dto.price.GraphPeriod @@ -84,7 +86,7 @@ fun PriceEditScreen( fun PriceEditContent( onBack: () -> Unit, priceModel: PriceDTO, - allPeriodsUsd: List, + allPeriodsUsd: ImmutableList, onClickReset: () -> Unit, onClickGraph: (GraphPeriod) -> Unit, onClickTradingPair: (TradingPair) -> Unit, @@ -321,7 +323,7 @@ private fun Preview() { ), source = "Kraken" ), - allPeriodsUsd = listOf( + allPeriodsUsd = persistentListOf( PriceWidgetData( pair = TradingPair.BTC_USD, period = GraphPeriod.ONE_DAY, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt index 0c4df4cd6..ffa2d4d31 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/price/PriceViewModel.kt @@ -3,6 +3,9 @@ package to.bitkit.ui.screens.widgets.price import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -65,9 +68,9 @@ class PriceViewModel @Inject constructor( private val _customPreferences = MutableStateFlow(PricePreferences()) val customPreferences: StateFlow = _customPreferences.asStateFlow() - private val _allPeriodsUsd = MutableStateFlow(listOf()) - val allPeriodsUsd: StateFlow> = _allPeriodsUsd.asStateFlow() - private val _allPrices = MutableStateFlow(listOf()) + private val _allPeriodsUsd = MutableStateFlow>(persistentListOf()) + val allPeriodsUsd: StateFlow> = _allPeriodsUsd.asStateFlow() + private val _allPrices = MutableStateFlow>(persistentListOf()) private val _previewPrice: MutableStateFlow = MutableStateFlow(null) val previewPrice = _previewPrice.asStateFlow() @@ -148,8 +151,8 @@ class PriceViewModel @Inject constructor( viewModelScope.launch { _isLoading.update { true } widgetsRepo.fetchAllPeriods().onSuccess { data -> - _allPrices.update { data } - _allPeriodsUsd.update { data.map { priceDTO -> priceDTO.widgets.first() } } + _allPrices.update { data.toImmutableList() } + _allPeriodsUsd.update { data.map { priceDTO -> priceDTO.widgets.first() }.toImmutableList() } _isLoading.update { false } }.onFailure { Logger.warn("collectAllPeriodPrices error. Trying again in 1 second", context = TAG) diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index 329dc6b1b..5566523c0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.toRelativeTimeString @@ -261,7 +262,7 @@ private fun Preview() { AppThemeSurface { BackupSettingsScreenContent( - uiState = BackupStatusUiState(categories = categories), + uiState = BackupStatusUiState(categories = categories.toImmutableList()), onBackupClick = {}, onResetAndRestoreClick = {}, onRetryBackup = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt index 756371b46..4b3b2a821 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -40,6 +40,9 @@ import com.synonym.bitkitcore.IBtPayment import com.synonym.bitkitcore.IDiscount import com.synonym.bitkitcore.ILspNode import com.synonym.bitkitcore.IcJitEntry +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.Routes @@ -75,8 +78,8 @@ fun ChannelOrdersScreen( LaunchedEffect(Unit) { blocktank.refreshOrders() } Content( - orders = orders, - cJitEntries = cJitEntries, + orders = orders.toImmutableList(), + cJitEntries = cJitEntries.toImmutableList(), onBack = onBackClick, onClickOrder = onOrderItemClick, onClickCjit = onCjitItemClick, @@ -85,8 +88,8 @@ fun ChannelOrdersScreen( @Composable private fun Content( - orders: List, - cJitEntries: List, + orders: ImmutableList, + cJitEntries: ImmutableList, modifier: Modifier = Modifier, onBack: () -> Unit = {}, onClickOrder: (String) -> Unit = {}, @@ -606,8 +609,8 @@ private val cjitEntry = IcJitEntry( private fun Preview() { AppThemeSurface { Content( - orders = listOf(order), - cJitEntries = listOf(cjitEntry), + orders = persistentListOf(order), + cJitEntries = persistentListOf(cjitEntry), ) } } @@ -617,8 +620,8 @@ private fun Preview() { private fun PreviewEmpty() { AppThemeSurface { Content( - orders = emptyList(), - cJitEntries = emptyList(), + orders = persistentListOf(), + cJitEntries = persistentListOf(), ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt index 9c0276207..c4fb3b320 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LanguageSettingsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.models.Language import to.bitkit.ui.components.Text13Up @@ -86,7 +87,7 @@ private fun Preview() { Content( uiState = LanguageUiState( selectedLanguage = Language.SPANISH, - languages = Language.entries + languages = Language.entries.toImmutableList() ), onBackClick = {}, onClickLanguage = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt index 0d7607cb3..307a17409 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -33,6 +32,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption @@ -48,7 +48,7 @@ fun LogsScreen( navController: NavController, viewModel: LogsViewModel = hiltViewModel(), ) { - val logs by viewModel.logs.collectAsState() + val logs by viewModel.logs.collectAsStateWithLifecycle() var showDeleteConfirmation by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -125,8 +125,8 @@ fun LogDetailScreen( viewModel: LogsViewModel = hiltViewModel(), ) { val context = LocalContext.current - val logs by viewModel.logs.collectAsState() - val logContent by viewModel.selectedLogContent.collectAsState() + val logs by viewModel.logs.collectAsStateWithLifecycle() + val logContent by viewModel.selectedLogContent.collectAsStateWithLifecycle() var isLoading by remember { mutableStateOf(true) } val listState = rememberLazyListState() diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressTypePreferenceViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressTypePreferenceViewModel.kt index 0c0522cff..f0833dcf9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressTypePreferenceViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressTypePreferenceViewModel.kt @@ -7,6 +7,9 @@ import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.AddressType import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -48,7 +51,7 @@ class AddressTypePreferenceViewModel @Inject constructor( viewModelScope.launch(bgDispatcher) { settingsStore.data.first().let { settings -> val selected = settings.selectedAddressType.toAddressType() ?: AddressType.P2WPKH - val monitored = settings.addressTypesToMonitor.toSet() + val monitored = settings.addressTypesToMonitor.toImmutableSet() _uiState.update { it.copy( selectedAddressType = selected, @@ -154,7 +157,7 @@ class AddressTypePreferenceViewModel @Inject constructor( @Immutable data class AddressTypePreferenceUiState( val selectedAddressType: AddressType = DEFAULT_ADDRESS_TYPE, - val monitoredTypes: Set = setOf(DEFAULT_ADDRESS_TYPE_STRING), + val monitoredTypes: ImmutableSet = persistentSetOf(DEFAULT_ADDRESS_TYPE_STRING), val showMonitoredTypes: Boolean = false, val isLoading: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index b4794a5c1..acc5c6562 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -28,6 +28,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.synonym.bitkitcore.AddressType +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel @@ -354,14 +356,14 @@ private fun Preview() { index = 4, path = "m/84'/0'/0'/0/4" ), - ), + ).toImmutableList(), balances = mapOf( "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" to 50000L, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" to 0L, "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" to 1500000L, "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" to 0L, "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" to 250000L, - ), + ).toImmutableMap(), selectedAddress = AddressModel( address = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", index = 0, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerViewModel.kt index 00d7339bd..a64d1ef98 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerViewModel.kt @@ -1,9 +1,16 @@ package to.bitkit.ui.settings.advanced +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.AddressType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -57,7 +64,7 @@ class AddressViewerViewModel @Inject constructor( _uiState.update { currentState -> currentState.copy( - addresses = addresses, + addresses = addresses.toImmutableList(), selectedAddress = addresses.firstOrNull(), ) } @@ -85,7 +92,7 @@ class AddressViewerViewModel @Inject constructor( ).getOrThrow() _uiState.update { currentState -> - currentState.copy(addresses = currentState.addresses + newAddresses) + currentState.copy(addresses = (currentState.addresses + newAddresses).toImmutableList()) } loadBalancesForAddresses(newAddresses) } @@ -104,7 +111,7 @@ class AddressViewerViewModel @Inject constructor( val addresses = _uiState.value.addresses.map { it.address } val balances = getBalanceForAddresses(addresses) - _uiState.update { it.copy(balances = balances) } + _uiState.update { it.copy(balances = balances.toImmutableMap()) } } _uiState.update { it.copy(isLoadingBalances = false) } @@ -125,9 +132,9 @@ class AddressViewerViewModel @Inject constructor( _uiState.update { currentState -> currentState.copy( - addresses = addresses, + addresses = addresses.toImmutableList(), selectedAddress = addresses.firstOrNull(), - balances = emptyMap(), // Clear balances for new address type + balances = persistentMapOf(), // Clear balances for new address type ) } @@ -156,9 +163,9 @@ class AddressViewerViewModel @Inject constructor( _uiState.update { currentState -> currentState.copy( - addresses = addresses, + addresses = addresses.toImmutableList(), selectedAddress = addresses.firstOrNull(), - balances = emptyMap(), + balances = persistentMapOf(), ) } loadBalancesForAddresses(addresses) @@ -182,7 +189,7 @@ class AddressViewerViewModel @Inject constructor( updatedBalances[address] = balance } - currentState.copy(balances = updatedBalances) + currentState.copy(balances = updatedBalances.toImmutableMap()) } } } @@ -208,9 +215,10 @@ class AddressViewerViewModel @Inject constructor( lightningRepo.getAddressBalance(address).map { it.toLong() } } +@Immutable data class UiState( - val addresses: List = emptyList(), - val balances: Map = emptyMap(), + val addresses: ImmutableList = persistentListOf(), + val balances: ImmutableMap = persistentMapOf(), val searchText: String = "", val selectedAddress: AddressModel? = null, val isLoading: Boolean = false, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 34ac82b08..c4bd020b1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R @@ -49,10 +51,10 @@ fun ConfirmMnemonicScreen( BlockScreenshots() val originalSeed = remember(uiState.bip39Mnemonic) { - uiState.bip39Mnemonic.split(" ").filter { it.isNotBlank() } + uiState.bip39Mnemonic.split(" ").filter { it.isNotBlank() }.toImmutableList() } val shuffledWords = remember(originalSeed) { - originalSeed.shuffled() + originalSeed.shuffled().toImmutableList() } var selectedWords by rememberSaveable { @@ -107,8 +109,8 @@ fun ConfirmMnemonicScreen( @Composable private fun ConfirmMnemonicContent( - originalSeed: List, - shuffledWords: List, + originalSeed: ImmutableList, + shuffledWords: ImmutableList, selectedWords: Array, pressedStates: BooleanArray, isComplete: Boolean, @@ -242,11 +244,11 @@ private fun SelectedWordItem( @Preview(showSystemUi = true) @Composable private fun Preview() { - val testWords = List(12) { "word${it + 1}" } + val testWords = List(12) { "word${it + 1}" }.toImmutableList() AppThemeSurface { ConfirmMnemonicContent( originalSeed = testWords, - shuffledWords = testWords.shuffled(), + shuffledWords = testWords.shuffled().toImmutableList(), selectedWords = arrayOfNulls(testWords.size), pressedStates = BooleanArray(testWords.size) { false }, isComplete = false, @@ -260,12 +262,12 @@ private fun Preview() { @Preview(showSystemUi = true) @Composable private fun Preview2() { - val testWords = List(12) { "word${it + 1}" } + val testWords = List(12) { "word${it + 1}" }.toImmutableList() val half = testWords.size / 2 AppThemeSurface { ConfirmMnemonicContent( originalSeed = testWords, - shuffledWords = testWords.shuffled(), + shuffledWords = testWords.shuffled().toImmutableList(), selectedWords = testWords.take(half).toTypedArray() + arrayOfNulls(half), pressedStates = BooleanArray(testWords.size) { it < half }, isComplete = false, @@ -279,12 +281,12 @@ private fun Preview2() { @Preview(showSystemUi = true) @Composable private fun Preview24Words() { - val testWords = List(24) { "word${it + 1}" } + val testWords = List(24) { "word${it + 1}" }.toImmutableList() val half = testWords.size / 2 AppThemeSurface { ConfirmMnemonicContent( originalSeed = testWords, - shuffledWords = testWords.shuffled(), + shuffledWords = testWords.shuffled().toImmutableList(), selectedWords = testWords.take(half).toTypedArray() + arrayOfNulls(half), pressedStates = BooleanArray(testWords.size) { it < half }, isComplete = false, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 0468834aa..f46acf507 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R @@ -95,7 +96,7 @@ private fun ShowMnemonicContent( onContinueClick: () -> Unit, modifier: Modifier = Modifier, ) { - val mnemonicWords = remember(mnemonic) { mnemonic.split(" ").filter { it.isNotBlank() } } + val mnemonicWords = remember(mnemonic) { mnemonic.split(" ").filter { it.isNotBlank() }.toImmutableList() } val buttonAlpha by animateFloatAsState( targetValue = if (showMnemonic) 0f else 1f, animationSpec = tween(durationMillis = 400), diff --git a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt index 1183efd69..80ece8fdf 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.models.FxRate import to.bitkit.repositories.CurrencyState @@ -60,7 +62,7 @@ fun LocalCurrencySettingsScreen( derivedStateOf { mostUsedCurrenciesList.mapNotNull { currency -> filteredRates.find { it.quote == currency } - } + }.toImmutableList() } } @@ -68,6 +70,7 @@ fun LocalCurrencySettingsScreen( derivedStateOf { filteredRates.filter { it.quote !in mostUsedCurrenciesList } .sortedBy { it.quote } + .toImmutableList() } } @@ -86,8 +89,8 @@ fun LocalCurrencySettingsScreen( fun LocalCurrencySettingsContent( searchText: String, onSearchTextChange: (String) -> Unit, - mostUsedRates: List, - otherCurrencies: List, + mostUsedRates: ImmutableList, + otherCurrencies: ImmutableList, selectedCurrency: String, onCurrencyClick: (String) -> Unit, onBackClick: () -> Unit, @@ -184,7 +187,7 @@ private fun Preview() { currencyFlag = "🇬🇧", lastUpdatedAt = 1234567890L, ), - ) + ).toImmutableList() val otherCurrencies = listOf( FxRate( @@ -209,7 +212,7 @@ private fun Preview() { currencyFlag = "🇨🇦", lastUpdatedAt = 1234567890L, ), - ) + ).toImmutableList() AppThemeSurface { LocalCurrencySettingsContent( diff --git a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt index 2006e8f39..354a8fb55 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import to.bitkit.R import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.settings.SectionHeader @@ -32,7 +35,7 @@ fun TagsSettingsScreen( val tags by settings.lastUsedTags.collectAsStateWithLifecycle() TagsSettingsContent( - tags = tags, + tags = tags.toImmutableList(), onClickTag = { tag -> settings.deleteLastUsedTag(tag) if (tags.size == 1) { @@ -45,7 +48,7 @@ fun TagsSettingsScreen( @Composable private fun TagsSettingsContent( - tags: List, + tags: ImmutableList, onClickTag: (String) -> Unit = {}, onBackClick: () -> Unit = {}, ) { @@ -86,7 +89,7 @@ private fun TagsSettingsContent( private fun Preview() { AppThemeSurface { TagsSettingsContent( - tags = listOf("tag1", "tag2", "tag3"), + tags = persistentListOf("tag1", "tag2", "tag3"), ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 04795bd4d..218e54867 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -48,6 +48,8 @@ import com.synonym.bitkitcore.IBtPayment import com.synonym.bitkitcore.IDiscount import com.synonym.bitkitcore.ILspNode import com.synonym.bitkitcore.IcJitEntry +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import org.lightningdevkit.ldknode.OutPoint import to.bitkit.R import to.bitkit.env.Env @@ -149,8 +151,8 @@ fun ChannelDetailScreen( @Composable private fun Content( channel: ChannelUi, - blocktankOrders: List = emptyList(), - cjitEntries: List = emptyList(), + blocktankOrders: ImmutableList = persistentListOf(), + cjitEntries: ImmutableList = persistentListOf(), txTime: ULong? = null, isRefreshing: Boolean = false, isClosedChannel: Boolean = false, @@ -669,7 +671,7 @@ private fun PreviewChannelWithOrder() { isUsable = true, ), ), - blocktankOrders = listOf( + blocktankOrders = persistentListOf( IBtOrder( id = "bt_order_12345", state = BtOrderState.OPEN, @@ -751,7 +753,7 @@ private fun PreviewPendingOrder() { isUsable = false, ), ), - blocktankOrders = listOf( + blocktankOrders = persistentListOf( IBtOrder( id = "pending_order_67890", state = BtOrderState.CREATED, @@ -838,7 +840,7 @@ private fun PreviewExpiredOrder() { isUsable = false, ), ), - blocktankOrders = listOf( + blocktankOrders = persistentListOf( IBtOrder( id = "expired_order_99999", state = BtOrderState.EXPIRED, @@ -912,7 +914,7 @@ private fun PreviewChannelWithCjit() { isUsable = true, ), ), - cjitEntries = listOf( + cjitEntries = persistentListOf( IcJitEntry( id = "cjit_entry_456", state = CJitStateEnum.COMPLETED, diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index f9611f6cc..6c1ada297 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -40,6 +40,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.ext.amountOnClose @@ -178,7 +180,7 @@ private fun Content( Caption13Up(stringResource(R.string.lightning__conn_pending), color = Colors.White64) ChannelList( status = ChannelStatusUi.PENDING, - channels = uiState.pendingConnections.reversed(), + channels = uiState.pendingConnections.reversed().toImmutableList(), onClickChannel = onClickChannel, ) } @@ -189,7 +191,7 @@ private fun Content( Caption13Up(stringResource(R.string.lightning__conn_open), color = Colors.White64) ChannelList( status = ChannelStatusUi.OPEN, - channels = uiState.openChannels.reversed(), + channels = uiState.openChannels.reversed().toImmutableList(), onClickChannel = onClickChannel, ) } @@ -201,7 +203,7 @@ private fun Content( Caption13Up(stringResource(R.string.lightning__conn_failed), color = Colors.White64) ChannelList( status = ChannelStatusUi.CLOSED, - channels = uiState.failedOrders.reversed(), + channels = uiState.failedOrders.reversed().toImmutableList(), onClickChannel = onClickChannel, ) } @@ -306,7 +308,7 @@ private fun BalanceColumn(label: String, balance: ULong, icon: ImageVector, colo @Composable private fun ChannelList( - channels: List, + channels: ImmutableList, status: ChannelStatusUi = ChannelStatusUi.OPEN, onClickChannel: (ChannelUi) -> Unit, ) { @@ -398,7 +400,7 @@ private fun Preview() { inboundCapacityMsat = 100_000_000u, ), ), - ), + ).toImmutableList(), openChannels = listOf( ChannelUi( name = "Connection 3", @@ -409,7 +411,7 @@ private fun Preview() { inboundCapacityMsat = 700_000_000u, ), ), - ), + ).toImmutableList(), failedOrders = listOf( ChannelUi( name = "Connection 4", @@ -431,7 +433,7 @@ private fun Preview() { inboundCapacityMsat = 70_000_000u, ), ), - ) + ).toImmutableList() ) ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 409cc4887..02041867e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -2,6 +2,7 @@ package to.bitkit.ui.settings.lightning import android.content.Context import android.net.Uri +import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.Activity @@ -14,6 +15,9 @@ import com.synonym.bitkitcore.SortDirection import com.synonym.bitkitcore.TransactionDetails import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -92,7 +96,7 @@ class LightningConnectionsViewModel @Inject constructor( closedChannel.toChannelUi( baseIndex = openChannels.size + pendingConnections.size + index ) - }.reversed() + }.reversed().toImmutableList() ) } } @@ -114,10 +118,11 @@ class LightningConnectionsViewModel @Inject constructor( _uiState.value.copy( isNodeRunning = isNodeRunning, - openChannels = openChannels.map { channel -> channel.mapToUiModel() }, + openChannels = openChannels.map { channel -> channel.mapToUiModel() }.toImmutableList(), pendingConnections = getPendingConnections(channels, blocktankState.paidOrders) - .map { it.mapToUiModel() }, - failedOrders = getFailedOrdersAsChannels(blocktankState.paidOrders).map { it.mapToUiModel() }, + .map { it.mapToUiModel() }.toImmutableList(), + failedOrders = getFailedOrdersAsChannels(blocktankState.paidOrders) + .map { it.mapToUiModel() }.toImmutableList(), localBalance = calculateLocalBalance(channels), remoteBalance = channels.calculateRemoteBalance(), ) @@ -496,13 +501,14 @@ class LightningConnectionsViewModel @Inject constructor( } } +@Stable data class LightningConnectionsUiState( val isNodeRunning: Boolean = true, val isRefreshing: Boolean = false, - val openChannels: List = emptyList(), - val pendingConnections: List = emptyList(), - val failedOrders: List = emptyList(), - val closedChannels: List = emptyList(), + val openChannels: ImmutableList = persistentListOf(), + val pendingConnections: ImmutableList = persistentListOf(), + val failedOrders: ImmutableList = persistentListOf(), + val closedChannels: ImmutableList = persistentListOf(), val localBalance: ULong = 0u, val remoteBalance: ULong = 0u, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt index 2c1cf66da..145bea29e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPaySettingsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.collections.immutable.persistentListOf import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS @@ -55,7 +56,7 @@ fun QuickPaySettingsScreenContent( onQuickPayAmountChange: (Int) -> Unit = {}, onBack: () -> Unit = {}, ) { - val sliderSteps = remember { listOf(1, 5, 10, 20, 50) } + val sliderSteps = remember { persistentListOf(1, 5, 10, 20, 50) } ScreenColumn { AppTopBar( diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 94af44aa3..9e8bcd454 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -1,5 +1,8 @@ package to.bitkit.ui.shared.toast +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -32,7 +35,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { val currentToast: StateFlow = _currentToast.asStateFlow() // Internal queue state - private val _queue = MutableStateFlow>(emptyList()) + private val _queue = MutableStateFlow>(persistentListOf()) private var timerJob: Job? = null private var isPaused = false @@ -43,9 +46,9 @@ class ToastQueueManager(private val scope: CoroutineScope) { _queue.update { current -> val newQueue = if (current.size >= MAX_QUEUE_SIZE) { // Drop oldest (first item) when queue full - current.drop(1) + toast + (current.drop(1) + toast).toImmutableList() } else { - current + toast + (current + toast).toImmutableList() } newQueue } @@ -91,7 +94,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { */ fun clear() { cancelTimer() - _queue.value = emptyList() + _queue.value = persistentListOf() _currentToast.value = null isPaused = false } @@ -100,7 +103,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { val nextToast = _queue.value.firstOrNull() ?: return // Remove from queue - _queue.update { it.drop(1) } + _queue.update { it.drop(1).toImmutableList() } // Display toast _currentToast.value = nextToast diff --git a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt index 2ba8313cb..c9fe07801 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BoostTransactionSheet.kt @@ -16,7 +16,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,6 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import to.bitkit.R import to.bitkit.models.BITCOIN_SYMBOL @@ -100,7 +100,7 @@ fun BoostTransactionSheet( } } - val uiState by viewModel.uiState.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() BottomSheet(onDismissRequest = onDismiss) { BoostTransactionContent( diff --git a/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt index 58867c36f..ce19aa834 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/NewTransactionSheet.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -24,6 +23,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.rememberLottieComposition @@ -56,8 +56,8 @@ fun NewTransactionSheet( settingsViewModel: SettingsViewModel, modifier: Modifier = Modifier, ) { - val currencies by currencyViewModel.uiState.collectAsState() - val details by appViewModel.transactionSheet.collectAsState() + val currencies by currencyViewModel.uiState.collectAsStateWithLifecycle() + val details by appViewModel.transactionSheet.collectAsStateWithLifecycle() CompositionLocalProvider( LocalCurrencyViewModel provides currencyViewModel, diff --git a/app/src/main/java/to/bitkit/ui/utils/Text.kt b/app/src/main/java/to/bitkit/ui/utils/Text.kt index 1770cedb9..2a0b42a48 100644 --- a/app/src/main/java/to/bitkit/ui/utils/Text.kt +++ b/app/src/main/java/to/bitkit/ui/utils/Text.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.formatPlural @@ -158,7 +160,7 @@ enum class BlockExplorerType { ADDRESS, TX } * ``` */ @Composable -fun localizedPlural(@StringRes id: Int, argMap: Map): String { +fun localizedPlural(@StringRes id: Int, argMap: ImmutableMap): String { val resources = LocalContext.current.resources return remember(id, argMap) { @@ -172,7 +174,10 @@ fun localizedPlural(@StringRes id: Int, argMap: Map): String { private fun PreviewLocalizedPlural() { AppThemeSurface { Text( - localizedPlural(R.string.settings__addr__spend_number, mapOf("fundsToSpend" to "1234", "count" to 2)) + localizedPlural( + R.string.settings__addr__spend_number, + persistentMapOf("fundsToSpend" to "1234", "count" to 2), + ) ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/ActivityListViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ActivityListViewModel.kt index fb3fc95bc..25d2934f9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ActivityListViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ActivityListViewModel.kt @@ -6,6 +6,9 @@ import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.PaymentType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -32,19 +35,20 @@ class ActivityListViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val activityRepo: ActivityRepo, ) : ViewModel() { - private val _filteredActivities = MutableStateFlow?>(null) + private val _filteredActivities = MutableStateFlow?>(null) val filteredActivities = _filteredActivities.asStateFlow() - private val _lightningActivities = MutableStateFlow?>(null) + private val _lightningActivities = MutableStateFlow?>(null) val lightningActivities = _lightningActivities.asStateFlow() - private val _onchainActivities = MutableStateFlow?>(null) + private val _onchainActivities = MutableStateFlow?>(null) val onchainActivities = _onchainActivities.asStateFlow() - private val _latestActivities = MutableStateFlow?>(null) + private val _latestActivities = MutableStateFlow?>(null) val latestActivities = _latestActivities.asStateFlow() - val availableTags: StateFlow> = activityRepo.state.map { it.tags }.stateInScope(emptyList()) + val availableTags: StateFlow> = + activityRepo.state.map { it.tags }.stateInScope(persistentListOf()) private val _filters = MutableStateFlow(ActivityFilters()) @@ -89,16 +93,16 @@ class ActivityListViewModel @Inject constructor( ) { debouncedSearch, filtersWithoutSearch, _ -> fetchFilteredActivities(filtersWithoutSearch.copy(searchText = debouncedSearch)) }.collect { activities -> - _filteredActivities.update { activities } + _filteredActivities.update { activities?.toImmutableList() } } } private suspend fun refreshActivityState() { val all = activityRepo.getActivities(filter = ActivityFilter.ALL).getOrNull() ?: emptyList() val filtered = filterOutReplacedSentTransactions(all) - _latestActivities.update { filtered.take(SIZE_LATEST) } - _lightningActivities.update { filtered.filter { it is Activity.Lightning } } - _onchainActivities.update { filtered.filter { it is Activity.Onchain } } + _latestActivities.update { filtered.take(SIZE_LATEST).toImmutableList() } + _lightningActivities.update { filtered.filter { it is Activity.Lightning }.toImmutableList() } + _onchainActivities.update { filtered.filter { it is Activity.Onchain }.toImmutableList() } } private suspend fun fetchFilteredActivities(filters: ActivityFilters): List? { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 2fdc4fc85..bd51b82f2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -27,6 +27,12 @@ import com.synonym.bitkitcore.SortDirection import com.synonym.bitkitcore.validateBitcoinAddress import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -236,7 +242,7 @@ class AppViewModel @Inject constructor( fun addTagToSelected(newTag: String) { _sendUiState.update { it.copy( - selectedTags = (it.selectedTags + newTag).distinct() + selectedTags = (it.selectedTags + newTag).distinct().toImmutableList() ) } viewModelScope.launch { @@ -247,7 +253,7 @@ class AppViewModel @Inject constructor( fun removeTag(tag: String) { _sendUiState.update { it.copy( - selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag } + selectedTags = it.selectedTags.filterNot { tagItem -> tagItem == tag }.toImmutableList() ) } } @@ -1099,7 +1105,7 @@ class AppViewModel @Inject constructor( private suspend fun onCoinSelectionContinue(utxos: List) { _sendUiState.update { - it.copy(selectedUtxos = utxos) + it.copy(selectedUtxos = utxos.toImmutableList()) } refreshFeeEstimates() setSendEffect(SendEffect.NavigateToConfirm) @@ -1929,7 +1935,7 @@ class AppViewModel @Inject constructor( ) } .onSuccess { utxos -> - _sendUiState.update { it.copy(selectedUtxos = utxos) } + _sendUiState.update { it.copy(selectedUtxos = utxos?.toImmutableList()) } } } refreshFeeEstimates() @@ -1967,7 +1973,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy( - fees = feesMap, + fees = feesMap.toImmutableMap(), fee = SendFee.OnChain(currentFee), ) } @@ -2257,7 +2263,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy( showSanityWarningDialog = null, - confirmedWarnings = it.confirmedWarnings + warning + confirmedWarnings = (it.confirmedWarnings + warning).toImmutableList() ) } } @@ -2377,6 +2383,7 @@ class AppViewModel @Inject constructor( } // region send contract +@Stable data class SendUiState( val address: String = "", val bolt11: String? = null, @@ -2386,19 +2393,19 @@ data class SendUiState( val isAmountInputValid: Boolean = false, val isUnified: Boolean = false, val payMethod: SendMethod = SendMethod.ONCHAIN, - val selectedTags: List = listOf(), + val selectedTags: ImmutableList = persistentListOf(), val decodedInvoice: LightningInvoice? = null, val showSanityWarningDialog: SanityWarning? = null, - val confirmedWarnings: List = listOf(), + val confirmedWarnings: ImmutableList = persistentListOf(), val shouldConfirmPay: Boolean = false, - val selectedUtxos: List? = null, + val selectedUtxos: ImmutableList? = null, val lnurl: LnurlParams? = null, val isLoading: Boolean = false, val speed: TransactionSpeed = TransactionSpeed.default(), val comment: String = "", val feeRates: FeeRates? = null, val fee: SendFee? = null, - val fees: Map = emptyMap(), + val fees: ImmutableMap = persistentMapOf(), val estimatedRoutingFee: ULong = 0uL, ) diff --git a/app/src/main/java/to/bitkit/viewmodels/BackupsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/BackupsViewModel.kt index 6ef0f4cd2..350c2c716 100644 --- a/app/src/main/java/to/bitkit/viewmodels/BackupsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/BackupsViewModel.kt @@ -1,8 +1,12 @@ package to.bitkit.viewmodels +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -37,7 +41,7 @@ class BackupsViewModel @Inject constructor( val cachedStatus = cachedStatuses[category] ?: BackupItemStatus(synced = 0, required = 1) category.toUiState(cachedStatus) } - _uiState.update { it.copy(categories = categories) } + _uiState.update { it.copy(categories = categories.toImmutableList()) } } } } @@ -72,8 +76,9 @@ data class BackupCategoryUiState( val disableRetry: Boolean = false, ) +@Immutable data class BackupStatusUiState( - val categories: List = emptyList(), + val categories: ImmutableList = persistentListOf(), ) fun BackupCategory.toUiState(status: BackupItemStatus = BackupItemStatus()): BackupCategoryUiState { diff --git a/app/src/main/java/to/bitkit/viewmodels/LanguageViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LanguageViewModel.kt index 17e34ae27..4f468544e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LanguageViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LanguageViewModel.kt @@ -1,8 +1,12 @@ package to.bitkit.viewmodels +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -32,7 +36,7 @@ class LanguageViewModel @Inject constructor( _uiState.update { it.copy( selectedLanguage = currentLanguage, - languages = appLocaleManager.getSupportedLanguages() + languages = appLocaleManager.getSupportedLanguages().toImmutableList() ) } } @@ -43,8 +47,9 @@ class LanguageViewModel @Inject constructor( } } +@Immutable data class LanguageUiState( val selectedLanguage: Language = Language.SYSTEM_DEFAULT, - val languages: List = listOf(), + val languages: ImmutableList = persistentListOf(), val isLoading: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/viewmodels/LogsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LogsViewModel.kt index 5e2587446..c2d7e8712 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LogsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LogsViewModel.kt @@ -6,6 +6,9 @@ import androidx.core.content.FileProvider import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,15 +32,15 @@ class LogsViewModel @Inject constructor( private const val TAG = "LogsViewModel" } - private val _logs = MutableStateFlow>(emptyList()) - val logs: StateFlow> = _logs.asStateFlow() + private val _logs = MutableStateFlow>(persistentListOf()) + val logs: StateFlow> = _logs.asStateFlow() - private val _selectedLogContent = MutableStateFlow>(emptyList()) - val selectedLogContent: StateFlow> = _selectedLogContent.asStateFlow() + private val _selectedLogContent = MutableStateFlow>(persistentListOf()) + val selectedLogContent: StateFlow> = _selectedLogContent.asStateFlow() fun loadLogs() { viewModelScope.launch { - val logFiles = logsRepo.getLogs().getOrDefault(emptyList()) + val logFiles = logsRepo.getLogs().getOrDefault(emptyList()).toImmutableList() _logs.update { logFiles } } } @@ -46,10 +49,10 @@ class LogsViewModel @Inject constructor( viewModelScope.launch { logsRepo.loadLogContent(logFile) .onSuccess { content -> - _selectedLogContent.update { content } + _selectedLogContent.update { content.toImmutableList() } } .onFailure { e -> - _selectedLogContent.update { listOf("Log file not found") } + _selectedLogContent.update { persistentListOf("Log file not found") } Logger.error("Failed to load log content", e, context = TAG) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/RestoreWalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/RestoreWalletViewModel.kt index b45b58bdb..4e4a8815e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/RestoreWalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/RestoreWalletViewModel.kt @@ -1,8 +1,15 @@ package to.bitkit.viewmodels +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -62,7 +69,7 @@ class RestoreWalletViewModel @Inject constructor( _uiState.update { it.copy( focusedIndex = null, - suggestions = emptyList() + suggestions = persistentListOf() ) } } @@ -71,7 +78,7 @@ class RestoreWalletViewModel @Inject constructor( fun onSelectSuggestion(suggestion: String) { _uiState.value.focusedIndex?.let { index -> updateWordValidity(index, suggestion) - _uiState.update { it.copy(suggestions = emptyList()) } + _uiState.update { it.copy(suggestions = persistentListOf()) } } } @@ -116,12 +123,12 @@ class RestoreWalletViewModel @Inject constructor( _uiState.update { it.copy( - words = newWords, - invalidWordIndices = invalidIndices, + words = newWords.toImmutableList(), + invalidWordIndices = invalidIndices.toImmutableSet(), is24Words = pastedWords.size == WORDS_MAX, shouldDismissKeyboard = invalidIndices.isEmpty(), focusedIndex = null, - suggestions = emptyList(), + suggestions = persistentListOf(), ) } recomputeValidationState() @@ -142,8 +149,8 @@ class RestoreWalletViewModel @Inject constructor( _uiState.update { it.copy( - words = newWords, - invalidWordIndices = newInvalidIndices, + words = newWords.toImmutableList(), + invalidWordIndices = newInvalidIndices.toImmutableSet(), ) } recomputeValidationState() @@ -151,7 +158,7 @@ class RestoreWalletViewModel @Inject constructor( private fun updateSuggestions(input: String, index: Int?) = viewModelScope.launch { if (index == null || input.length < 2) { - _uiState.update { it.copy(suggestions = emptyList()) } + _uiState.update { it.copy(suggestions = persistentListOf()) } return@launch } @@ -162,7 +169,7 @@ class RestoreWalletViewModel @Inject constructor( suggestions } - _uiState.update { it.copy(suggestions = filtered) } + _uiState.update { it.copy(suggestions = filtered.toImmutableList()) } } private suspend fun RestoreWalletUiState.areButtonsEnabled(): Boolean { @@ -187,10 +194,11 @@ class RestoreWalletViewModel @Inject constructor( } } +@Immutable data class RestoreWalletUiState( - val words: List = List(WORDS_MAX) { "" }, - val invalidWordIndices: Set = emptySet(), - val suggestions: List = emptyList(), + val words: ImmutableList = List(WORDS_MAX) { "" }.toImmutableList(), + val invalidWordIndices: ImmutableSet = persistentSetOf(), + val suggestions: ImmutableList = persistentListOf(), val focusedIndex: Int? = null, val bip39Passphrase: String = "", val showingPassphrase: Boolean = false, diff --git a/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt index 1f201e01d..339a45917 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt @@ -1,8 +1,12 @@ package to.bitkit.viewmodels +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -31,12 +35,13 @@ class TagsViewModel @Inject constructor( fun loadTagSuggestions() { viewModelScope.launch(Dispatchers.IO) { val tags = settingsStore.data.first().lastUsedTags - _uiState.update { it.copy(tagsSuggestions = tags) } + _uiState.update { it.copy(tagsSuggestions = tags.toImmutableList()) } } } } +@Immutable data class AddTagUiState( - val tagsSuggestions: List = listOf(), + val tagsSuggestions: ImmutableList = persistentListOf(), val tagInput: String = "" ) diff --git a/app/src/main/java/to/bitkit/viewmodels/VssDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/VssDebugViewModel.kt index b99e0ad6e..89079ded2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/VssDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/VssDebugViewModel.kt @@ -8,6 +8,9 @@ import com.synonym.vssclient.KeyVersion import com.synonym.vssclient.LdkNamespace import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -52,7 +55,7 @@ class VssDebugViewModel @Inject constructor( vssBackupClient.listKeys().onSuccess { keys -> Logger.info("VSS keys: ${keys.size}", context = TAG) keys.forEach { Logger.debug(" ${it.key} v${it.version}", context = TAG) } - _uiState.update { it.copy(vssKeys = keys) } + _uiState.update { it.copy(vssKeys = keys.toImmutableList()) } ToastEventBus.send( type = Toast.ToastType.INFO, title = "Found ${keys.size} VSS key(s)", @@ -74,7 +77,7 @@ class VssDebugViewModel @Inject constructor( _uiState.update { it.copy(isLoading = true) } vssBackupClient.deleteAllKeys().onSuccess { deletedCount -> Logger.info("Deleted $deletedCount VSS keys", context = TAG) - _uiState.update { it.copy(vssKeys = emptyList()) } + _uiState.update { it.copy(vssKeys = persistentListOf()) } ToastEventBus.send( type = Toast.ToastType.INFO, title = "Deleted $deletedCount VSS key(s)", @@ -99,7 +102,7 @@ class VssDebugViewModel @Inject constructor( if (wasDeleted) { Logger.info("Deleted VSS key: $key", context = TAG) _uiState.update { state -> - state.copy(vssKeys = state.vssKeys.filter { it.key != key }) + state.copy(vssKeys = state.vssKeys.filter { it.key != key }.toImmutableList()) } ToastEventBus.send( type = Toast.ToastType.INFO, @@ -130,7 +133,7 @@ class VssDebugViewModel @Inject constructor( vssBackupClientLdk.listAllKeysTagged().onSuccess { tagged -> Logger.info("VSS LDK keys: ${tagged.size}", context = TAG) tagged.forEach { Logger.debug(" ${it.second.key} v${it.second.version}", context = TAG) } - val items = tagged.map { VssLdkKeyItem(keyVersion = it.second, namespace = it.first) } + val items = tagged.map { VssLdkKeyItem(keyVersion = it.second, namespace = it.first) }.toImmutableList() _uiState.update { it.copy(vssLdkKeys = items) } ToastEventBus.send( type = Toast.ToastType.INFO, @@ -159,7 +162,7 @@ class VssDebugViewModel @Inject constructor( state.copy( vssLdkKeys = state.vssLdkKeys.filter { it.keyVersion.key != key || it.namespace != namespace - } + }.toImmutableList() ) } ToastEventBus.send( @@ -229,8 +232,8 @@ class VssDebugViewModel @Inject constructor( @Stable data class VssDebugUiState( val isLoading: Boolean = false, - @Stable val vssKeys: List = emptyList(), - @Stable val vssLdkKeys: List = emptyList(), + val vssKeys: ImmutableList = persistentListOf(), + val vssLdkKeys: ImmutableList = persistentListOf(), ) data class VssLdkKeyItem( diff --git a/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt b/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt index e601bc7bc..7240d6dd7 100644 --- a/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt +++ b/app/src/test/java/to/bitkit/repositories/ActivityDetailViewModelTest.kt @@ -5,6 +5,8 @@ import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.IBtOrder import com.synonym.bitkitcore.OnchainActivity import com.synonym.bitkitcore.PaymentType +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test @@ -60,7 +62,8 @@ class ActivityDetailViewModelTest : BaseUnitTest() { @Test fun `findOrderForTransfer finds order by channelId`() = test { val order = mock { on { id } doReturn ORDER_ID } - whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = listOf(order)))) + val state = BlocktankState(orders = listOf(order).toImmutableList()) + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(state)) val result = sut.findOrderForTransfer(ORDER_ID, null) @@ -70,7 +73,8 @@ class ActivityDetailViewModelTest : BaseUnitTest() { @Test fun `findOrderForTransfer finds order by channelId matching order id`() = test { val order = mock { on { id } doReturn ORDER_ID } - whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = listOf(order)))) + val state = BlocktankState(orders = listOf(order).toImmutableList()) + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(state)) val result = sut.findOrderForTransfer(ORDER_ID, null) @@ -79,7 +83,7 @@ class ActivityDetailViewModelTest : BaseUnitTest() { @Test fun `findOrderForTransfer returns null when order not found`() = test { - whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = emptyList()))) + whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(BlocktankState(orders = persistentListOf()))) val result = sut.findOrderForTransfer("non-existent-id", null) diff --git a/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt b/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt index 11a9e3ec7..eac059a62 100644 --- a/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/HealthRepoTest.kt @@ -2,6 +2,8 @@ package to.bitkit.repositories import app.cash.turbine.test import com.synonym.bitkitcore.IBtOrder +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.Before @@ -154,7 +156,7 @@ class HealthRepoTest : BaseUnitTest() { } val lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = listOf(mockChannel), + channels = listOf(mockChannel).toImmutableList(), ) whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(lightningState)) @@ -174,7 +176,7 @@ class HealthRepoTest : BaseUnitTest() { } val lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = listOf(mockChannel), + channels = listOf(mockChannel).toImmutableList(), ) whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(lightningState)) @@ -191,7 +193,7 @@ class HealthRepoTest : BaseUnitTest() { fun `channels status is error when no channels exist`() = test { val lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = emptyList(), + channels = persistentListOf(), ) whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(lightningState)) @@ -208,12 +210,12 @@ class HealthRepoTest : BaseUnitTest() { fun `paid orders override channels error to pending`() = test { val lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = emptyList(), + channels = persistentListOf(), ) whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(lightningState)) val paidOrder = mock() - val blocktankState = BlocktankState(paidOrders = listOf(paidOrder)) + val blocktankState = BlocktankState(paidOrders = listOf(paidOrder).toImmutableList()) whenever(blocktankRepo.blocktankState).thenReturn(MutableStateFlow(blocktankState)) sut = createSut() @@ -253,7 +255,7 @@ class HealthRepoTest : BaseUnitTest() { } val lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, - channels = listOf(mockChannel), + channels = listOf(mockChannel).toImmutableList(), ) whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(lightningState)) diff --git a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt index 224498515..63625d275 100644 --- a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt @@ -4,6 +4,7 @@ import app.cash.turbine.test import com.synonym.bitkitcore.AddressType import com.synonym.bitkitcore.GetAddressResponse import com.synonym.bitkitcore.GetAddressesResponse +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -67,7 +68,7 @@ class WalletRepoTest : BaseUnitTest() { on { inboundCapacityMsat } doReturn 500_000u on { isChannelReady } doReturn true } - ) + ).toImmutableList() private val channelReady = Event.ChannelReady( channelId = "testChannelId", userChannelId = "testUserChannelId", diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt index 703926a78..9db528317 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt @@ -2,6 +2,8 @@ package to.bitkit.ui.screens.wallets.send import android.content.Context import com.synonym.bitkitcore.FeeRates +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test @@ -130,10 +132,10 @@ class SendFeeViewModelTest : BaseUnitTest() { ), ) = SendUiState( amount = amount, - selectedUtxos = emptyList(), + selectedUtxos = persistentListOf(), address = address, speed = TransactionSpeed.Medium, feeRates = FeeRates(fast = 10u, mid = 5u, slow = 2u), - fees = fees, + fees = fees.toImmutableMap(), ) } diff --git a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt index e36008926..465809ba2 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt @@ -1,5 +1,6 @@ package to.bitkit.viewmodels +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf import org.junit.Assert.assertEquals @@ -79,7 +80,7 @@ class AmountInputViewModelTest : BaseUnitTest() { primaryDisplay: PrimaryDisplay = PrimaryDisplay.BITCOIN, displayUnit: BitcoinDisplayUnit = BitcoinDisplayUnit.MODERN, ) = CurrencyState( - rates = testRates, + rates = testRates.toImmutableList(), selectedCurrency = "USD", currencySymbol = "$", primaryDisplay = primaryDisplay, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f78fafcf0..1e6ad337b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", ve hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltAndroidx" } jna = { module = "net.java.dev.jna:jna", version = "5.18.1" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } +kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.8" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.7.1" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }