ViewModel refetching data again with distinctUntilChanged() - android

I have a Fragment that I want to do a fetch once on its data, I have used distinctUntilChanged() to fetch just once because my location is not changing during this fragment.
Fragment
private val viewModel by viewModels<LandingViewModel> {
VMLandingFactory(
LandingRepoImpl(
LandingDataSource()
)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sharedPref = requireContext().getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
val nombre = sharedPref.getString("name", null)
location = name!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchShops(location)
}
private fun fetchShops(localidad: String) {
viewModel.setLocation(location.toLowerCase(Locale.ROOT).trim())
viewModel.fetchShopList
.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Loading -> {
showProgress()
}
is Resource.Success -> {
hideProgress()
myAdapter.setItems(it.data)
}
is Resource.Failure -> {
hideProgress()
Toast.makeText(
requireContext(),
"There was an error loading the shops.",
Toast.LENGTH_SHORT
).show()
}
}
})
}
Viewmodel
private val locationQuery = MutableLiveData<String>()
fun setLocation(location: String) {
locationQuery.value = location
}
val fetchShopList = locationQuery.distinctUntilChanged().switchMap { location ->
liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(Resource.Loading())
try{
emit(repo.getShopList(location))
}catch (e:Exception){
emit(Resource.Failure(e))
}
}
}
Now, if I go to the next fragment and press back, this fires again, I know that maybe this is because the fragment is recreating and then passing a new instance of viewmodel and thats why the location is not retained, but if I put activityViewModels as the instance of the viewmodel, it also happends the same, the data is loaded again on backpress, this is not acceptable since going back will get the data each time and this is not server efficient for me, I need to just fetch this data when the user is in this fragment and if they press back to not fetch it again.
Any clues ?
I'm using navigation components, so I cant use .add or do fragment transactions, I want to just fetch once on this fragment when creating it first time and not refetching on backpress of the next fragment

TL;DR
You need to use a LiveData that emits its event only once, even if the ui re-subscribe to it. for more info and explanation and ways to fix, continue reading.
When you go from Fragment 1 -> Fragment 2, Fragment 1 is not actually destroyed right away, it just un-subscribe from your ViewModel LiveData.
Now when you go back from F2 to F1, the fragment will re-subscribe back to ViewModel LiveData, and since the LiveData is - by nature - state holder, then it will re-emit its latest value right away, causing the ui to rebind.
What you need is some sort of LiveData that won't emit an event that has been emitted before.
This is common use case with LiveData, there's a pretty nice article talking about this need for a similar LiveData for different types of use cases, you can read it here.
Although the article proposed a couple of solutions but those can be a bit of an overkill sometimes, so a simpler solution would be using the following ActionLiveView
// First extend the MutableLiveData class
class ActionLiveData<T> : MutableLiveData<T>() {
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T?>) {
// Being strict about the observer numbers is up to you
// I thought it made sense to only allow one to handle the event
if (hasObservers()) {
throw Throwable("Only one observer at a time may subscribe to a ActionLiveData")
}
super.observe(owner, Observer { data ->
// We ignore any null values and early return
if (data == null) return
observer.onChanged(data)
// We set the value to null straight after emitting the change to the observer
value = null
// This means that the state of the data will always be null / non existent
// It will only be available to the observer in its callback and since we do not emit null values
// the observer never receives a null value and any observers resuming do not receive the last event.
// Therefore it only emits to the observer the single action so you are free to show messages over and over again
// Or launch an activity/dialog or anything that should only happen once per action / click :).
})
}
// Just a nicely named method that wraps setting the value
#MainThread
fun sendAction(data: T) {
value = data
}
}
You can find more explainiation for ActionLiveData in this link if you want.
I would advise using the ActionLiveData class, I've been using it for small to medium project size and it's working alright so far, but again, you know your use cases better than me. :)

Related

Why state flow calls callectLatest multiple times?

