In short: when Observe is active it works correctly when I do notify, but when I go back to the previous fragment (I use the navigation component) and again navigate to the current fragment, there is a creation of the fragment, and for some reason the Observe is called.
Why is the Observe not deleted when going back? It should behave according to the fragment's lifecycle.
I tried removing on onStop and still the observe called.
More detail:
Each of my project fragments is divided into 3 parts: model, viewModel, view
In the view section, I first set the viewModel.
class EmergencyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
emergencyFragmentViewModel = ViewModelProviders.of(this).get(EmergencyFragmentViewModel::class.java)
}
And in onViewCreated I set the Observer object so that any changes made in LiveData I get a change notification here:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.observe(viewLifecycleOwner, Observer {
Log.d("sendEmergencyEvent", "isEmergencyEventMediaLDSuccess observer called")
}
})
}
In the ViewModel class, I set the LiveData parameter as follows:
EmergencyFragmentViewModel: ViewModel() {
var isEmergencyEventMediaLDSuccess: LiveData<Boolean> = MutableLiveData()
private set
private val observerEventMedia = Observer<Boolean> { (isEmergencyEventMediaLDSuccess as MutableLiveData).value = it}
And in the init I set an observer:
init {
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.observeForever(observerEventMedia)
}
And of course removes when needed
override fun onCleared() {
super.onCleared()
EmergencyFragmentModel.isEmergencyEventMediaLDSuccessModel.removeObserver(observerEventMedia)
}
The part of the model is defined as follows:
class EmergencyFragmentModel {
companion object{
val isEmergencyEventMediaLDSuccessModel: LiveData<Boolean> = MutableLiveData()
And I do request network and when a reply comes back I perform a notify
override fun onResponse(call: Call<Int>, response: Response<Int>) {
if(response.isSuccessful) {
(isEmergencyEventLDModelSuccess as MutableLiveData).postValue(true)
Log.d("succeed", "sendEmergencyEvent success: ${response.body().toString()}")
}
Can anyone say what I'm missing? Why when there is an active Observe and I go back to the previous fragment (I use the navigation component) and navigate to the current fragment again, the Observe is called? I can understand that when a ViewModel instance is created and it executes setValue for the LiveData parameter, then it is notified. But Why is the observe not removed when I go back? I tried removing the Observe on the onStop and it keeps happening.
override fun onStop() {
super.onStop()
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObservers(viewLifecycleOwner)
emergencyFragmentViewModel.isEmergencyEventMediaLDSuccess.removeObserver(observeEmergencyEventLDSuccess)
}
#Pawel is right. LiveData stores the value and everytime you observe it (in your onViewCreated, in this case), it'll emit the last value stored.
Maybe you want something like SingleLiveEvent, which clean its value after someone reads it.
So when you go back and forth, it won't emit that last value (once it was cleaned).
As I understand your question, you only want to run the observer, when the new value differs from the old one. That can be done by retaining the value in another variable in the viewModel.
if (newValue == viewModel.retainedValue) return#observe
viewModel.retainedValue = newValue
I fixed this by creating an extension in kotlin by checkin the lifecycle state.
fun <T> LiveData<T>.observeOnResumedState(viewLifecycleOwner: LifecycleOwner, observer: Observer<T>) {
this.observe(viewLifecycleOwner) {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
observer.onChanged(it)
}
}
}
And here is how i observe
viewModel.result.observeOnResumedState(viewLifecycleOwner) {
// TODO
}
Related
So I have this in an onCreate in an activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
lifecycleScope.launch(Dispatchers.IO) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.state.collect() { state ->
println(state)
when (state) {
MyViewModel.State.First -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, FirstFragment())
}
}
MyViewModel.State.Second -> launch(Dispatchers.Main) {
supportFragmentManager.commit {
replace(R.id.nav_host_fragment_content_main, SecondFragment())
}
}
MyViewModel.State.Init -> {}
}
}
}
}
}
and in the viewmodel I have a stateflow
class MyViewModel : ViewModel() {
enum class State {
First,
Second,
Init; }
val state = MutableStateFlow(State.Init)
fun goToFirst() {
viewModelScope.launch {
println("go to first")
state.emit(State.First)
}
}
fun goToSecond() {
viewModelScope.launch {
println("go to second")
state.emit(State.Second)
}
}
}
the app displays the list fragment and I can add and remove users its great... until the list is empty. The activity stops collecting from the stateflow and never switches to the empty. Its gets weird though. In the viewModel, as an experiment, I added
init {
while (isActive) {
delay(1000)
state.emit(State.First)
delay(1000)
state.emit(State.Second)
}
}
and it switches back forth between fragments. It just doesn't switch to the empty state fragment when I use the buttons on the screen to clear the list. I've tried using SharedFlow and I've tried using a stateflow of an enum class that had two vales list and empty. Samething. The collector in the activity doesn't fire everytime. I know about conflation. the values are different. I've tried almost every combination of Dispatchers and add in a coroutine exception handler to every launch that never catches anything. I've also tried using globalscope. Why doesn't the collector in the activity fire everytime a different value is emitted?
I was using a viewModel factory in the activity and in the fragments. So the fragment would call a function it's viewmodel to change the state, but the viewmodel the activity was listening to never sent any state change.
I'm using the PagedList architectural components using a custom Paged datasource and I am having trouble returning results to the observe method.
below doesn't work:
fragment onviewcreated method
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel?.searchDetails?.observe(viewLifecycleOwner, {
populateSearchResults(it)
})
}
viewmodel code where I update livedata from LivePagedListBuilder upon a user click event(not in viewmodel's init(){} method)
fun onProcessSearch(vararg searchValue: Array<String?>?) {
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(PageSize).build()
searchDetails = LivePagedListBuilder(PageKeyedSearchDataFactory(viewModelScope, searchValue[0]), pagedListConfig).build()
}
below works,
just moving the observe part after calling the above view model function works,
Fragment code:
override fun onClick(view: View) {
viewModel?.onProcessSearch(searchValue)
if (view.id == R.id.match_patient_search_button) {
patientDetailsViewModel?.patientSearchAddedDetails?.observe(viewLifecycleOwner, {
populateSearchResults(it)
GlobalHelper.hideKeyBoard(view)
})
}
}
Could anyone help why the above works, because as per doc we should observer livedata on onViewCreated method.
I'm developing android app and I'm facing problem with passing data when user pressed back button (which means onBackPress event is fired).
I wanted to fire event with observer with viewmodel but it doesn't work.
like this.
// First Fragment
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal.observe { state ->
Timber.i("Event fired")
}
...
// Second fragment which was displayed with fragment transaction. This code is when user pressed back button. like override fun onBackPressed
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal(5)
// MyViewModel
...
val currencyVal = MutableLiveData<Int>()
...
fun setCurrencyVal(currencyVal: Int) {
currencyVal.value = currencyVal
}
Here's bindViewModel function
protected inline fun <reified T : ViewModel> bindViewModel(
crossinline initializer: T.() -> Unit = {}
): Lazy<T> = nonConcurrentLazy {
ViewModelProviders.of(requireActivity())
.get(T::class.java)
.also { it.initializer() }
}
And also passing data via fragment transaction doesn't work.
Could anyone please suggest how to pass data when user presses back button in FragmentActivity?
Thanks.
Please make sure your fragment instance is same.
If it's not same, you need to create view model by activity.
Hope this helps.
I am missing something. Is viewModel and firstViewModel the same object? Also if so are you sure that you are creating the ViewModel of the Activity, but not the Fragment?
mViewModel = ViewModelProviders.of(requireActivity()).get(YourViewModel.class);
I'm struggling with a LiveData observer which is firing twice. In my fragment I'm observing a LiveData as below, using viewLifeCycleOwner as LifeCycleOwner
private lateinit var retailViewModel: RetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retailViewModel = ViewModelProviders.of(this).get(RetailsViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
retailViewModel.retailLiveData.observe(viewLifecycleOwner, Observer {
// updating UI here, but firing twice!
}
retailViewModel.getRetailById(retail.id)
}
And this is my view model:
class RetailsViewModel(override val service: MyFoodyApiService = MyFoodyApiService.service) :
BaseViewModel(service) {
var retailLiveData: MutableLiveData<Retail> = MutableLiveData()
fun getRetailById(id: Int) {
scope.launch {
try {
val response =
service.getRetailById(authString, id).await()
when (response.isSuccessful) {
true -> {
response.body()?.let { payload ->
retailLiveData.postValue(payload.data)
} ?: run {
errorLiveData.postValue("An error occurred: ${response.message()}")
}
}
false -> errorLiveData.postValue("An error occurred: ${response.message()}")
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
}
When I run the fragment for the first time, everything works fine, however when I go to its DetailFragment and come back, retailLiveData Observer callback is fired twice. According to this article this was a known problem solved with the introduction of viewLifeCycleOwner who should be helpful to remove active observers once fragment's view is destroyed, however it seems not helping in my case.
This happens because view model retains value when you open another fragment, but the fragment's view is destroyed. When you get back to the fragment, view is recreated and you subscribe to retailLiveData, which still holds the previous value and notifies your observer as soon as fragment moves to started state. But you are calling retailViewModel.getRetailById(retail.id) in onViewCreated, so after awhile the value is updated and observer is notified again.
One possible solution is to call getRetailById() from view model's init method, the result will be cached for view model lifetime then.
I am just not able to figure out what is wrong in this code and why the observer is not called when the value is updated. I am using Fragement with livedata and here is the complete code. When app starts fragment gets it value from default data which in this case is 100. But after the value is updated using queueChannelId(channelId) method the observer is not called. I put a print statement and I can see method is executed in main thread. Please help
Fragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel =
ViewModelProviders.of(this).get(SomeViewModel::class.java)
viewModel.getChannelId().observe(this, Observer {
// Only called with default value of mutablelivedata
})
}
I can assure that onDestroyView and onDestroy have not been called anytime.
ViewModel:
fun getChannelId() : MutableLiveData<Int> {
return repository.getChannelId()
}
Repository:
var channelIdObservable = MutableLiveData(100)
fun queueChannelId(channelId: Int) {
channelIdObservable.value = channelId
}
fun getChannelId() : MutableLiveData<Int> = channelIdObservable
if you are calling queueChannelId from some other Thread try
channelIdObservable.postValue (channelId)
P.S: I cant see any other issue here.Share your code of how are u calling queueChannelId.