Not sure if I'm using flow + collectAsState correc...
# compose
c
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
Copy code
val happy: Flow<Boolean> = prefs.data
    .map { preferences ->
        preferences[isHappy] ?: false
    }
and then I show it via
Copy code
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
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
Interesting. This seems pretty "bad" but it works! My ViewModel
Copy code
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
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
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
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
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
Yea, I had same problem @Colton Idle, seen that runBlocking solution, but as @Francesc said.
n
@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
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
@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
You can call collectAsState without initializer and let the model provide the initial state. That’s what I normally do.
o
@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
Ahh yes, I do use `StateFlow`s.
j
There is a stateIn which doesn’t need an initial value
o
@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
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
Oh. Maybe I should convert to state flow
o
@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
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
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
Copy code
val isLoggedIn = preferenceManager.isLoggedIn.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(),
    initialValue = null
)
Inside activity
Copy code
lifecycleScope.launchWhenCreated {
    mainViewModel.isLoggedIn.collect {
        it?.apply {
            if (this) {
                setContent {
                    MainScreen()
                }
            } else {
                setContent {
                    AuthScreen()
                }
            }
        }
    }
}
2
i
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
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.
478 Views