Observe live data objects in fragment using activity context? - android

I'm using navigation bottom with shared ViewModel with all fragments inside navigation bottom but it throws this exception when recall fragment second time
java.lang.IllegalArgumentException: Cannot add the same observer with different lifecycles
I have tried to make all observers attached to activity not to it's fragment as below
1-Declare viewModel in fragemt
viewModel = activity?.run {
ViewModelProviders.of(this,viewModelFactory).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
2-Observer livedata object
viewModel.msg.observe(activity!!, Observer {
Log.i(TAG,it)
})
3- remove observer
override fun onStop() {
super.onStop()
viewModel.msg.removeObservers(activity!!)
}
This code is working fine with me, but I wondering if my code is correct and working probably?
thanks in advance

It is a common mistake we do while using live-data in fragment. Using this/activity on fragment can be duplicate. You should use viewLifecycleOwner for livedata observing in fragment.
viewModel.msg.observe(viewLifecycleOwner, Observer {
Log.i(TAG,it)
})
For more information, read this article https://medium.com/#cs.ibrahimyilmaz/viewlifecycleowner-vs-this-a8259800367b
You don't need to remove the observer manually.

Why you are adding the observer to the fragment with the activity lifecycle? If you have some logic that needs to be executed when fragment is not active, add it to your activity. So instead of what you have, you need:
viewModel.msg.observe(this, Observer {
Log.i(TAG, it)
})
What happens in your case is that each time you reopen your fragment, you attach a new observer with the same lifecycle, which seems to be an error. Livedata observers were specifically designed so that you don't have to write code for handling lifecycles manually.

Related

Sharing a cold flow of MutableStateFlow in ViewModel between two fragments

I've got a problem with a MutableStateFlow property shared between two fragments.
To make it understandable:
I have a BasicViewModel which should be always one instance for both of the fragments because of the nav graph implementation
private val basicViewModel: basicViewModel by navGraphViewModels(R.id.basic_graph) { defaultViewModelProviderFactory }
This ViewModel have a MutableStateFlow property declared like this
private val _basicProperty = MutableStateFlow<BasicClass?>(null)
val basicProperty : Flow<BasicClass?> = _basicId
.filterNotNull()
.flatMapConcat { someRepository.getBasicProperty(it) }
.onEach { _basicProperty.value = it }
.catch { }
Then, I have FragmentA and FragmentB declared in navigation using nav graphs which calls the property similarly, like this
basicViewModel.basicProperty
.filterNotNull()
.mapNotNull { it.innerProperty}
.onEach { doSomething(it) }
.launchIn(viewLifecycleOwner.lifecycleScope)
It all looks fine, but when I navigate to FragmentA the flow of BasicProperty loads (data loads from WebApi) then, I navigate to FragmentB and the flow loads again instead of calling already loaded data which in App it looks kinda lagging because of the reload
Question: What should I do/change to get the already existing data from BasicViewModel in FragmentB?
Your _basicProperty is a hot StateFlow, but you never use it for collecting anything. The property you have exposed, basicProperty, is a cold flow, so each subscriber that collects it will start a new run of the cold flow. Each of these cold flows will be posting their updates to the MutableStateFlow, so at that point, its state is unpredictable because it's showing the latest thing any collector of the shared cold flow is doing.
I think what you want is for there to be one flow of execution that's shared. So you should have a single StateFlow that is performing the connection, like this:
val basicProperty : StateFlow<BasicClass?> = _basicId
.filterNotNull()
.flatMapConcat { someRepository.getBasicProperty(it) }
.catch { }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
Your original code doesn't do anything to start the flow until each Fragment comes along to collect it. But this code's call to stateIn starts the flow a single time in the viewModelScope (in this case immediately because of the Eagerly parameter).
Now this flow only runs once. You can still have each Fragment run its own downstream flow like you were already doing.

Can't observe correctly: viewLifecycleOwner vs this

Let's imagine the following case: we have BottomNavigationView with Navigation Component and MVVM architecture.
In which case in Fragment this line of code will now work
onViewCreated()
viewModel.isActionDone.observe(viewLifecycleOwner) {
doReaction()
}
but this code will?
private val observer = Observer<Boolean> {
doReaction()
}
onViewCreated()
viewModel.isActionDone.observe(this, observer)
In my app, the second code works great, but the first code doesn`t work correctly: when I move to another element in the BottomNavigationView, the observer is called several times.
P.S. this code also calling observer several times.
viewModel.isActionDone.observe(viewLifecycleOwner, observer)
Сould you please help me understand what could be wrong with the first code?
it's because every time onStart() is called on your fragment, the livedata becomes active again and emits the last value again. you should use the Event class as a wrapper or you could use the SingleLiveEvent class which is now considered an anti pattern.
you could read this for more information: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
It's because When you move to another fragment, NavController destroy the view and stop the fragment and when you back to the first fragment, fragment will be started and view will be created again.
So when you're using "viewLifecycleOwner" for observe, your LiveData emit the last value each time when you back to the first fragment. but when using "this", it does not.
So you should think about your business that you need have emits each time your view created or each time fragment created and use suitable LifecycleOwner.
I home it help you.

ViewModel not getting cleared in Navigation navigate and live data in viewmodel stays alive

So, I have implemented single activity with multiple fragments pattern using Navigation. I used viewmodel for each fragment for non-ui operatios.
The problem is when you navigate using findNavController().navigate(), the fragment is not actually destroyed. Only the onDestroyView is called. So, the fragment's onDestroy never gets called and subsequently the viewmodel doesn't get cleared and so the LiveData observer also remains alive and when i come back to the fragment the observer is created again and so live data is observed twice. Once with the old data that it holds and second with the new data from some operations.
For example,
I have fragmentA and fragmentB
A shows a list and B you can add something which will be shown in the list. maybe fetching new data from api in fragment B to be shown in A.
So, when i go back from fragment B to A the observer gets called twice first with old data then second with the updated data. In the end the list shows correct data but I don't want two observers happening.
I have followed this article
https://medium.com/#BladeCoder/architecture-components-pitfalls-part-1-9300dd969808
and tried to use viewLifeCycleOwner instead of this but that does not help and issue still exists.
I also tried removing observer before observing :
vm.ld.removeObservers(this)
vm.ld.observe(viewLifeCyclerOwner, observer)
still the issue remains.
(I tried removing observer in onDestroyView also, still the issue remains.)
The only work around i found is manually calling the viewmodel onCleared in onDestroyView and clearing the livedata.
In fragment onDestroyView
vm.clear()
In viewmodel
fun clear() = onCleared()
override fun onCleared() {
//do stuff
}
Now, this solves my issue. But i feel this is not a solid solution and there can be a better way to do this. I would be glad if anybody can shed a light on this one. Thanks.
I wasted a couple of days troubleshooting a similar issue. Double check how your VM is initialized:
val myVm: MyViewModel by activityViewModels()
vs.
val myVm: MyViewModel by viewModels()
If you use the by activityViewModels() delegate, you are instructing Android to tie the lifetime of the VM to the host activity rather than the current Fragment. Thus, your VM won't be cleared even when the fragment is destroyed. I learned this the hard way. Switching back to the by viewModels() delegate leaves the VM scoped to the lifetime of the Fragment. When the fragment is destroyed, if it's the only observer, the VM will clear.
I was totally confused by the this vs. viewLifecycleOwner target when observing. Apparently, the choice of target has only to do with whether you intend to manually control presenting the dialog of a DialogFragment. Another gem of confusion.
In your case, if you're switching between fragments and onDestroy isn't being called, it could also be because fragments are being retained. For example, ViewPager2 has offscreenPageLimit, which instructs Android to keep hidden fragments in memory as you switch pages, adding even further to this mess of having to know absolutely everything about everything just to use the SDK.
You can use viewModelStore.clear() in your fragment, and then override onCleared() in ViewModel for dispose what your need
You can set the value of your livedata to null in onDestroyView.
example :
override fun onDestroyView() {
super.onDestroyView()
vm.ld.value = null
}
or
in ViewModel
fun resetLiveData() {
_ld.value = null
}
in Fragment
override fun onDestroyView() {
super.onDestroyView()
vm.resetLiveData()
}

Why Observers added as observeForever to LiveData must be removed?

I've read on Android LiveData documentation that:
You can register an observer without an associated LifecycleOwner object using the observeForever(Observer) method. In this case, the observer is considered to be always active and is therefore always notified about modifications. You can remove these observers calling the removeObserver(Observer) method.
I'm building an app using MVVM architecture pattern using ViewModel and declaring LiveDatas inside my ViewModel class. At my viewModel I have set a observeForever to a LiveData:
val password by lazy {
MutableLiveData<String>()
}
init {
initObservable()
}
private fun initObservable() {
password.observeForever {
...
}
}
From what I understood from the documentation, I should remove the observer everytime the view that instantiates the ViewModel (with the previous code) was destroyed, right? But shouldn't the Observers be destroyed once the view is destroyed (since the ViewModel instance was instantiated in the view and will also be destroyed)?
"I should remove the observer everytime the view that instantiates the ViewModel (with the previous code) was destroyed, right?"
If you are obsering a LiveData in ViewModel using observeForever(observer):
You should not worry about View's lifecycle, because it is different from ViewModel's life. ViewModel should be able to out-live the View that creates it. Instead, the framework will call onCleared() when the ViewModel is not needed, so that's where you should handle removing the observer.
If you are observing a LiveData in View using observe(lifecyclerowner, observer)
Observers will automatically removed by the framework when the lifecycleowner is destroyed.
"But shouldn't the Observers be destroyed once the view is destroyed (since the ViewModel instance was instantiated in the view and will also be destroyed)?"
This question is more of a Java question than Android.
Think about what it means by "being destroyed". When a View or ViewModel is destroyed by the Android Framework, it does not mean that the object is completely removed from the memory. Your activities and fragments will not be garbage collected as long as there are other objects (such as observer) that has reference to them.
If you call observe(activity, observer), then the Android Framework can track the connection between the activity instance and the observer instance, and therefore it can kill observer when it wants to kill activity. However if you simply call observeForever(observer) there is simply no way for Android Framework to tell which object this observer belongs to.
Implementing Sanlok Lee's answer in a ViewModel, it would look like this:
val password by lazy {
MutableLiveData<String>()
}
private val passwordObserver = Observer<String> {
...
}
init {
initObservable()
}
private fun initObservable() {
password.observeForever(passwordObserver)
}
override fun onCleared() {
password.removeObserver(passwordObserver)
super.onCleared()
}
From what I understood from the documentation, I should remove the
observer everytime the view that instantiates the ViewModel
To achieve this you should instantiate your viewmodel inside the View (Activity, Fragment) and observe the livedata like this
val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
by passing this you tie observing livedata to view's lifecycle, so when View (Activity, Fragment) will be destroyed both viewmodel and observer will be destroyed.

MVVM pattern and startActivity

I recently decided to have a closer look at the new Android Architecture Components that Google released, especially using their ViewModel lifecycle-aware class to a MVVM architecture, and LiveData.
As long as I'm dealing with a single Activity, or a single Fragment, everything is fine.
However, I can't find a nice solution to handle Activity switching.
Say, for the sake of a short example, that Activity A has a button to launch Activity B.
Where would the startActivity() be handled?
Following the MVVM pattern, the logic of the clickListener should be in the ViewModel. However, we want to avoid having references to the Activity in there. So passing the context to the ViewModel is not an option.
I narrowed down a couple of options that seem "OK", but was not able to find any proper answer of "here's how to do it.".
Option 1 : Have an enum in the ViewModel with values mapping to possible routing (ACTIVITY_B, ACTIVITY_C). Couple this with a LiveData.
The activity would observe this LiveData, and when the ViewModel decides that ACTIVITY_C should be launched, it'd just postValue(ACTIVITY_C). Activity can then call startActivity() normally.
Option 2 : The regular interface pattern. Same principle as option 1, but Activity would implement the interface. I feel a bit more coupling with this though.
Option 3 : Messaging option, such as Otto or similar. ViewModel sends a Broadcast, Activity picks it up and launches what it has to. Only problem with this solution is that, by default, you should put the register/unregister of that Broadcast inside the ViewModel. So doesn't help.
Option 4 : Having a big Routing class, somewhere, as singleton or similar, that could be called to dispatch relevant routing to any activity. Eventually via interface? So every activity (or a BaseActivity) would implement
IRouting { void requestLaunchActivity(ACTIVITY_B); }
This method just worries me a bit when your app starts having a lot of fragments/activities (because the Routing class would become humongous)
So that's it. That's my question. How do you guys handle this?
Do you go with an option that I didn't think of?
What option do you consider the most relevant and why?
What is the recommended Google approach?
PS : Links that didn't get me anywhere
1 - Android ViewModel call Activity methods
2 - How to start an activity from a plain non-activity java class?
NSimon, its great that you start using AAC.
I wrote a issue in the aac's-github before about that.
There are several ways doing that.
One solution would be using a
WeakReference to a NavigationController which holds the Context of the Activity. This is a common used pattern for handling context-bound stuff inside a ViewModel.
I highly decline this for several reasons. First: that usually means that you have to keep a reference to your NavigationController which fixes the context leak, but doesnt solve the architecture at all.
The best way (in my oppinion) is using LiveData which is lifecycle aware and can do all the wanted stuff.
Example:
class YourVm : ViewModel() {
val uiEventLiveData = SingleLiveData<Pair<YourModel, Int>>()
fun onClick(item: YourModel) {
uiEventLiveData.value = item to 3 // can be predefined values
}
}
After that you can listen inside your view for changes.
class YourFragmentOrActivity {
//assign your vm whatever
override fun onActivityCreated(savedInstanceState: Bundle?) {
var context = this
yourVm.uiEventLiveData.observe(this, Observer {
when (it?.second) {
1 -> { context.startActivity( ... ) }
2 -> { .. }
}
})
}
}
Take care that ive used a modified MutableLiveData, because else it will always emit the latest result for new Observers which leads to bad behaviour. For example if you change activity and go back it will end in a loop.
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveData"
}
}
Why is that attempt better then using WeakReferences, Interfaces, or any other solution?
Because this event split UI logic with business logic. Its also possible to have multiple observers. It cares about the lifecycle. It doesnt leak anything.
You could also solve it by using RxJava instead of LiveData by using a PublishSubject. (addTo requires RxKotlin)
Take care about not leaking a subscription by releasing it in onStop().
class YourVm : ViewModel() {
var subject : PublishSubject<YourItem> = PublishSubject.create();
}
class YourFragmentOrActivityOrWhatever {
var composite = CompositeDisposable()
onStart() {
YourVm.subject
.subscribe( { Log.d("...", "Event emitted $it") }, { error("Error occured $it") })
.addTo(compositeDisposable)
}
onStop() {
compositeDisposable.clear()
}
}
Also take care that a ViewModel is bound to an Activity OR a Fragment. You can't share a ViewModel between multiple Activities since this would break the "Livecycle-Awareness".
If you need that persist your data by using a database like room or share the data using parcels.
You should call startActivity from activity, not from viewmodel. If you want to open it from viewmodel, you need to create livedata in viewmodel with some navigation parameter and observe on livedata inside the activity
You can extend your ViewModel from AndroidViewModel, which has the application reference, and start the activity using this context.

Categories

Resources