MutableLiveData not posting value - android

I'm trying to post a state as "Loading" to display a progress bar to the user while downloading data from the server, it looks like this:
private fun loadBottomSheetItems(currentViewState: BusinessMapViewState.Display, getBusinessByIdsRequest: GetBusinessByIdsRequest) {
viewModelScope.launch {
_businessMapViewState.postValue(
currentViewState.copy(
bottomSheetState = BottomSheetViewState.Loading <--------------- Always that state!
)
)
val responseFlow = businessRepository.getBusinessListByIds(
getBusinessByIdsRequest
)
responseFlow.collect { result ->
if (result.isSuccess()) {
val businesses = result.asSuccess().value.businessList
_businessMapViewState.postValue(
currentViewState.copy(
bottomSheetState = BottomSheetViewState.Display(
items = businesses.map { business ->
BusinessListCardItemModel(
businessId = business.id,
businessName = business.name
)
}
)
)
)
} else {
_businessMapViewState.postValue(
currentViewState.copy(
bottomSheetState = BottomSheetViewState.Error
)
)
}
}
}
}
But when I post the "Loading" state, that state doesn't change after the data is loaded.
If I remove the postValue block for the "Loading" state, or add a delay, the data displays correctly, but I need a progress bar.
I also tried to move the postValue block for the "Loading" state outside the viewModelScope, nothing changes
UPDATE
I solved the problem, the other part of my code was changing the state of ui 🙈

Don’t use postValue if you need things to happen orderly. “Post” means ”do this sometime soon” and causes the value to be changed at some later time while the coroutine is still running. It will necessarily happen sometime after your coroutine relinquishes the main thread.
Use value = instead. This is fine because viewModelScope is on Dispatchers.Main.

Related

Android compose multiple stateflows recomposition

Still new to compose + kotlin, so I'm having some trouble getting stateflow working. There might be some fundamentals that I don't understand or missing some function.
Problem:
I have two stateflows in my view model and both would trigger a recomposition of the other one.
ViewModel:
private val _networkUiState = MutableStateFlow<NetworkUIState>(NetworkUIState.Empty)
val networkUIState: StateFlow<NetworkUIState> = _networkUiState.asStateFlow()
private val _uiState = MutableStateFlow<UIState>(UIState.Empty)
val uiState: StateFlow<UIState> = _uiState.asStateFlow()
fun someAPICall(
) {
_networkUiState.value = NetworkUIState.Loading
networkJob = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
try {
val response = repo.apiCall()
withContext(Dispatchers.Main) {
_networkUiState.value = NetworkUIState.Loaded
_uiState.value = UIState.DoSomething(response)
}
} catch (error: NetworkException) {
_networkUiState.value = NetworkUIState.NetworkError(error)
}
}
}
//exceptionHandler calls _networkUIState.value = NetworkUIState.NetworkError(some error) as well
Compose:
val networkUIState = viewModel.networkUIState.collectAsState().value
val uiState = viewModel.uiState.collectAsState().value
Column() {
//...
//UI code
when (uiState) {
is UIState.DoSomething -> {
//read uiState.response and do something
}
is UIState.DoAnotherThing -> {
//read response and do something else
}
}
when (networkUIState) {
is NetworkUIState.Error -> {
//display network error
}
is NetworkUIState.Loading -> {
//display loading dialog
}
else -> {}
}
}
What happens:
1st time calling api:
NetworkUIState triggers loading (display loading)
NetworkUIState triggers loaded (hides loading)
uiState triggers DoSomething with response data
2nd time calling api:
NetworkUIState triggers loading (display loading)
uiState triggers DoSomething with response data (from last call)
NetworkUIState triggers loaded (hides loading)
uiState triggers DoSomething with response data (new data)
I understand this is because of the recomposition of NetworkUiState before UIState but UIState still has the old value.
My question is how can I avoid this when I absolutely need to separate these 2 states at least in the view model?
Simplest thing you can do is providing an Idle or DoNothing state for your UiState which and set to this state when you are done doing event or when you start a new api call. This is a pattern i use a lot, i also use this if there shouldn't be a Loading in ui initially. uiState starts with Idle, when user clicks to fetch from api i set to Loading and then Error or Success based on data fetch result.
I also use this pattern with gestures to not leave state in Up state when user lifts fingers to not start drawing from Up state as in this answer

How to start executing a block of code after changing the value of MutableLiveData when using .observeAsState()?

How to start executing a block of code after changing the value of MutableLiveData when using .observeAsState()?
Example: MutableLiveData changes and after need to call Toast.
#Composable
fun TextInfo() {
val isSuccess by remember { viewModel.isSuccess.observeAsState() }//var isSuccess = MutableLiveData<Boolean>() — in ViewModel
LaunchedEffect(isSuccess) {
Log.d("IS SUCCESS", "trues")
}
}
Showing a Toast is a side effect, so you need to put it inside a LaunchedEffect. Make the LiveData state the key of the LaunchedEffect. This causes the side effect to only occur when this particular LiveData's value changes.
val myDataState = remember { someLiveData.observeAsState() }
LaunchedEffect(myDataState) {
// show the toast
}
Read about it in the documentation here.

