StateFlow setting value drops some events, but updating doesn't - android

In my app, I have a UIState sealed class to represent the UI state.
sealed class UIState<T> {
class ShowLoading<T> : UIState<T>()
class HideLoading<T> : UIState<T>()
class ShowEmptyData<T> : UIState<T>()
class ShowData<T>(val data: T) : UIState<T>()
class ShowError<T>(val errorUIState: ErrorUIState) : UIState<T>()
}
Accordingly, my viewmodel code is:
someRequest.apply { response ->
when (response) {
is ApiResponse.Success -> {
_uiStateFlow.value = (UIState.HideLoading()) // First, hide the loading
// Do some work
_uiStateFlow.value = (UIState.ShowData(data))
}
is ApiResponse.Error -> {
_uiStateFlow.value = (UIState.ShowError(error))
}
}
}
In this case, most of the time my hideLoading state doesn't collect, it drops because the success/error state comes immediately after hideLoading, and my UI doesn't collect it. E.g. If I will delay success/error states set for 100ms, hideLoading will be collected from the UI.
I use collect instead of collectLatest.
But then I've found out, that when I change the set value part to update {}, the UI collects all the states.
someRequest.apply { response ->
when (response) {
is ApiResponse.Success -> {
_uiStateFlow.update { (UIState.HideLoading()) } // First, hide the loading
// Do some work
_uiStateFlow.update { (UIState.ShowData(data)) }
}
is ApiResponse.Error -> {
_uiStateFlow.update { (UIState.ShowError(error)) }
}
}
}
So what's the difference between .value and update and why this one works pretty well? Thank you.
P.S. I've used emit() as well. Under the hood it's the same as .value, it's just a suspend function.

As stated in the StateFlow documentation:
Updates to the value are always conflated.
Conflated means if values are posted faster than they are collected, then the collector only gets the most recent result. This allows values to always be posted to the StateFlow without having to wait for old values to be collected.
As for why update is allowing that loading state through, I suspect it is only because an atomic update generally takes a little bit longer, and so the collector usually wins the race, in this specific case on your specific test device. This is not a reliable solution to ensuring collectors get all intermediate values.
I'm not seeing why you need a HideLoading state in the first place. Your UI can simply hide the loading state automatically when it receives data to show. Logically, the loading is complete when the data is returned.
If you truly do need this HideLoading state, you should use a SharedFlow with a replay value that is large enough to ensure it is not skipped. But this presents other problems, like collectors possibly getting outdated data to show because it's being replayed to them.
Side note, your sealed class should probably be a sealed interface since it holds no state, and its children that hold no state can be objects with a generic type of Nothing so you don't have to keep instantiating them just to use them and never have to needlessly specify a generic type when they never hold that type anyway. And since the other ones are data wrappers, they might as well be data classes. Like this:
sealed interface UIState<out T> {
object ShowLoading : UIState<Nothing>
// object HideLoading : UIState<Nothing>
object ShowEmptyData : UIState<Nothing>
data class ShowData<out T>(val data: T) : UIState<T>
data class ShowError(val errorUIState: ErrorUIState) : UIState<Nothing>
}

Related

How to update my composable upon service property update?