So, I would like to use StateFlow instead of LiveData, but I can not figure out what's the problem with my logic.
I have a flow, which has a default null value. When I open a dialog which contains a some datas, after that I select one data, I emit the new value to the flow.
In the first time, after the dialog closed, collectLatest called, and I get the null value (init), after the emit, I get the new value, it is good. But If I open the dialog again, and select value, and close the dialog, the collectLatest fun called 3-times, and I again open the dialog... and collectLatest called 4 times and so on.
So this is very bad behavior, and I'm sure , I did something wrong, but I don't see the bug.
In the liveData the expected behavior is after the dialog close, that the observer fun is called just once. I would like to achive this.
I also checked, that I emit the new value only once, so there is no reason why collectLatest fire multiple times.
ViewModel:
private val _previousManufacture = MutableStateFlow<PreviousManufactureView?>(null)
val previousManufacture = _previousManufacture.asStateFlow()
private suspend fun setPreviousManufactureByMachineId(machineId: String) {
val result = stateReportRepository.getPreviousManufactureByMachineId(machineId)
if (result is Result.Success) {
_previousManufacture.emit(result.data)
} else {
_previousManufacture.emit(null)
}
}
Fragment:
lifecycleScope.launchWhenCreated {
viewModel.previousManufacture.collectLatest {
var d = it
}
}
[Update]
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.vm = viewModel
initFlows()
}
private fun initFlows() {
lifecycleScope.launchWhenCreated {
viewModel.openStateOfWorkflowBrowser.collectLatest {
openStateOfWorkflowSelectionDialog()
}
}
...
}
Sorry, I missed this before in my comment, but I think the problem is that you are calling launchWhenCreated in the lifecycleScope of the Fragment, not in its viewLifecycle.lifecycleScope. So if the Fragment is reused (like after a dialog fragment has a appeared), the old collector is not cancelled and a new one is added, because the lifecycle of the Fragment has not ended, only the lifecycle of its previous view. You should almost always use viewLifecycle.lifecycleScope when you are using coroutines in a Fragment.

How to combine livedata and kotlin flow

Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again

StateFlow fetching the same data again on Back Navigation

