Pablo
03/05/2025, 5:21 PMZach Klippenstein (he/him) [MOD]
03/05/2025, 5:56 PMmust it be modified in Dispatchers.Main to be able to triger a recompositionNope, you can write state objects from any thread. However, you may want to consider wrapping the write in a
Snapshot.withMutableSnapshot
especially if you're potentially updating multiple states, to ensure observers see all the changes together.Pablo
03/05/2025, 7:14 PMZach Klippenstein (he/him) [MOD]
03/05/2025, 8:15 PMPablo
03/05/2025, 8:44 PM@Stable
class RateRequestDialogState(
private val dataStoreRepository: DefaultDataStoreRepository,
private val coroutineScope: CoroutineScope
) {
var shouldDisplayDialog by mutableStateOf(false)
private set
private var dontShowAgain by mutableStateOf(false)
private var launchCount by mutableIntStateOf(0)
private var firstLaunchDate by mutableLongStateOf(0L)
init {
coroutineScope.launch(Dispatchers.IO) {
dontShowAgain = dataStoreRepository.readBoolean(DataStoreHelper.Key.RATE_DIALOG_DONT_SHOW_AGAIN.value).first()
if (dontShowAgain)
return@launch
// Increment launch counter
launchCount = dataStoreRepository.readInt(DataStoreHelper.Key.RATE_DIALOG_LAUNCH_COUNT.value).first()
launchCount++
dataStoreRepository.saveInt(DataStoreHelper.Key.RATE_DIALOG_LAUNCH_COUNT.value, launchCount)
// Get date of first launch
firstLaunchDate = dataStoreRepository.readLong(DataStoreHelper.Key.RATE_DIALOG_FIRST_LAUNCH_DATE.value).first()
if (firstLaunchDate == 0L) {
firstLaunchDate = System.currentTimeMillis()
dataStoreRepository.saveLong(
DataStoreHelper.Key.RATE_DIALOG_FIRST_LAUNCH_DATE.value,
firstLaunchDate
)
}
// Wait at least n days before opening
if (launchCount >= LAUNCHES_UNTIL_PROMPT) {
if (System.currentTimeMillis() >= firstLaunchDate + (DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000)) {
shouldDisplayDialog = true
}
}
}
}
}
the remember function to generate the state holder and remember it:
@Composable
fun rememberRateRequestDialogState(
dataStoreRepository: DefaultDataStoreRepository,
coroutineScope: CoroutineScope = rememberCoroutineScope()
): RateRequestDialogState {
val rateRequestDialogState = remember(
dataStoreRepository,
coroutineScope
) {
RateRequestDialogState(
dataStoreRepository = dataStoreRepository,
coroutineScope = coroutineScope
)
}
DisposableEffect(Unit) {
onDispose {
rateRequestDialogState.dispose()
}
}
return rateRequestDialogState
}
how I generate the state holder and how I use the variable in a composable:
val stateHolder = rememberRateRequestDialogState(
dataStoreRepository = koinInject()
)
if (stateHolder.shouldDisplayDialog)
RateRequestDialog({ stateHolder.hideDialog() }, R.mipmap.ic_launcher_foreground)
}
Pablo
03/05/2025, 8:46 PMif (stateHolder.shouldDisplayDialog)
of the composable is not called again when the shouldDisplayDialog
is set to true in the init method of the state holder, because is not being recomposed. But it works if I change Dispatchers.IO to Dispatchers.Main in the init method of the state holderPablo
03/05/2025, 8:52 PMPablo
03/05/2025, 8:53 PMPablo
03/05/2025, 8:59 PMPablo
03/05/2025, 9:50 PMZach Klippenstein (he/him) [MOD]
03/05/2025, 10:34 PMZach Klippenstein (he/him) [MOD]
03/05/2025, 10:35 PMZach Klippenstein (he/him) [MOD]
03/05/2025, 10:35 PMPablo
03/05/2025, 10:36 PMPablo
03/05/2025, 10:36 PM// Increment launch counter
Log.d("XXXX", "DialogState before readint launchCount: ${launchCount}")
launchCount = dataStoreRepository.readInt(DataStoreHelper.Key.RATE_DIALOG_LAUNCH_COUNT.value).first()
Log.d("XXXX", "DialogState after readint launchCount: ${launchCount}")
launchCount++
Log.d("XXXX", "DialogState after adding 1 launchCount: ${launchCount}")
dataStoreRepository.saveInt(DataStoreHelper.Key.RATE_DIALOG_LAUNCH_COUNT.value, launchCount)
Log.d("XXXX", "DialogState after saveint launchCount: ${launchCount}")
Pablo
03/05/2025, 10:36 PMPablo
03/05/2025, 10:38 PM2025-03-05 23:10:31.284 10749-10769 XXXX <http://com.app|com.app> D DialogState before readint launchCount: 0
2025-03-05 23:10:31.285 10749-10769 XXXX <http://com.app|com.app> D readInt returning flow
2025-03-05 23:10:31.286 10749-10769 XXXX <http://com.app|com.app> D DialogState after readint launchCount: 0
2025-03-05 23:10:31.291 10749-10769 XXXX <http://com.app|com.app> D DialogState after sumar 1 launchCount: 1
2025-03-05 23:10:31.318 10749-10782 XXXX <http://com.app|com.app> D saveInt saving flow
2025-03-05 23:10:31.566 10749-10782 XXXX <http://com.app|com.app> D DialogState after saveint launchCount: 0
Pablo
03/05/2025, 10:39 PMPablo
03/05/2025, 10:39 PMPablo
03/05/2025, 10:39 PMPablo
03/05/2025, 10:40 PMPablo
03/05/2025, 10:40 PMPablo
03/05/2025, 10:40 PMPablo
03/05/2025, 10:51 PMPablo
03/05/2025, 10:55 PMval stateHolder = rememberRateRequestDialogState(
dataStoreRepository = koinInject()
)
LaunchedEffect(true) {
stateHolder.initialize()
}
if (stateHolder.shouldDisplayDialog)
RateRequestDialog({ stateHolder.hideDialog() }, R.mipmap.ic_launcher_foreground)
You mean that this is the correct way?Pablo
03/05/2025, 10:58 PMZach Klippenstein (he/him) [MOD]
03/07/2025, 7:33 PMZach Klippenstein (he/him) [MOD]
03/07/2025, 7:37 PMremember
lambda, and also why doing side effects from a constructor is generally bad. Side effects should be a separate explicit thing from object construction (for this reason, and also for making testing easier)Pablo
03/07/2025, 7:47 PM