From 5809881aaf9bf974ee2d49ed03735d8ff3005076 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 28 Jan 2026 16:12:04 -0300 Subject: [PATCH] Refactor account persistence to use Kotlin Coroutines Flow instead of LiveData - Replace `LiveData` with `MutableStateFlow` in the `Account` model and `AccountStateViewModel` to observe and save account changes. - Implement custom setters for `signPolicy` and `didBackup` in the `Account` class to trigger state updates automatically. - Simplify `AccountStateViewModel` by using a `Job` to collect and save account state changes on a background dispatcher. - Remove redundant manual calls to `LocalPreferences.saveToEncryptedStorage` across several UI components (`SignPolicySettingsScreen`, `ProfileSubscription`, `AccountBackupDialog`, etc.), as saving is now handled reactively. - Remove `runtime-livedata` dependency from `build.gradle` and `libs.versions.toml`. - Clean up unused imports and the obsolete `AccountLiveData` class. --- app/build.gradle | 5 --- .../greenart7c3/nostrsigner/models/Account.kt | 39 +++++++++++++++---- .../service/ProfileSubscription.kt | 1 - .../nostrsigner/ui/AccountStateViewModel.kt | 27 +++++-------- .../ui/SignPolicySettingsScreen.kt | 2 - .../ui/actions/AccountBackupDialog.kt | 8 ---- .../ui/actions/ExportAllAccountsScreen.kt | 1 - gradle/libs.versions.toml | 1 - 8 files changed, 40 insertions(+), 44 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 51e09302..e5c8beb2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,14 +189,9 @@ dependencies { implementation libs.ui implementation libs.ui.tooling.preview - implementation libs.runtime.livedata - // Navigation implementation libs.navigation.compose - // Observe Live data as State - implementation libs.runtime.livedata - implementation libs.material3 implementation libs.material.icons.extended diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt index eb74bde6..a857439c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/Account.kt @@ -6,10 +6,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.Clipboard -import androidx.lifecycle.LiveData import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.DataStoreAccess -import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -26,6 +24,9 @@ import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch @Stable @@ -35,10 +36,35 @@ class Account( val npub: String, val name: MutableStateFlow, val picture: MutableStateFlow, - var signPolicy: Int, - var didBackup: Boolean, + signPolicy: Int, + didBackup: Boolean, ) { - val saveable: AccountLiveData = AccountLiveData(this) + var signPolicy: Int = signPolicy + set(value) { + if (field != value) { + field = value + _saveable.value = AccountState(this) + } + } + + var didBackup: Boolean = didBackup + set(value) { + if (field != value) { + field = value + _saveable.value = AccountState(this) + } + } + + private val _saveable = MutableStateFlow(AccountState(this)) + val saveable = _saveable.asStateFlow() + + init { + Amber.instance.applicationIOScope.launch { + combine(name, picture) { _, _ -> }.drop(1).collect { + _saveable.value = AccountState(this@Account) + } + } + } suspend fun sign(eventTemplate: EventTemplate): T = signer.sign(eventTemplate) @@ -87,7 +113,6 @@ class Account( fun copyToClipboard(clipboardManager: Clipboard) { Amber.instance.applicationIOScope.launch { didBackup = true - LocalPreferences.saveToEncryptedStorage(Amber.instance, this@Account, null, null, null) val nsec = getNsec() Amber.instance.applicationIOScope.launch(Dispatchers.Main) { clipboardManager.setClipEntry( @@ -106,7 +131,5 @@ class Account( } } -class AccountLiveData(account: Account) : LiveData(AccountState(account)) - @Immutable class AccountState(val account: Account) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/ProfileSubscription.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/ProfileSubscription.kt index 0813c702..40ce933b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/ProfileSubscription.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/ProfileSubscription.kt @@ -88,7 +88,6 @@ class ProfileSubscription( if (atLeastOne) { scope.launch { - LocalPreferences.saveToEncryptedStorage(appContext, account, null, null, null) LocalPreferences.setLastMetadataUpdate(appContext, npub, TimeUtils.now()) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index c8f8afc6..79a23aae 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -17,6 +17,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import com.vitorpamplona.quartz.utils.Hex import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -48,6 +49,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { private val _accountContent = MutableStateFlow(AccountState.LoggedOff) val accountContent = _accountContent.asStateFlow() val toasts = MutableSharedFlow(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private var observerJob: Job? = null init { tryLoginExistingAccount(null, npub) @@ -103,15 +105,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { } private fun prepareLogoutOrSwitch() { - when (val state = accountContent.value) { - is AccountState.LoggedIn -> { - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - state.account.saveable.removeObserver(saveListener) - } - } - else -> {} - } - + observerJob?.cancel() _accountContent.update { AccountState.LoggedOff } } @@ -236,15 +230,12 @@ class AccountStateViewModel(npub: String?) : ViewModel() { ) { _accountContent.update { AccountState.LoggedIn(account, route) } - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - account.saveable.observeForever(saveListener) - } - } - - private val saveListener: (com.greenart7c3.nostrsigner.models.AccountState) -> Unit = { - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - Log.d(Amber.TAG, "Account saved") - LocalPreferences.saveToEncryptedStorage(Amber.instance, it.account, null, null, null) + observerJob?.cancel() + observerJob = Amber.instance.applicationIOScope.launch(Dispatchers.IO) { + account.saveable.collect { + Log.d(Amber.TAG, "Account saved") + LocalPreferences.saveToEncryptedStorage(Amber.instance, it.account, null, null, null) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SignPolicySettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SignPolicySettingsScreen.kt index 028e23bf..23ce2413 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SignPolicySettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SignPolicySettingsScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.ui.components.AmberButton @@ -107,7 +106,6 @@ fun SignPolicySettingsScreen( onClick = { scope.launch(Dispatchers.IO) { account.signPolicy = selectedOption - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) scope.launch(Dispatchers.Main) { navController.navigateUp() } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt index 40d5db62..ac5dd72e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupDialog.kt @@ -330,7 +330,6 @@ private fun NSecQrButton( ) { Amber.instance.applicationIOScope.launch { account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) showDialog = false } } @@ -345,7 +344,6 @@ private fun NSecQrButton( onLoading(true) val ncryptsec = account.nip49Encrypt(password) account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) Amber.instance.applicationIOScope.launch(Dispatchers.Main) { navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec)) } @@ -369,7 +367,6 @@ private fun NSecQrButton( onLoading(true) val ncryptsec = account.nip49Encrypt(password) account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) Amber.instance.applicationIOScope.launch(Dispatchers.Main) { navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec)) } @@ -533,9 +530,6 @@ private fun SeedWordsButton( Amber.instance.applicationIOScope.launch { account.didBackup = true showSeedWords = true - Amber.instance.applicationIOScope.launch { - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) - } } }, onError = { _, message -> @@ -588,7 +582,6 @@ private fun NSecCopyButton( onApproved = { scope.launch(Dispatchers.IO) { account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) if (password.isNotBlank()) { encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading) } else { @@ -645,7 +638,6 @@ private fun encryptCopyNSec( try { val key = account.nip49Encrypt(password) account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) Amber.instance.applicationIOScope.launch(Dispatchers.Main) { clipboardManager.setClipEntry( ClipEntry( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt index 883edb85..44e5124b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ExportAllAccountsScreen.kt @@ -122,7 +122,6 @@ fun ExportAllAccountsScreen( LocalPreferences.allSavedAccounts(context).forEach { accountInfo -> LocalPreferences.loadFromEncryptedStorage(context, accountInfo.npub)?.let { account -> account.didBackup = true - LocalPreferences.saveToEncryptedStorage(context, account, null, null, null) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1db20dbe..0cd2d1ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,6 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" } room-paging = { module = "androidx.room:room-paging", version.ref = "roomKtx" } -runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose_ui" } security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCryptoKtx" } security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" } ui = { module = "androidx.compose.ui:ui", version.ref = "compose_ui" }