Reputation: 743
I am using an Observable field in a ViewModel. When the Observable field gets updated, I change the UI visibility.
This can be done either done by
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
}
}
remove the callback in ondestroy.
or
directly mapping in XML like @{}
using two-way binding.
Now the question is how do I remove the listener if using two-way binding? I know the Livedata can be a replacement for this.
Upvotes: 6
Views: 3397
Reputation: 16699
I am not sure regarding which memory leak you are talking.
Memory leak in Java occur when one object exists long period of time and it contains strong references to other objects that should not be used anymore, thus should be destroyed by GC, but still persist because of that strong reference.
In Android specifically memory leaks usually occur when some long lasting object stores strong reference to an Activity (or in some cases Fragment). All the other memory leaks in android are not so impactful(except the ones with bitmaps - but it is a completely different topic)
So let us return to the data binding with an ObservableField
and its callbacks inside the ViewModel
or two way data binding via @={}
. In most of the cases there will be no memory leak in both of those cases. To understand why - you will need to understand how does Android framework operates with UI and also understand now does view data binding works. So what happens when you are creating a callback via either ObservableField
and callback or with @={}
When you write
val someField: ObservabaleField = ObservableFiled<String>("someText")
val someCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
}
}
someField.addOnPropertyChangedCallback(someCallback)
// and in the layout
android:text="@={viewModel.someField}"
In the generated file it does something like this
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, viewModelSomeFieldGet);
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
//...
return onChangeViewModelSomeOtherStuff(object, fieldId);
case 1 :
return onChangeViewModelSomeField((androidx.databinding.ObservableField<java.lang.String>) object, fieldId);
}
return false;
}
As you can see there is no context
neither activity
or fragment
leaks since there is no strong reference to them stored anywhere. There is no references to context
, activity
or fragment
in your ViewModel
either(I hope!). Moreover it works the other way around - ui stores link to the ViewModel
in the binding implementation thus our ViewModel
may be leaking. It is rear case since the UI of an Activity or a Fragment usually gets destroyed along with its ActivityBindingImpl
or FragmentBindingImpl
bindings but...
To be sure you have manual way to clear references: in either Activity' onDestroy
or Fragment' onDestroyView
call
clearFindViewByIdCache()
binding.unbind()
binding = null
// if you store view link in your viewModel(which is bad and may cause leaks) this is the perfect place to nullify it
viewModel.view = null
Also to handle binding auto clearing you may use AutoClearedValue
the actual usage may look like(if you don't care about its type)
override var binding: ViewDataBinding? by autoCleared()// that is all - no need of onDestroy or onDestroyView
Edit
If you want to manually unregister all the callbacks from your ObservableField
s you can do it. The best way to do it is in onCleared()
method of ViewModel
. You should call observableField.removeOnPropertyChangedCallback(callback)
to handle the stuff. It will look like this considering ObservableField
and callback declarations above:
class MyViewModel: ViewModel{
//ObservableField and callback declarations
...
override void onCleared(){
someField.removeOnPropertyChangedCallback(someCallback)
}
}
Edit end
This all things I've just described ensures absence of memory leaks while using ObservableFields
and view data bindings. It is all about a correct implementation. Of course you can implement it with leaks, but you can implement it without ones.
Comment if something is still unclear - I will try to expand the answer.
A bit more info about Fragment dependent leaks here
Hope it helps.
Upvotes: 2
Reputation: 765
removeOnPropertyChangedCallback never gets called?
This actually does get called, eventually and periodically, by the Data Binding framework to clean listeners that have been collected. It’s likely however, that your ViewModel will still have some callbacks registered when it is destroyed, and this is okay. The Data Binding framework uses weak references for the observers and it’s not required that they be unregistered before the ViewModel is destroyed. This won’t cause any memory leaks.
With that said, if you rotate the phone rapidly, several times in a row, while on the same screen. You’ll notice ObservableViewModel.addOnPropertyChangedCallBack is called several times and if you look inside the source for android.databinding.ViewDataBinding, you’ll see the observer count does rise each time.
This is where the periodic removal comes in. If you use the app long enough, rotate a few times, and have a breakpoint set on ObservableViewModel.removeOnPropertyChangedCallback. You’ll see that it is called periodically to clean up old observers and if you look up the call stack you can find more detail about where that comes from, how it’s triggered, etc.
You can track more at: https://caster.io/lessons/android-mvvm-pattern-with-architecture-component-viewmodels.
Hope this help you!!
Upvotes: 1
Reputation: 3504
You can do that using removeOnPropertyChangedCallback
function in ViewModel class. Here is how your ViewModel would look like:
abstract class ObservableViewModel(app: Application): AndroidViewModel(app), Observable {
@delegate:Transient
private val mCallBacks: PropertyChangeRegistry by lazy { PropertyChangeRegistry() }
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
mCallBacks.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
mCallBacks.remove(callback)
}
fun notifyChange() {
mCallBacks.notifyChange(this, 0)
}
fun notifyChange(viewId:Int){
mCallBacks.notifyChange(this, viewId)
}
}
Upvotes: 1