Travis Griggs
04/24/2023, 6:13 PMIan Lake
04/24/2023, 6:20 PMTravis Griggs
04/24/2023, 7:59 PMStylianos Gakis
04/24/2023, 9:08 PMrunBlocking {}
to get the value from inside the DataStore.
In general, it’s a good idea to have some sort of sane default, or have some loading state in your screen while you’re still fetching that state from the DataStore.Travis Griggs
04/24/2023, 9:28 PMdata class VisibilityOptions(
val showID: Boolean = false, val showPower: Boolean = false, val showSignal: Boolean = false
)
I've managed to do the following so far:
Added the following line at a top level, as suggested by the blog above:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "x.y.z")
Then I added "persist" and "recall" functions to my VisibilityOptions:
suspend fun persist(context: Context) {
context.dataStore.edit { pref ->
pref[idKey] = showID
pref[powerKey] = showPower
pref[signalKey] = showSignal
}
}
companion object {
private val idKey = booleanPreferencesKey("status_showID")
private val powerKey = booleanPreferencesKey("status_showPower")
private val signalKey = booleanPreferencesKey("status_showSignal")
suspend fun recall(context: Context): VisibilityOptions {
return context.dataStore.data.map { pref ->
VisibilityOptions(
showID = pref[idKey] ?: false,
showPower = pref[powerKey] ?: false,
showSignal = pref[signalKey] ?: false
)
}.single()
}
}
And then I tried to use it thusly:Travis Griggs
04/24/2023, 9:30 PMval context = LocalContext.current
var visibilityOptions by remember { mutableStateOf(VisibilityOptions()) }
LaunchedEffect(true) { visibilityOptions = VisibilityOptions.recall(context) }
LaunchedEffect(visibilityOptions) { visibilityOptions.persist(context)}
StatusHeader(repo = repo,
options = visibilityOptions,
onOptionsChange = { update -> visibilityOptions = update })
The first launched effect is just supposed to run the first time and update the value past the default creation. And the onOptionsChange will update the visibilityOptions, which the other LaunchedEffect should persist. Unfortunately, this blows up because apparently single() doesn't like that when it changes. There's a piece of this "new" puzzle that hasn't clicked for me yetStylianos Gakis
04/24/2023, 9:38 PMcollectAsStateWithLifecycle(defaultLoadingValue)
in your compose state?
What is it that “blows up” exactly that you are describing though? single()
should just take whatever the current state is and return immediately after receiving the first value that it got. So what goes wrong in this process?Travis Griggs
04/24/2023, 9:49 PMTravis Griggs
04/24/2023, 9:52 PMFATAL EXCEPTION: main Process: com.example.statusdemo, PID: 27518 java.lang.IllegalArgumentException: Flow has more than one element at kotlinx.coroutines.flow.FlowKt__ReduceKt$single$2.emit(Reduce.kt:58) at com.example.statusdemo.composables.VisibilityOptions$Companion$recall$$inlined$map$1$2.emit(Emitters.kt:223) at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15) at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15) at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:87) at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66) at androidx.datastore.core.DataStoreImpl$data$1$invokeSuspend$$inlined$map$1$2.emit(Emitters.kt:223) at kotlinx.coroutines.flow.FlowKt__LimitKt$dropWhile$1$1.emit(Limit.kt:40) at kotlinx.coroutines.flow.FlowKt__LimitKt$takeWhile$lambda-6$$inlined$collectWhile$1.emit(Limit.kt:143) at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:398)
Stylianos Gakis
04/24/2023, 9:52 PMsingle()
, if you just want the current value, you can use first()
instead.Travis Griggs
04/24/2023, 9:55 PMTravis Griggs
04/24/2023, 9:57 PMTravis Griggs
04/24/2023, 9:57 PMStylianos Gakis
04/24/2023, 10:14 PMrunBlocking
everywhere I’d assume 😅
About using them correctly, I guess technically this works, but I really wouldn’t add all this logic in my composable directly.
Optimally somewhere else, in a ViewModel for example, there’d be a UiState exposed, maybe through a StateFlow, which would contain this state in it. While also handling initially starting with a sane default/loading state, and then all the UI code has to do is collectAsState
and you get a value from the first frame, without having to do all this.
Then your functions which mutate the state would be function calls on the ViewModel, which would change that state and you’d automatically get the new state in your UI as you’re listening to the flow.
NowInAndroid does this in some places. For example, here’s a “DataSource” class which basically just hides the DataStore under the hood, they got a bunch of those, but you won’t need so many layers in-between. You can just have it directly in your ViewModel. And then a ViewModel example is interacting with it like this, exposing the internal UiState, while initializing with some Loading
state, and exposing a function which launches a coroutine which changes the state, which as I said before will automatically also update the uiState, which will update the compose state and so on 😄 All in a nice little loop 😅Travis Griggs
04/24/2023, 10:18 PMrunBlocking
... yes. But I felt the scorn/shame of the new coroutine kids on the block if I were to do that. Trying to "when in Rome..." here. But I swear, the ideology evolves/shifts steadilyStylianos Gakis
04/24/2023, 10:25 PMColton Idle
04/25/2023, 1:01 PMColton Idle
04/25/2023, 1:01 PMColton Idle
04/25/2023, 1:01 PMTravis Griggs
04/25/2023, 4:54 PM