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.
This commit is contained in:
greenart7c3
2026-01-28 16:12:04 -03:00
parent 717b6effc5
commit 5809881aaf
8 changed files with 40 additions and 44 deletions
-5
View File
@@ -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
@@ -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<String>,
val picture: MutableStateFlow<String>,
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 <T : Event> sign(eventTemplate: EventTemplate<T>): 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>(AccountState(account))
@Immutable
class AccountState(val account: Account)
@@ -88,7 +88,6 @@ class ProfileSubscription(
if (atLeastOne) {
scope.launch {
LocalPreferences.saveToEncryptedStorage(appContext, account, null, null, null)
LocalPreferences.setLastMetadataUpdate(appContext, npub, TimeUtils.now())
}
}
@@ -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>(AccountState.LoggedOff)
val accountContent = _accountContent.asStateFlow()
val toasts = MutableSharedFlow<ToastMsg?>(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)
}
}
}
}
@@ -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()
}
@@ -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(
@@ -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)
}
}
-1
View File
@@ -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" }