I'm using a service locator (as advised in https://developer.android.com/training/dependency-injection#di-alternatives, but I'll switch to proper DI later I promise) to handle auth in my app. I have an authentication service that has a user property that I set and unset using logIn and logOut methods
I'd like my ContentView to react to changes in auth.user but I can't quite figure out how. I've tried wrapping it into by remember { mutableStateOf() } but I don't see any update upon login.. any idea what I am missing?
Thanks in advance! (snippets below)
#Composable
fn ContentView() {
val auth = ServiceLocator.auth
var loggedInUser: User? by remember { mutableStateOf(auth.user) } // <-- I would like my composable to react to changes to auth.user
if (loggedInUser) {
ViewA()
} else {
ViewB()
}
}
object ServiceLocator {
val auth = AuthenticationService()
}
class AuthenticationService {
var user: User? = null
fun logIn() {
// sets user...
}
fun logOut() {
// undefs user...
}
In your code snippet on this line
var loggedInUser: User? by remember { mutableStateOf(auth.user) }
you are creating an instance of MutableState<User?> with an initial value of the value that is at that time referenced by auth.user.
Due to remember { } this initialization happens only when the composable ContentView enters composition and then the MutableState instance is remembered across recompositions and reused.
If you later change the variable auth.user no recomposition will happen, because the value stored in loggedInUser (in the mutable state) has not changed.
The documentation for mutableStateOf explains what this call actually does behind the scenes
Return a new MutableState initialized with the passed in value.
The MutableState class is a single value holder whose reads and writes are observed by Compose. Additionally, writes to it are transacted as part of the Snapshot system.
Let's dissect this piece of information.
Return a new MutableState initialized with the passed in value.
Calling mutableStateOf returns a MutableState instance that is initialized with the value that was passed as the parameter.
The MutableState class is a single value holder
Every instance of this class stores a single value of state. It might store other values for the implementation purposes, but it exposes only a single value of state.
whose reads and writes are observed by Compose
Compose observes reads and writes that happen to instances of MutableState
This is the piece of information that you have missed.
The writes need to happen to the instance of the MutableState (loggedInUser in your case), not to the variable that has been passed in as the initial value (auth.user in your case).
If you really think about it, there is no built-in mechanism in Kotlin to observe changes to a variable, so it is understandable that there has to be a wrapper for Compose to be able to observe the changes. And that we have to change the state through the wrapper instead of changing the variable directly.
Knowing all that you could just move the mutable state into AuthenticationService and things would work
import androidx.compose.runtime.mutableStateOf
class AuthenticationService {
var user: User? by mutableStateOf(null)
private set
// ... rest of the service
}
#Composable
fun ContentView() {
val auth = ServiceLocator.auth
// no remember { } block this time because now the MutableState reference is being kept by
// the AuthenticationService so it won't reset on recomposition
val loggedInUser = auth.user
if (loggedInUser != null) {
ViewA()
} else {
ViewB()
}
}
However now your AuthenticationService depends on mutableStateOf and thus on the Composable runtime which you might want to avoid. A "Service" (or Repository) should not need to know details about the UI implementation.
There are other options to track state changes and not depend on Compose runtime. From the documentation section Compose and other libraries
Compose comes with extensions for Android's most popular stream-based
solutions. Each of these extensions is provided by a different
artifact:
Flow.collectAsState() doesn't require extra dependencies. (because it is part of kotlinx-coroutines-core)
LiveData.observeAsState() included in the androidx.compose.runtime:runtime-livedata:$composeVersion artifact.
Observable.subscribeAsState() included in the androidx.compose.runtime:runtime-rxjava2:$composeVersion or
androidx.compose.runtime:runtime-rxjava3:$composeVersion artifact.
These artifacts register as a listener and represent the values as a
State. Whenever a new value is emitted, Compose recomposes those parts
of the UI where that state.value is used.
Example using a Kotlin MutableStateFlow
// No androidx.compose.* dependencies anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class AuthenticationService {
private val user = MutableStateFlow<User?>(null)
val userFlow = user.asStateFlow()
fun logIn() {
user.value = User(/* potential parameters */)
}
fun logOut() {
user.value = null
}
}
And then in the composable we collect the flow as state.
import androidx.compose.runtime.collectAsState
#Composable
fun ContentView() {
val auth = ServiceLocator.auth
val loggedInUser = auth.userFlow.collectAsState().value
if (loggedInUser != null) {
ViewA()
} else {
ViewB()
}
}
To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.
Eventually (when you app grows in complexity) you might want to put another layer between your Service/Repository layer and your UI layer (the composables). A layer that will hold and manage the UI state so you will be able to cover both positive outcomes (a successful login) and negative outcomes (a failed login).
If you are going the MVVM (Model-View-ViewModel) way or the MVI (Model-View-Intent) way, that layer would be covered by ViewModels. In that case the composables manage only some transient UI state themselves, while they get (or observe) the rest of the UI state from the VMs and call the VMs to perform actions. The VMs then interact with the Service/Repository layer and update the UI state accordingly. An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