I am working with RecyclerView and using Retrofit to fetch the data from Server. I am using Kotlin with MVVM Design Pattern. I have used LiveData it was working fine. But with Stateflow causing issues when we navigate to another Fragment and Comes back to the Same Fragment again. It just fetches the same data again. Below is the code for ViewModel and the observer:
//View Model
private val _allTimeSheetsResponse =
MutableStateFlow<ResponsesResult<AllTimeSheetsResponse>>(ResponsesResult.Empty)
val allTimeSheetsResponse : StateFlow<ResponsesResult<AllTimeSheetsResponse>> get() = _allTimeSheetsResponse
fun getAllTimeSheets(auth: String) =
viewModelScope.launch {
timeSheetsRepository.getAllTimeSheets(auth).collect {
_allTimeSheetsResponse.value = it
}
}
//Observer
lifecycleScope.launchWhenStarted{
timeSheetsViewModel.allTimeSheetsResponse.collect { timeSheetsResponse ->
when (timeSheetsResponse) {
is ResponsesResult.Loading -> binding.progressBarLayout.show()
is ResponsesResult.Failure -> {
binding.progressBarLayout.gone()
binding.nothingFoundLayout.show()
handleApiError(timeSheetsResponse)
}
is ResponsesResult.Success -> {
binding.progressBarLayout.gone()
if (timeSheetsResponse.value.payload.isNotEmpty()) {
showAllTimeSheetsRecyclerAdapter.submitList(timeSheetsResponse.value.payload)
} else {
binding.nothingFoundLayout.show()
}
}
else -> Unit
}
}
}
Because you call getAllTimeSheets many times (eg. onCreateView or onViewCreated). Trying call it when accessing allTimeSheetsResponse` for the first time.
Your ViewModel's getAllTimeSheets() function starts a new coroutine to collect from the repo's cold Flow each time you call it, so each time the Fragment comes back, presumably. You should remove this function and simply convert the repo's cold Flow directly to a StateFlow:
val allTimeSheetsResponse: StateFlow<ResponsesResult<AllTimeSheetsResponse>> =
timeSheetsRepository.getAllTimeSheets(auth)
.stateIn(viewModelScope, SharingStarted.Eagerly, ResponsesResult.Empty)
You can pass the auth parameter into the ViewModel's factory through to its constructor.
When you are using the navigationComponent and call navController.navigate() to open a fragment, in the background destination fragment replaces with the old destination's fragment. so old fragment will keep in the fragmentManager backStack. but its view will destroy. and when navigate back, old fragment comes from backStack (not created again) and just its view creates.
So it's better to call getAllTimeSheets() in Fragment's onCreate. (to call one time). When fetching done, all data will set in _allTimeSheetsResponse
And then you should observe allTimeSheetsResponse in onViewCreated with viewLifecycleOwner scope.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.allTimeSheetsResponse.onEach { response ->
// do sth with response
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
fun getAllTimeSheets(auth: String) :StateFlow<ResponsesResult<AllTimeSheetsResponse>> {
var mutableStateFlow = MutableStateFlow<ResponsesResult<AllTimeSheetsResponse>>(ResponsesResult.Empty)
viewModelScope.launch {
timeSheetsRepository.getAllTimeSheets(auth).collect {
mutableStateFlow.value = it
}
}
return mutableStateFlow
}

Android ViewModel MutableLiveData update multiple times

Scenario
Hi,
I have an Activity with a ViewPager. In the ViewPagerAdapter, I create instances of a same fragment with different data.
And in each instance I initialize a ViewModel
val dataViewModelFactory = this.activity?.let { DataViewModelFactory(it) }
mainViewModel = ViewModelProviders.of(this, dataViewModelFactory).get(MainViewModel::class.java)
In my fragment, I observe two MutableLiveData when I call APIs
mainViewModel.isResponseSuccessful.observe(this, Observer { it ->
if(it) {
//do Something
}else{
Toast.makeText(activity, "Error in Sending Request", Toast.LENGTH_SHORT).show()
}
})
mainViewModel.isLoading.observe(this, Observer {
if (it) {
println("show progress")
} else {
println("dismiss progress")
}
})
In each fragment, on a button click I load another fragment. And if required call and API to fetch data.
PROBLEM
The code comes to the observe block multiple times in my fragment. When I comeback from one fragment to previous fragment, even though no API is called, the code on observe block is executed.
What I tried
I tried using an activity instance in the ViewModel initialization
mainViewModel = ViewModelProviders.of(activity,dataViewModelFactory).get(MainViewModel::class.java)
But it did not work.
Please help,
If you want to prevent multiple calls of your observer u can just change MutableLiveData to SingleLiveEvent. Read this
It might help you:
import java.util.concurrent.atomic.AtomicBoolean
class OneTimeEvent<T>(
private val value: T
) {
private val isConsumed = AtomicBoolean(false)
private fun getValue(): T? =
if (isConsumed.compareAndSet(false, true)) value
else null
fun consume(block: (T) -> Unit): T? =
getValue()?.also(block)
}
fun <T> T.toOneTimeEvent() =
OneTimeEvent(this)
First, when you want to post a value on LiveData, use toOneTimeEvent() extension function to wrap it in a OneTimeEvent:
liveData.postValue(yourObject.toOneTimeEvent())
Second, when you are observing on the LiveData, use consume { } function on the delivered value to gain the feature of OneTimeEvent. You'll be sure that the block of consume { } will be executed only once.
viewModel.liveData.observe(this, Observer {
it.consume { yourObject ->
// TODO: do whatever with 'yourObject'
}
})
In this case, when the fragment resumes, your block of code does not execute again.

how to stop observer to automatically observe data when pop back to previous fragment? [duplicate]

I have some problem in nested fragment in Kotlin. I have nested fragment with ViewModel. After resuming fragment from back button press all observers on viewModel LiveData triggers again although my data does not changed.
First i googled and tried for define observer in filed variable and check if it is initialized then do not observer it again:
lateinit var observer: Observer
fun method(){
if (::observer.isInitialized) return
observer = Observer{ ... }
viewModel.x_live_data.observe(viewLifecycleOwner ,observer)
}
So at first enter to fragment it works fine and also after resume it does not trigger again without data change but it does not trigger also on data change!
What is going on?
LiveData always stores the last value and sends it to each Observer that is registered. That way all Observers have the latest state.
As you're using viewLifecycleOwner, your previous Observer has been destroyed, so registering a new Observer is absolutely the correct thing to do - you need the new Observer and its existing state to populate the new views that are created after you go back to the Fragment (since the original Views are destroyed when the Fragment is put on the back stack).
If you're attempting to use LiveData for events (i.e., values that should only be processed once), LiveData isn't the best API for that as you must create an event wrapper or something similar to ensure that it is only processed once.
After knowing what happen I decide to go with customized live data to trigger just once. ConsumableLiveData. So I will put answer here may help others.
class ConsumableLiveData<T>(var consume: Boolean = false) : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(
owner,
Observer<T> {
if (consume) {
if (pending.compareAndSet(true, false)) observer.onChanged(it)
} else {
observer.onChanged(it)
}
}
)
}
override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}
}
And for usage just put as bellow. It will trigger just once after any update value. This will great to handle navigation or listen to click or any interaction from user. Because just trigger once!
//In viewModel
val goToCreditCardLiveData = ConsumableLiveData<Boolean>(true)
And in fragment:
viewModel.goToCreditCardLiveData.observe(viewLifecycleOwner) {
findNavController().navigate(...)
}
If u are using kotlin and for only one time trigger of data/event use MutableSharedFlow
example:
private val data = MutableSharedFlow<String>() // init
data.emit("hello world) // set value
lifecycleScope.launchWhenStarted {
data.collectLatest { } // value only collect once unless a new trigger come
}
MutableSharedFlow won't trigger for orientation changes or come back to the previous fragment etc

Categories

Resources