[#634] Stop synchronizer when leaving app

* [#634] Collect flows with lifecycle awareness

* [#686] Adopt SDK 1.11.0
This commit is contained in:
Carter Jernigan 2022-12-22 03:38:02 -05:00 committed by GitHub
parent ad89309ec1
commit 4f7c10f4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 110 additions and 64 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ local.properties
/.idea/artifacts /.idea/artifacts
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/inspectionProfiles/Project_Default.xml /.idea/inspectionProfiles/Project_Default.xml
/.idea/androidTestResultsUserPreferences.xml

View File

@ -6,6 +6,8 @@
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" /> <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value> </value>
</option> </option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" /> <option name="FORCE_REARRANGE_MODE" value="1" />

View File

@ -120,7 +120,7 @@ ANDROIDX_COMPOSE_VERSION=1.3.1
ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.1 ANDROIDX_CONSTRAINTLAYOUT_VERSION=1.0.1
ANDROIDX_CORE_VERSION=1.9.0 ANDROIDX_CORE_VERSION=1.9.0
ANDROIDX_ESPRESSO_VERSION=3.5.0 ANDROIDX_ESPRESSO_VERSION=3.5.0
ANDROIDX_LIFECYCLE_VERSION=2.5.1 ANDROIDX_LIFECYCLE_VERSION=2.6.0-alpha03
ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.5.3 ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.5.3
ANDROIDX_PROFILE_INSTALLER_VERSION=1.3.0-alpha02 ANDROIDX_PROFILE_INSTALLER_VERSION=1.3.0-alpha02
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha04 ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha04
@ -145,7 +145,7 @@ ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.4 ZCASH_BIP39_VERSION=1.0.4
# TODO [#279]: Revert to stable SDK before app release # TODO [#279]: Revert to stable SDK before app release
# TODO [#279]: https://github.com/zcash/secant-android-wallet/issues/279 # TODO [#279]: https://github.com/zcash/secant-android-wallet/issues/279
ZCASH_SDK_VERSION=1.10.0-beta01-SNAPSHOT ZCASH_SDK_VERSION=1.11.0-beta01-SNAPSHOT
ZXING_VERSION=3.5.0 ZXING_VERSION=3.5.0

View File

@ -173,6 +173,7 @@ dependencyResolutionManagement {
library("androidx-core", "androidx.core:core-ktx:$androidxCoreVersion") library("androidx-core", "androidx.core:core-ktx:$androidxCoreVersion")
library("androidx-constraintlayout", "androidx.constraintlayout:constraintlayout-compose:$androidxConstraintLayoutVersion") library("androidx-constraintlayout", "androidx.constraintlayout:constraintlayout-compose:$androidxConstraintLayoutVersion")
library("androidx-lifecycle-livedata", "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion") library("androidx-lifecycle-livedata", "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion")
library("androidx-lifecycle-compose", "androidx.lifecycle:lifecycle-runtime-compose:$androidxLifecycleVersion")
library("androidx-navigation-compose", "androidx.navigation:navigation-compose:$androidxNavigationComposeVersion") library("androidx-navigation-compose", "androidx.navigation:navigation-compose:$androidxNavigationComposeVersion")
library("androidx-profileinstaller", "androidx.profileinstaller:profileinstaller:$androidxProfileInstallerVersion") library("androidx-profileinstaller", "androidx.profileinstaller:profileinstaller:$androidxProfileInstallerVersion")
library("androidx-security-crypto", "androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion") library("androidx-security-crypto", "androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion")
@ -236,6 +237,7 @@ dependencyResolutionManagement {
"androidx-activity-compose", "androidx-activity-compose",
"androidx-compose-material-icons-core", "androidx-compose-material-icons-core",
"androidx-compose-material-icons-extended", "androidx-compose-material-icons-extended",
"androidx-lifecycle-compose",
"androidx-navigation-compose", "androidx-navigation-compose",
"androidx-viewmodel-compose" "androidx-viewmodel-compose"
) )

View File

@ -17,6 +17,7 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -86,21 +87,20 @@ class WalletCoordinator(context: Context) {
flowOf(InternalSynchronizerStatus.NoWallet) flowOf(InternalSynchronizerStatus.NoWallet)
} else { } else {
callbackFlow<InternalSynchronizerStatus.Available> { callbackFlow<InternalSynchronizerStatus.Available> {
val synchronizer = synchronizerMutex.withLock { val closeableSynchronizer = synchronizerMutex.withLock {
val synchronizer = Synchronizer.new( Synchronizer.new(
context = context, context = context,
zcashNetwork = persistableWallet.network, zcashNetwork = persistableWallet.network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network), lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
birthday = persistableWallet.birthday, birthday = persistableWallet.birthday,
seed = persistableWallet.seedPhrase.toByteArray() seed = persistableWallet.seedPhrase.toByteArray()
) )
synchronizer.start(walletScope)
} }
trySend(InternalSynchronizerStatus.Available(synchronizer)) trySend(InternalSynchronizerStatus.Available(closeableSynchronizer))
awaitClose { awaitClose {
Twig.info { "Closing flow and stopping synchronizer" } Twig.info { "Closing flow and synchronizer" }
synchronizer.stop() closeableSynchronizer.close()
} }
} }
} }
@ -112,11 +112,9 @@ class WalletCoordinator(context: Context) {
* Note that this synchronizer is closed as soon as it stops being collected. For UI use * Note that this synchronizer is closed as soon as it stops being collected. For UI use
* cases, see [WalletViewModel]. * cases, see [WalletViewModel].
*/ */
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val synchronizer: StateFlow<Synchronizer?> = synchronizerOrLockoutId val synchronizer: StateFlow<Synchronizer?> = synchronizerOrLockoutId
.flatMapLatest { .flatMapLatest { it }
it
}
.map { .map {
when (it) { when (it) {
is InternalSynchronizerStatus.Available -> it.synchronizer is InternalSynchronizerStatus.Available -> it.synchronizer

View File

@ -9,13 +9,13 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import cash.z.ecc.android.sdk.ext.collectWith
import co.electriccoin.zcash.ui.common.BindCompLocalProvider import co.electriccoin.zcash.ui.common.BindCompLocalProvider
import co.electriccoin.zcash.ui.design.compat.FontCompat import co.electriccoin.zcash.ui.design.compat.FontCompat
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
@ -78,6 +78,7 @@ class MainActivity : ComponentActivity() {
} }
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
private fun setupUiContent() { private fun setupUiContent() {
setContent { setContent {
Override(configurationOverrideFlow) { Override(configurationOverrideFlow) {
@ -88,7 +89,7 @@ class MainActivity : ComponentActivity() {
.fillMaxHeight() .fillMaxHeight()
) { ) {
BindCompLocalProvider { BindCompLocalProvider {
val isEnoughSpace by storageCheckViewModel.isEnoughSpace.collectAsState() val isEnoughSpace by storageCheckViewModel.isEnoughSpace.collectAsStateWithLifecycle()
if (isEnoughSpace == false) { if (isEnoughSpace == false) {
WrapNotEnoughSpace() WrapNotEnoughSpace()
} else { } else {
@ -98,18 +99,17 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
// Force collection to improve performance; sync can start happening while // Force collection to improve performance; sync can start happening while
// the user is going through the backup flow. Don't use eager collection in the view model, // the user is going through the backup flow.
// so that the collection is still tied to UI lifecycle. walletViewModel.synchronizer.collectAsStateWithLifecycle()
walletViewModel.synchronizer.collectWith(lifecycleScope) {
} }
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun MainContent() { private fun MainContent() {
when (val secretState = walletViewModel.secretState.collectAsState().value) { when (val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value) {
SecretState.Loading -> { SecretState.Loading -> {
// For now, keep displaying splash screen using condition above. // For now, keep displaying splash screen using condition above.
// In the future, we might consider displaying something different here. // In the future, we might consider displaying something different here.

View File

@ -5,7 +5,8 @@ package co.electriccoin.zcash.ui.screen.address
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -17,6 +18,7 @@ internal fun MainActivity.WrapWalletAddresses(
WrapWalletAddresses(this, goBack) WrapWalletAddresses(this, goBack)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapWalletAddresses( private fun WrapWalletAddresses(
activity: ComponentActivity, activity: ComponentActivity,
@ -24,7 +26,8 @@ private fun WrapWalletAddresses(
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = walletViewModel.addresses.collectAsState().value val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Display loading indicator
} else { } else {

View File

@ -27,7 +27,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -38,6 +37,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.sdk.fixture.PersistableWalletFixture import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import cash.z.ecc.sdk.model.PersistableWallet import cash.z.ecc.sdk.model.PersistableWallet
import co.electriccoin.zcash.spackle.model.Index import co.electriccoin.zcash.spackle.model.Index
@ -78,7 +79,7 @@ fun ComposablePreview() {
/** /**
* @param onComplete Callback when the user has completed the backup test. * @param onComplete Callback when the user has completed the backup test.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLifecycleComposeApi::class)
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun BackupWallet( fun BackupWallet(
@ -89,7 +90,7 @@ fun BackupWallet(
onComplete: () -> Unit, onComplete: () -> Unit,
onChoicesChanged: ((choicesCount: Int) -> Unit)? onChoicesChanged: ((choicesCount: Int) -> Unit)?
) { ) {
val currentBackupStage = backupState.current.collectAsState().value val currentBackupStage = backupState.current.collectAsStateWithLifecycle().value
Scaffold( Scaffold(
topBar = { topBar = {
@ -123,6 +124,7 @@ fun BackupWallet(
} }
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun BackupMainContent( fun BackupMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
@ -138,7 +140,7 @@ fun BackupMainContent(
bottom = paddingValues.calculateBottomPadding() bottom = paddingValues.calculateBottomPadding()
) )
) { ) {
when (backupState.current.collectAsState().value) { when (backupState.current.collectAsStateWithLifecycle().value) {
is BackupStage.EducationOverview -> EducationOverview() is BackupStage.EducationOverview -> EducationOverview()
is BackupStage.EducationRecoveryPhrase -> EducationRecoveryPhrase() is BackupStage.EducationRecoveryPhrase -> EducationRecoveryPhrase()
is BackupStage.Seed -> SeedPhrase(wallet) is BackupStage.Seed -> SeedPhrase(wallet)
@ -216,6 +218,7 @@ private data class TestChoice(val originalIndex: Index, val word: String)
* - It is possible for the same word to appear twice in the word choices * - It is possible for the same word to appear twice in the word choices
* - The test answer ordering is not randomized, to ensure it can never be in the correct order to start with * - The test answer ordering is not randomized, to ensure it can never be in the correct order to start with
*/ */
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun TestInProgress( private fun TestInProgress(
splitSeedPhrase: List<String>, splitSeedPhrase: List<String>,
@ -233,7 +236,7 @@ private fun TestInProgress(
@Suppress("MagicNumber") @Suppress("MagicNumber")
listOf(it[1], it[0], it[3], it[2]) listOf(it[1], it[0], it[3], it[2])
} }
val currentSelectedTestChoice = selectedTestChoices.current.collectAsState().value val currentSelectedTestChoice = selectedTestChoices.current.collectAsStateWithLifecycle().value
if (currentSelectedTestChoice.size == testIndices.size) { if (currentSelectedTestChoice.size == testIndices.size) {
if (currentSelectedTestChoice.all { splitSeedPhrase[it.key.value] == it.value }) { if (currentSelectedTestChoice.all { splitSeedPhrase[it.key.value] == it.value }) {
// the user got the test correct // the user got the test correct

View File

@ -5,8 +5,9 @@ package co.electriccoin.zcash.ui.screen.home
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.EmulatorWtfUtil import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.BuildConfig import co.electriccoin.zcash.ui.BuildConfig
@ -34,6 +35,7 @@ internal fun MainActivity.WrapHome(
) )
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun WrapHome( internal fun WrapHome(
activity: ComponentActivity, activity: ComponentActivity,
@ -49,13 +51,14 @@ internal fun WrapHome(
AppUpdateCheckerImp.new() AppUpdateCheckerImp.new()
) )
} }
val updateAvailable = checkUpdateViewModel.updateInfo.collectAsState().value.let { val updateAvailable = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
it?.appUpdateInfo != null && it.state == UpdateState.Prepared it?.appUpdateInfo != null && it.state == UpdateState.Prepared
} }
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
if (null == walletSnapshot) { if (null == walletSnapshot) {
// Display loading indicator // Display loading indicator
} else { } else {
@ -67,9 +70,11 @@ internal fun WrapHome(
!FirebaseTestLabUtil.isFirebaseTestLab(context) && !FirebaseTestLabUtil.isFirebaseTestLab(context) &&
!EmulatorWtfUtil.isEmulatorWtf(context) !EmulatorWtfUtil.isEmulatorWtf(context)
val transactionSnapshot = walletViewModel.transactionSnapshot.collectAsStateWithLifecycle().value
Home( Home(
walletSnapshot, walletSnapshot,
walletViewModel.transactionSnapshot.collectAsState().value, transactionSnapshot,
goScan = goScan, goScan = goScan,
goRequest = goRequest, goRequest = goRequest,
goSend = goSend, goSend = goSend,

View File

@ -7,8 +7,9 @@ import android.content.Context
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.fixture.SeedPhraseFixture import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import cash.z.ecc.sdk.model.PersistableWallet import cash.z.ecc.sdk.model.PersistableWallet
@ -30,6 +31,7 @@ internal fun MainActivity.WrapOnboarding() {
WrapOnboarding(this) WrapOnboarding(this)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun WrapOnboarding( internal fun WrapOnboarding(
activity: ComponentActivity activity: ComponentActivity
@ -46,7 +48,7 @@ internal fun WrapOnboarding(
!EmulatorWtfUtil.isEmulatorWtf(applicationContext) !EmulatorWtfUtil.isEmulatorWtf(applicationContext)
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383 // TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
if (!onboardingViewModel.isImporting.collectAsState().value) { if (!onboardingViewModel.isImporting.collectAsStateWithLifecycle().value) {
Onboarding( Onboarding(
onboardingState = onboardingViewModel.onboardingState, onboardingState = onboardingViewModel.onboardingState,
isDebugMenuEnabled = isDebugMenuEnabled, isDebugMenuEnabled = isDebugMenuEnabled,
@ -93,6 +95,7 @@ internal fun WrapOnboarding(
} }
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapRestore(activity: ComponentActivity) { private fun WrapRestore(activity: ComponentActivity) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
@ -101,7 +104,7 @@ private fun WrapRestore(activity: ComponentActivity) {
val applicationContext = LocalContext.current.applicationContext val applicationContext = LocalContext.current.applicationContext
when (val completeWordList = restoreViewModel.completeWordList.collectAsState().value) { when (val completeWordList = restoreViewModel.completeWordList.collectAsStateWithLifecycle().value) {
CompleteWordSetState.Loading -> { CompleteWordSetState.Loading -> {
// Although it might perform IO, it should be relatively fast. // Although it might perform IO, it should be relatively fast.
// Consider whether to display indeterminate progress here. // Consider whether to display indeterminate progress here.

View File

@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -36,6 +35,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.Body import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
@ -69,7 +70,7 @@ fun ComposablePreview() {
* @param onImportWallet Callback when the user decides to import an existing wallet. * @param onImportWallet Callback when the user decides to import an existing wallet.
* @param onCreateWallet Callback when the user decides to create a new wallet. * @param onCreateWallet Callback when the user decides to create a new wallet.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun Onboarding( fun Onboarding(
onboardingState: OnboardingState, onboardingState: OnboardingState,
@ -78,7 +79,7 @@ fun Onboarding(
onCreateWallet: () -> Unit, onCreateWallet: () -> Unit,
onFixtureWallet: () -> Unit onFixtureWallet: () -> Unit
) { ) {
val currentStage = onboardingState.current.collectAsState().value val currentStage = onboardingState.current.collectAsStateWithLifecycle().value
Scaffold( Scaffold(
topBar = { topBar = {
OnboardingTopAppBar(onboardingState, isDebugMenuEnabled, onFixtureWallet) OnboardingTopAppBar(onboardingState, isDebugMenuEnabled, onFixtureWallet)
@ -95,13 +96,13 @@ fun Onboarding(
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLifecycleComposeApi::class)
private fun OnboardingTopAppBar( private fun OnboardingTopAppBar(
onboardingState: OnboardingState, onboardingState: OnboardingState,
isDebugMenuEnabled: Boolean, isDebugMenuEnabled: Boolean,
onFixtureWallet: () -> Unit onFixtureWallet: () -> Unit
) { ) {
val currentStage = onboardingState.current.collectAsState().value val currentStage = onboardingState.current.collectAsStateWithLifecycle().value
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) }, title = { Text(text = stringResource(id = R.string.app_name)) },
@ -149,6 +150,7 @@ private fun DebugMenu(onFixtureWallet: () -> Unit) {
* @param onImportWallet Callback when the user decides to import an existing wallet. * @param onImportWallet Callback when the user decides to import an existing wallet.
* @param onCreateWallet Callback when the user decides to create a new wallet. * @param onCreateWallet Callback when the user decides to create a new wallet.
*/ */
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
fun OnboardingMainContent( fun OnboardingMainContent(
paddingValues: PaddingValues, paddingValues: PaddingValues,
@ -157,7 +159,7 @@ fun OnboardingMainContent(
Column( Column(
Modifier.padding(top = paddingValues.calculateTopPadding()) Modifier.padding(top = paddingValues.calculateTopPadding())
) { ) {
val onboardingStage = onboardingState.current.collectAsState().value val onboardingStage = onboardingState.current.collectAsStateWithLifecycle().value
when (onboardingStage) { when (onboardingStage) {
OnboardingStage.ShieldedByDefault -> ShieldedByDefault(paddingValues) OnboardingStage.ShieldedByDefault -> ShieldedByDefault(paddingValues)

View File

@ -5,7 +5,8 @@ package co.electriccoin.zcash.ui.screen.profile
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.sdk.model.WalletAddresses import cash.z.ecc.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -34,6 +35,7 @@ internal fun MainActivity.WrapProfile(
) )
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal fun WrapProfile( internal fun WrapProfile(
@ -47,7 +49,7 @@ internal fun WrapProfile(
onAbout: () -> Unit onAbout: () -> Unit
) { ) {
val viewModel by activity.viewModels<WalletViewModel>() val viewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = viewModel.addresses.collectAsState().value val walletAddresses = viewModel.addresses.collectAsStateWithLifecycle().value
WrapProfile( WrapProfile(
walletAddresses, walletAddresses,

View File

@ -7,7 +7,8 @@ import android.content.Intent
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.sdk.model.ZecRequest import cash.z.ecc.sdk.model.ZecRequest
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -22,13 +23,14 @@ internal fun MainActivity.WrapRequest(
WrapRequest(this, goBack) WrapRequest(this, goBack)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapRequest( private fun WrapRequest(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit goBack: () -> Unit
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val walletAddresses = walletViewModel.addresses.collectAsState().value val walletAddresses = walletViewModel.addresses.collectAsStateWithLifecycle().value
if (null == walletAddresses) { if (null == walletAddresses) {
// Display loading indicator // Display loading indicator

View File

@ -52,6 +52,8 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.sdk.model.SeedPhraseValidation import cash.z.ecc.sdk.model.SeedPhraseValidation
import co.electriccoin.zcash.spackle.model.Index import co.electriccoin.zcash.spackle.model.Index
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -185,6 +187,7 @@ private fun RestoreTopAppBar(onBack: () -> Unit, onClear: () -> Unit) {
// TODO [#672] Implement custom seed phrase pasting for wallet import // TODO [#672] Implement custom seed phrase pasting for wallet import
// TODO [#672] https://github.com/zcash/secant-android-wallet/issues/672 // TODO [#672] https://github.com/zcash/secant-android-wallet/issues/672
@OptIn(ExperimentalLifecycleComposeApi::class)
@Suppress("UNUSED_PARAMETER", "LongParameterList") @Suppress("UNUSED_PARAMETER", "LongParameterList")
@Composable @Composable
private fun RestoreMainContent( private fun RestoreMainContent(
@ -195,7 +198,7 @@ private fun RestoreMainContent(
parseResult: ParseResult, parseResult: ParseResult,
paste: () -> String? paste: () -> String?
) { ) {
val currentUserWordList = userWordList.current.collectAsState().value val currentUserWordList = userWordList.current.collectAsStateWithLifecycle().value
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@ -6,9 +6,10 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -29,6 +30,7 @@ internal fun MainActivity.WrapScanValidator(
) )
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapScanValidator( private fun WrapScanValidator(
activity: ComponentActivity, activity: ComponentActivity,
@ -37,7 +39,8 @@ private fun WrapScanValidator(
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsState().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (synchronizer == null) { if (synchronizer == null) {
// Display loading indicator // Display loading indicator
} else { } else {

View File

@ -5,7 +5,8 @@ package co.electriccoin.zcash.ui.screen.seed
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.copyToClipboard import co.electriccoin.zcash.ui.screen.backup.copyToClipboard
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
@ -19,6 +20,7 @@ internal fun MainActivity.WrapSeed(
WrapSeed(this, goBack) WrapSeed(this, goBack)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapSeed( private fun WrapSeed(
activity: ComponentActivity, activity: ComponentActivity,
@ -27,14 +29,15 @@ private fun WrapSeed(
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val persistableWallet = run { val persistableWallet = run {
val secretState = walletViewModel.secretState.collectAsState().value val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
if (secretState is SecretState.Ready) { if (secretState is SecretState.Ready) {
secretState.persistableWallet secretState.persistableWallet
} else { } else {
null null
} }
} }
val synchronizer = walletViewModel.synchronizer.collectAsState().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer || null == persistableWallet) { if (null == synchronizer || null == persistableWallet) {
// Display loading indicator // Display loading indicator
} else { } else {

View File

@ -5,8 +5,9 @@ package co.electriccoin.zcash.ui.screen.send
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.sdk.send import cash.z.ecc.sdk.send
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
@ -21,6 +22,7 @@ internal fun MainActivity.WrapSend(
WrapSend(this, goBack) WrapSend(this, goBack)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapSend( private fun WrapSend(
activity: ComponentActivity, activity: ComponentActivity,
@ -29,9 +31,11 @@ private fun WrapSend(
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val synchronizer = walletViewModel.synchronizer.collectAsState().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val spendableBalance = walletViewModel.walletSnapshot.collectAsState().value?.spendableBalance()
val spendingKey = walletViewModel.spendingKey.collectAsState().value val spendableBalance = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value?.spendableBalance()
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
if (null == synchronizer || null == spendableBalance || null == spendingKey) { if (null == synchronizer || null == spendableBalance || null == spendingKey) {
// Display loading indicator // Display loading indicator
} else { } else {

View File

@ -5,7 +5,8 @@ package co.electriccoin.zcash.ui.screen.settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
@ -23,6 +24,7 @@ internal fun MainActivity.WrapSettings(
) )
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapSettings( private fun WrapSettings(
activity: ComponentActivity, activity: ComponentActivity,
@ -31,7 +33,8 @@ private fun WrapSettings(
) { ) {
val walletViewModel by activity.viewModels<WalletViewModel>() val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsState().value val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer) { if (null == synchronizer) {
// Display loading indicator // Display loading indicator
} else { } else {

View File

@ -7,9 +7,10 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.support.model.SupportInfo import co.electriccoin.zcash.ui.screen.support.model.SupportInfo
@ -26,13 +27,14 @@ internal fun MainActivity.WrapSupport(
WrapSupport(this, goBack) WrapSupport(this, goBack)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
internal fun WrapSupport( internal fun WrapSupport(
activity: ComponentActivity, activity: ComponentActivity,
goBack: () -> Unit goBack: () -> Unit
) { ) {
val viewModel by activity.viewModels<SupportViewModel>() val viewModel by activity.viewModels<SupportViewModel>()
val supportMessage = viewModel.supportInfo.collectAsState().value val supportMessage = viewModel.supportInfo.collectAsStateWithLifecycle().value
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@ -7,9 +7,10 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
@ -26,6 +27,7 @@ internal fun MainActivity.WrapCheckForUpdate() {
WrapCheckForUpdate(this) WrapCheckForUpdate(this)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapCheckForUpdate(activity: ComponentActivity) { private fun WrapCheckForUpdate(activity: ComponentActivity) {
// TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382 // TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382
@ -38,7 +40,7 @@ private fun WrapCheckForUpdate(activity: ComponentActivity) {
) )
} }
val updateInfo = checkUpdateViewModel.updateInfo.collectAsState().value val updateInfo = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value
updateInfo?.let { updateInfo?.let {
if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) { if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) {
@ -53,6 +55,7 @@ private fun WrapCheckForUpdate(activity: ComponentActivity) {
} }
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapUpdate( private fun WrapUpdate(
activity: ComponentActivity, activity: ComponentActivity,
@ -69,7 +72,7 @@ private fun WrapUpdate(
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val updateInfo = viewModel.updateInfo.collectAsState().value val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value
when (updateInfo.state) { when (updateInfo.state) {
UpdateState.Done, UpdateState.Canceled -> { UpdateState.Done, UpdateState.Canceled -> {

View File

@ -5,8 +5,9 @@ package co.electriccoin.zcash.ui.screen.warning
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.warning.view.NotEnoughSpaceView import co.electriccoin.zcash.ui.screen.warning.view.NotEnoughSpaceView
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
@ -16,10 +17,11 @@ fun MainActivity.WrapNotEnoughSpace() {
WrapNotEnoughSpace(this) WrapNotEnoughSpace(this)
} }
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable @Composable
private fun WrapNotEnoughSpace(activity: ComponentActivity) { private fun WrapNotEnoughSpace(activity: ComponentActivity) {
val storageCheckViewModel by activity.viewModels<StorageCheckViewModel>() val storageCheckViewModel by activity.viewModels<StorageCheckViewModel>()
val spaceRequiredToContinue by storageCheckViewModel.spaceRequiredToContinueMegabytes.collectAsState() val spaceRequiredToContinue by storageCheckViewModel.spaceRequiredToContinueMegabytes.collectAsStateWithLifecycle()
NotEnoughSpaceView( NotEnoughSpaceView(
storageSpaceRequiredGigabytes = storageCheckViewModel.requiredStorageSpaceGigabytes, storageSpaceRequiredGigabytes = storageCheckViewModel.requiredStorageSpaceGigabytes,