Changing Data Class From Live Data

I have a BaseViewModel that basically has the function to get the user data like so:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun getUserData() = viewModelScope.launch {
_userResponse.value = Resource.Loading
_userResponse.value = repository.getLoggedInUserData()
}
}
In my Fragment, I access this data by just calling viewModel.getUserData(). This works. However, I'd like to now be able to edit the data. For example, the data class of UserResponse looks like this:
data class UserResponse(
var id: Int,
var username: String,
var email: String
)
In other fragments, I'd like to edit username and email for example. How do I do access the UserResponse object and edit it? Is this a good way of doing things? The getUserData should be accessed everywhere and that is why I'm including it in the abstract BaseViewModel. Whenever the UserResponse is null, I do the following check:
if (viewModel.userResponse.value == null) {
viewModel.getUserData()
}
If you want to be able to edit the data in userResponse, really what you're talking about is changing the value it holds, right? The best way to do that is through the ViewModel itself:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun setUserResponse(response: UserResponse) {
_userResponse.value = response
}
...
}
This has a few advantages - first, the view model is responsible for holding and managing the data, and provides an interface for reading, observing, and updating it. Rather than having lots of places where the data is manipulated, those places just call this one function instead. That makes it a lot easier to change things later, if you need to - the code that calls the function might not need to change at all!
This also means that you can expand the update logic more easily, since it's all centralised in the VM. Need to write the new value to a SavedStateHandle, so it's not lost if the app goes to the background? Just throw that in the update function. Maybe persist it to a database? Throw that in. None of the callers need to know what's happening in there
The other advantage is you're actually setting a new value on the LiveData, which means your update behaviour is consistent and predictable. If the user response changes (either a whole new one, or a change to the current one) then everything observeing that LiveData sees the update, and can decide what to do with it. It's less brittle than this idea that one change to the current response is "new" and another change is "an update" and observers will only care about one of those and don't need to be notified of the other. Consistency in how changes are handled will avoid bugs being introduced later, and just make it easier to reason about what's going on
There's nothing stopping you from updating the properties of the object held in userResponse, just like there's nothing stopping you from holding a List in a LiveData, and adding elements to that list. Everything with a reference to that object will see the new data, but only if they look at it. The point of LiveData and the observer pattern is to push updates to observers, so they can react to changes (like, say, updating text displayed in a UI). If you change one of the vars in that data class, how are you going to make sure everything that needs to see those changes definitely sees them? How can you ensure that will always happen, as the app gets developed, possibly by other people? The observer pattern is about simplifying that logic - update happens, observers are notified, the end
If you are going to do things this way, then I'd still recommend putting an update function in your VM, and let that update the vars. You get the same benefits - centralising the logic, enabling things like persistence if it ever becomes necessary, etc. It could be as simple as
fun setUserResponse(response: UserResponse) {
_userResponse.value?.run {
id = response.id
username = response.username
email = response.email
}
}
and if you do decide to go with the full observer pattern for all changes later, everything is already calling the function the right way, no need for changes there. Or you could just make separate updateEmail(email: String) etc functions, whatever you want to do. But putting all that logic in the VM is a good idea, it's kinda what it's there for
Oh and you access that object through userResponse.value if you want to poke at it - but like I said, better to do that inside a function in the VM, keep that implementation detail, null-safety etc in one place, so callers don't need to mess with it
The ideal way to update userResponse you should change/edit _userResponse so that your userResponse we'll give you the updated data.
it should be something like this
_userResponse.value = Resource<UserResponse>()

How to correctly model UI states with sealed classes in Compose?

