Is SharedPreferences still the recommended way to ...
# compose-android
t
Is SharedPreferences still the recommended way to store some persistant keyed boolean flags across invocations with greenfield Compose apps?
t
Thanks. I'd like to use this. My uses in the past of SharedPreferences have been synchronous (I guess). I just do boolAt type of thing and initialize local objects. But it seems I need to approach this a little differently, because this seems very much to want me to do something asynchronous. I have little to zero experience with Flow yet (been just happily plugging away with MutableState so far). So it's a little unclear how I initialize my composition remembered "options" with values previously archived in the data store.
s
SharedPreferences blocks the UI thread when you do this blocking get, it’d be the same as doing
runBlocking {}
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.
t
Right, I have a default values. Looks like this:
Copy code
data 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:
Copy code
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "x.y.z")
Then I added "persist" and "recall" functions to my VisibilityOptions:
Copy code
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:
Copy code
val 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 yet
s
Instead of having two sources of truth and having to manually sync them up, why not make it all listen to the DataStore directly, without a local state that you edit locally and then syncing it to the datastore? Could take the flow which comes from the datastore, and consume it using
collectAsStateWithLifecycle(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?
t
In part, because I have zero experience with Flows. 😄 I've been enjoying learning compose with as minimal "extras" and "baggage" as possible.
In answer to "what blows up", on the first "write", it silently dies and the Run console shows nothing, but in the bowels of the ever cavorting Logcat, I find this:
Copy code
FATAL 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)
s
Ah of course, you used
single()
, if you just want the current value, you can use
first()
instead.
t
that worked, thank you. I had to make my response by optional ? and used a ?.let to set it initially.
I like the idea of one source of truth for sure. But I also liked how easy it was to develop the original version and makes the interfaces nice and primitive. For example, it's very easy to build previews where I'm just passing true/false option flags. It's a little unclear how I do that otherwise. Am I using LaunchedEffect in the correct way?
This definitely did not feel "easier" than the SharedPreferences method. But then, simple blocking code is always easier, until it's not.
s
It’d feel equally easy if you just did
runBlocking
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 😅
t
runBlocking
... 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 steadily
s
I am only suggesting it since it’s the same as using SharedPrefs with the blocking APIs it provides. Definitely don’t recommend actually doing so 😄
c
I think the datastore docs specifically mention that its async... and if you want it to be sync then use runBlocking...
so if the docs say it... it must be okay? 🙃
but yeah. datastore prefs seems like a lot of overhead every time i set it up, but i just started to copy pasta it into every project and so its not so bad.
t
Well just be careful you don't malign the async folks