https://kotlinlang.org logo
Title
c

Colton Idle

09/05/2021, 4:01 AM
Not sure if I'm using flow + collectAsState correctly (it's my first time using a Flow, but a library I'm using exposes a flow, so I suppose it makes sense to use it) Code in thread
My ViewModel
val happy: Flow<Boolean> = prefs.data
    .map { preferences ->
        preferences[isHappy] ?: false
    }
and then I show it via
Text(text = "happy: " + vm.happy.collectAsState(false).value)
The problem is that even after I save to prefs after setting happy to true, when I restart the app I see, for a split second that the text says "happy: false" even though the flowable is actually true. I think what I'm really after is maybe NOT using a flowable in this case, because
happy
is critical to my application (think logged in state bool or something) and so I guess I don't actually want this as a flowable? I want my application to wait until I have that first value. Any help is appreciated!
k

K Merle

09/05/2021, 4:26 AM
You'll have to retrieve default value synchronously or replace DataStore with SharedPreference and use some kind of hot flow lib to handle the actual flow.
c

Colton Idle

09/05/2021, 4:46 AM
Interesting. This seems pretty "bad" but it works! My ViewModel
val happy: Flow<Boolean> = prefs.data
    .map { preferences ->
        preferences[isHappy] ?: false
    }

val happySynchronous: Boolean = runBlocking {
        prefs.data
    .map { preferences ->
        preferences[isHappy] ?: false
    }
}

Text(text = "happy: " + vm.happy.collectAsState(vm.happySynchronous).value)
r

rajesh

09/05/2021, 4:58 AM
Reading from data store should be pretty fast, even without one notice it. In my app, I use StateFlow with default value which fetch data from datastore.
c

Colton Idle

09/05/2021, 5:06 AM
The problem that this is what I use for "loggedIn" state, and so when the app thinks it's logged out it launches the login screen, even though I'm logged in. In most cases, yes it would be fast enough, but since I react to it with a navigational event, that's giving me issues.
r

rajesh

09/05/2021, 5:08 AM
That's the same case that I use (tracking loggedIn state) without any problem. And app launches main screen correctly when user is logged in.
f

Francesc

09/05/2021, 5:12 AM
Using runBlocking defeats the purpose of using data store. Data store is meant to avoid the pitfalls of shared preferences, and this gets us back to square one.
k

K Merle

09/05/2021, 6:30 AM
Yea, I had same problem @Colton Idle, seen that runBlocking solution, but as @Francesc said.
n

Nikola Drljaca

09/05/2021, 6:56 AM
@Colton Idle Perhaps you could use
stateIn()
to convert the flow into a state(hot) flow. Or maybe since the value is boolean you could use
.first()
on the flow to retrieve one value. Its a terminal operator so it should give you the value right away.
m

Michael Paus

09/05/2021, 7:01 AM
I don’t understand why you provide an initial state on the view side. To my opinion the model should know its initial state and provide it to the view and not vice versa.
k

K Merle

09/05/2021, 7:29 AM
@Michael Paus DataStore also has default value, which is usually hardcoded. So having default value
false
and flow returning
true
makes bad UX sometimes.
m

Michael Paus

09/05/2021, 7:33 AM
You can call collectAsState without initializer and let the model provide the initial state. That’s what I normally do.
o

Oleksandr Balan

09/05/2021, 7:43 AM
@Michael Paus I guess you can call
collectAsState
without init value only on the
StateFlow
. So you have to use
stateIn
to convert Flow from DataStore to the StateFlow. But
stateIn
requires an inital value.
m

Michael Paus

09/05/2021, 7:45 AM
Ahh yes, I do use `StateFlow`s.
j

Javier

09/05/2021, 8:54 AM
There is a stateIn which doesn’t need an initial value
o

Oleksandr Balan

09/05/2021, 1:40 PM
@Javier
stateIn
without initial value is
suspend
method, thus it may be not be possible to use when defining a property baked by flow from DataStore. @Colton Idle I would say in such case the third state should by used and set as initial value. Either
null
or convert boolean from DataStore to some 3-state enum: unknown / happy / unhappy 🤷
j

Javier

09/05/2021, 1:49 PM
I don’t see the problem, you can just use suspend fun in your repositories and pass the scope
same that you can do with retrofit or ktor requests
c

Colton Idle

09/05/2021, 2:15 PM
Oh. Maybe I should convert to state flow
o

Oleksandr Balan

09/05/2021, 2:25 PM
@Javier Sure, but in the end you want a StateFlow as a property in ViewModel so in Compose you can covert it to the State. But in property definition (
val happy: StateFlow = ...
) you cannot use suspend functions.
j

Javier

09/05/2021, 2:28 PM
you can have a stateFlow property in the viewModel, and collect the data store flow without problems, as MVI approach is doing
you can just collect the flow in some vm function and emit in the stateflow for example
r

rajesh

09/05/2021, 2:31 PM
I'm sharing the approach that i use in my app (I'm observing
isLoggedIn
StateFlow in main activity as it should decide which compose screen to display, but you can use similar in compose screen) Inside viewmodel
val isLoggedIn = preferenceManager.isLoggedIn.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    initialValue = null
)
Inside activity
lifecycleScope.launchWhenCreated {
    mainViewModel.isLoggedIn.collect {
        it?.apply {
            if (this) {
                setContent {
                    MainScreen()
                }
            } else {
                setContent {
                    AuthScreen()
                }
            }
        }
    }
}
2
i

Ian Lake

09/05/2021, 10:10 PM
Maybe what you actually want is a tri-state -
Loading
,
LoggedIn
, and
LoggedOut
- you really don't want to ever be blocking the UI thread on disk access (and any persisted data is, by definition, disk access, at some level). That way you can delay your UI until you're out of the
Loading
state
c

Colton Idle

09/05/2021, 10:23 PM
Yeah, that's true. Although, with some of the guidance given by the android 12 splash screen documentation it seems like have "some" small piece of user preference retrieval in the critical path is "fine". Even like just trying to fetch "theme_preference" would bring me right back to this same problem because my designers want to keep showing the splash screen until we know the theme that the user has chosen for our app.