Say you have a simple screen that displays a list of items. I could define the 3 possible states of the screen like this:
sealed class State() {
data class Success(val items: List<Item> = listOf()): State()
object Loading: State()
data class Error(val error: String? = null): State()
}
In my Compose UI, I treat the states accordingly:
when(state) {
is State.Success -> {
LazyColumn(...) {...}
}
is State.Error -> {
Text(state.error ?: "")
}
is State.Loading -> {
LoadingBar()
}
}
The first state will be State.Loading() as I will initiate a network request to get the items. When the items arrive, the state becomes State.Success().
But what if, afterwards, the user presses on a button that should trigger another network request?
On top of the existing content, I need to push the State.Loading class again to show the loading indicator while the new network request is finished.
Yet this state has no knowledge of the previous items content, so the UI will only show the loading indicator, and it will not render the content from the previous State.Success().
I could fix this by introducing a new State called: State.LoadingActionWhileSuccess yet this is not scalable.
How would you treat such situations? Adding other State classes could work, but it would become un-manageable after some point.
My biggest worry for this approach is when the screen complexity rises, the combination of possible states gets out of hand.
I could hold the state in a regular data class and mutate it, but I found that this approach can introduce illegal states for concurrent mutations.
Maybe don't allow the user to start another network call while one is underway by disabling buttons, etc.. Most of the apps I've seen do this. When you do a pull-to-refresh, the refresh icon always shows until the call has ended. This prevents you from making additional calls.
Then again, without knowing your use-case, I'm only making a general suggestion.

How to get the value of a Flow outside a coroutine?

How can I get the value of a Flow outside a coroutine similarly to LiveData?
// Suspend function 'first' should be called only from a coroutine or another suspend function
flowOf(1).first()
// value is null
flowOf(1).asLiveData().value
// works
MutableLiveData(1).value
Context
I'm avoiding LiveData in the repository layer in favor of Flow. Yet, I need to set, observe and collect the value for immediate consumption. The later is useful for authentication purpose in a OkHttp3 Interceptor.
You can do this
val flowValue: SomeType
runBlocking(Dispatchers.IO) {
flowValue = myFlow.first()
}
Yes its not exactly what Flow was made for.
But its not always possible to make everything asynchronous and for that matter it may not even always be possible to 'just make a synchronous method'. For instance the current Datastore releases (that are supposed to replace shared preferences on Android) do only expose Flow and nothing else. Which means that you will very easiely get into such a situation, given that none of the Lifecycle methods of Activities or Fragments are coroutines.
If you can help it you should always call coroutines from suspend functions and avoid making runBlocking calls. A lot of the time it works like this. But it´s not a surefire way that works all the time. You can introduce deadlocks with runBlocking.
Well... what you're looking for isn't really what Flow is for. Flow is just a stream. It is not a value holder, so there is nothing for you retrieve.
So, there are two major avenues to go down, depending on what your interceptor needs.
Perhaps your interceptor can live without the data from the repository. IOW, you'll use the data if it exists, but otherwise the interceptor can continue along. In that case, you can have your repository emit a stream but also maintain a "current value" cache that your interceptor can use. That could be via:
BroadcastChannel
LiveData
a simple property in the repository that you update internally and expose as a val
If your interceptor needs the data, though, then none of those will work directly, because they will all result in the interceptor getting null if the data is not yet ready. What you would need is a call that can block, but perhaps evaluates quickly if the data is ready via some form of cache. The details of that will vary a lot based on the implementation of the repository and what is supplying the Flow in the first place.
You could use something like this:
fun <T> SharedFlow<T>.getValueBlockedOrNull(): T? {
var value: T?
runBlocking(Dispatchers.Default) {
value = when (this#getValueBlockedOrNull.replayCache.isEmpty()) {
true -> null
else -> this#getValueBlockedOrNull.firstOrNull()
}
}
return value
}
You can use MutableStateFlow and MutableSharedFlow for emitting the data from coroutine and receiving the data inside Activity/Fragment. MutableStateFlow can be used for state management. It requires default value when initialised. Whereas MutableSharedFlow does not need any default value.
But, if you don't want to receive stream of data, (i.e) your API call sends data only once, you can use suspend function inside coroutine scope and the function will perform the task and return the result like synchronous function call.
To get the value of a Flow outside of a coroutine, the best option is to create the flow as a StateFlow and then call the value property on the StateFlow.
class MyClass {
private val mutableProperty = MutableStateFlow(1)
val property = mutableProperty.asStateFlow()
...
mutableProperty.value = 2
}
...
val readProperty = MyClass().property.value
val propertyAsFlow = MyClass().property as Flow<Int>

Categories

Resources