Why does Android Jetpack Compose snapShotFlow re emit last value on configuration change

My current Android Jetpack Compose application employs snapShotFlow to convert mutableStateOf() to flow and trigger user actions as follows
In ViewModel:-
var displayItemState by mutableStateOf(DisplayItemState())
#Immutable
data class DisplayItemState(
val viewIntent: Intent? = null
)
In composable:-
val displayItemState = viewModel.displayItemState
LaunchedEffect(key1 = displayItemState) {
snapshotFlow { displayItemState }
.distinctUntilChanged()
.filter { it.viewIntent != null }
.collectLatest { displayItemState ->
context.startActivity(displayItemState.viewIntent)
}
}
everything works as expected while I keep my test device in portrait or landscape.
However when I change the device orientation the last collected snapShotFlow value is resent.
If I reset the displayItemState as follows in the snapShotFlow this fixes the issue
however this feels like the wrong fix. What am i doing wrong? what is the correct approach to stop the snapShotFlow from re triggering on orientation change
val displayItemState = viewModel.displayItemState
LaunchedEffect(key1 = displayItemState) {
snapshotFlow { displayItemState }
.distinctUntilChanged()
.filter { it.viewIntent != null }
.collectLatest { displayItemState ->
context.startActivity(displayItemState.viewIntent)
viewModel.displayItemState = DisplayItemState()
}
}
That's intended behavior, you are not doing anything wrong. Compose's (Mutable)State holds the last value, similarly to StateFlow, so new collection from them always starts with the last value.
Your solution is ok, something very similar is actually recommended in Android's app architecture guide here:
For example, when showing transient messages on the screen to let the user know that something happened, the UI needs to notify the ViewModel to trigger another state update when the message has been shown on the screen.
Another possibility would be to use SharedFlow instead of MutableState in your viewModel - SharedFlow doesn't keep the last value so there won't be this problem.

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

StateFlow Observer is being triggered Two Times - Is this a good solution?

Okay so I've been using StateFlow with Room database for a while. Now I have one common case. At the start of my app I have a logic that if ROOM database is empty I should show an EmptyContent(), otherwise I will show the ListContent() from ROOM database.
Now every time I launch the app, I'm always getting that EmptyContent() shown for a HALF a second maybe, and then the ListContent() is displayed. After that when I'm using the app everything works normal. But at that app launch time, while ROOM database is working I guess, that EmptyContent() is shown for just a small amount of period (Because my StateFlow default value is an empty list), and after that the actual LIST from Database is displayed.
Now I have one solution for that, to just use delay() function inside a Coroutine, to wait for example 200MS and then trigger the function for reading the DATABASE, because those 200MS are enough for ROOM database to actually get the value and update my STATE FLOW variable with the actual data instead of using that StateFlow default value for a half second at the beginning.
Is that a good solution, I must ask? Because I'm using coroutine, the thread is not blocked, and I'm just waiting until ROOM database updates my STATE FLOW variable the second time.
#Composable
fun displayContent(
tasks: List<ToDoTask>,
ListContent: #Composable () -> Unit
) {
val scope = rememberCoroutineScope()
var counter by remember { mutableStateOf(0)}
LaunchedEffect(Unit){
scope.launch {
delay(200)
counter = 1
}
}
if(counter == 1){
if (tasks.isNotEmpty()) {
ListContent()
} else {
EmptyContent()
}
}
}
My suggestion would be map your expected states.
For instance:
sealed class RequestState<out T> {
object Idle : RequestState<Nothing>()
object Loading : RequestState<Nothing>()
data class Success<T>(val data: T) : RequestState<T>()
data class Error(
val t: Throwable,
var consumed: Boolean = false
) : RequestState<Nothing>()
}
And your function would be something like:
#Composable
fun YourScreen() {
val requestState = viewModel.screenData.collectAsState()
when (requestState) {
is Idle ->
// This is the default state, do nothing
is Loading ->
// Display some progress indicator
is Success ->
YourListScreen(requestState.data) // Show the list
is Error ->
// Display an error.
}
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
Of course, in your view model you must emit these values properly...
class YourView: ViewModel() {
private val _screenData =
MutableStateFlow<RequestState<List<ToDoTask>>>(RequestState.Idle)
val screenDatae: StateFlow<RequestState<List<ToDoTask>>> = _screenData
fun loadData() {
_screenData.value = Loading
try {
// load the data from database
_screenData.value = Success(yourLoadedData)
} catch (e: Exception) {
_screenData.value = Error(e)
}
}
}

Categories

Resources