Live Data and 2-Way Data Binding: Custom setter not being called - android

I am using 2-way data binding to update a LiveData String object from my ViewModel with a string set in the EditText:
<android.support.design.widget.TextInputEditText
android:id="#+id/writeReviewTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={viewModel.liveReviewTitle}"
/>
So, from my understanding, the ViewModel would have its liveReviewTitle attribute updated every time the text changed in the EditText. I assume this is happening through the usage of a TextWatcher or some sort of listening mechanism that is being taken care of for me by the library. I also thought that when the text needed to be updated, it would have its setter called. Which does not seem to be the case! When the text changes, I need to do some more stuff in my ViewModel, therefore I implemented a custom setter for liveReviewTitle, but it is not being called (I tried debugging).
This is how it looks like in the ViewModel class:
var liveReviewTitle: MutableLiveData<String> = MutableLiveData()
set(value) {
field = value
customLogicHere()
}
Tried debugging this setter but it never seems to be called! What is happening here? Feels a little confusing. The text is being updated, and it is saved in the ViewModel, it is just the setter that is not called.

Of course it's never called, you're not setting a new MutableLiveData, you're setting a new String value inside the MutableLiveData (possibly with setValue).
However, you should be able to intercept the value that's being set and execute custom logic after setting the value if you expose a MediatorLiveData instead of the MutableLiveData directly.
EDIT: the following should work as expected:
val liveReviewTitle: MutableLiveData<String> = MutableLiveData()
private val mediator = MediatorLiveData<String>().apply {
addSource(liveReviewTitle) { value ->
setValue(value)
customLogicHere()
}
}.also { it.observeForever { /* empty */ } }

#EpicPandaForce solution is proper but in EditText two way binding can be obtained in much simpler way.
Add attribute afterTextChanged to your widget as below:
android:afterTextChanged="#{viewModel::doLogic}"
Then in your ViewModel class just write method:
fun doLogic(s: Editable) {
//update Livedata or do other logic
}
EDIT
I have missed important documentation note. Much easier (and far more proprer) will be:
android:text="#={viewModel.someLivedata}
and then in our LifecycleOwner class we can update value of liveData everywhe when we need, and of course we can react on changes from registered observer.

#EpicPandaForce is right about your setter, it's for the MutableLiveData itself and not the value it's holding. So your LiveData should be a val, no need for it to be a var, and the framework should do the right thing as long as you set a LifecycleOwner on the binding. You could add another Observer to your LiveData in place of a custom setter to add your custom logic.

Related

Comparison between Layout Binding and Manual Binding(with observe) in MVVM architecture of android

Using Android Jetpack components and MVVM architecture, we can get live data updates in a View from a View Model in 2 ways, one is to bind the layout with the live data variable, other way is to observe the variable in code.
To illustrate my question I have taken an example. Suppose there is a view model interface getTimeString() which returns the current time.
a) Layout Data Binding
The view in the layout looks something like this
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
...
app:binder_data_date="#{sampleVM.timeString}"/>
The binding adapter looks something like this
#BindingAdapter("binder_data_date")
public static void binder_data_date(TextView text, String data) {
text.setText(data);
}
b) Manual Data binding (just to give it a name):
In Manual data binding, there is nothing in the layout view with respect to the binder, and I observe the live data using the observe() and update the textview.
FragmentSplashScreenManualBindingBinding fragmentSplashScreenBinding;
SampleViewModel sampleViewModel1 = ViewModelProviders.of(this).get(SampleViewModel.class);
public void onSomeRandomFunc(...) {
....
sampleViewModel1.getTimeString().observe(getViewLifecycleOwner(), data -> {
fragmentSplashScreenBinding.sampleText.setText(data);
});
}
I know the first method is much easier to use and both works.
But is using the second method and the way to access the variable (fragmentSplashScreenBinding.sampleText.setText()) in fragment to update the View correct?
Will the performance get impacted if I use the second method?
Your manual Data binding is not incorrect and doesn't have a significant impact on the performance but you will lose two benefits:
Null pointer exception handling: Layout Data Binding handles null data and you don't need to check null objects to prevent app crash when you want to extract data objects and pass them to views.
Code Reusability: If you want to use your layout in different Activities, with
Layout Data Binding you just need to pass the data variable to the layout. But for Manual Data binding you should copy the same code for each java class to assign variable to views which will make a lot of boilerplate code in complex views.
Moreover, If you are using data binding to replace findViewById() as your second method there is a better way called View Binding which you can read more about it here.
Instead of answering your 2 points in post directly - let me mention few key features of both data binding and Live data - which may eventually help you choose 1 over the other.
Live data class supports Transformations - this useful class provide a way to apply any changes to be done to the live data object before dispatching it to the observers, or you may need to return a different LiveData instance based on the value of another one. Below is a sample example of applying the Transformation on LiveData from Official android docs,
class ScheduleViewModel : ViewModel() {
val userName: LiveData
init {
val result = Repository.userName
userName = Transformations.map(result) { result -> result.value }
} }
As you can see from above example - in the "init" the LiveData Object is "transformed" using Transformations.map before dispatching its content to "observers"
Data binding is mostly works with set of Observables and cannot "transform" the data under observation before dispatching like in above example.
Another useful feature of with LiveData is a class called MediatorLiveData - this subclass which may observe other LiveData objects and react based on changes to it - With data binding AFAIK its very much restricted to a specific Observable Fields.

How can Two-way Data Binding leads to infinite loops?

I'm learning Data Binding by reading up on the official docs. Everything makes sense expect the possible infinite loops in the two-way binding. As per the official docs on two-way binding:
Be careful not to introduce infinite loops when using two-way data binding. When the user changes an attribute, the method annotated using #InverseBindingAdapter is called, and the value is assigned to the backing property. This, in turn, would call the method annotated using #BindingAdapter, which would trigger another call to the method annotated using #InverseBindingAdapter, and so on.
I understand first part of the statement that the method annotate with #InverseBindingAdapter will be called if the attribute is changed and new value is assigned to the backing property.
But what I don't understand is why #InverseBindingAdapter method is called again when #BindingAdapter method is called in this process and how it leads to infinite loops?
Better late than never I guess :) The reason why an infinite loop can happen is InverseBindingAdapter is a basically an observer for changes. So when a user changed something the onChanged observer in InverseBindingAdapter is triggered and executes some logic. So then BindingAdapter also reacts to the change in the field and updates value again so the change listener in InverseBindingAdapter is triggered again and not we are in a loop.
So here is some visual for that
User -> Types in their name "Joe"
InverseBindingAdapter -> triggered by the update
ObservableField/LiveData -> also updated with 2 way binding and now contains value "Joe"
As ObservableField/LiveData was updated BindingAdapter is triggered to set the new value into the filed.
InverseBindingAdapter -> detected another change in the field and got triggered.
step 3, 4, 5 on repeat....
Check my article on Medium on advanced DataBinding it actually describes this case with the ViewPager and 2 way binding example. (Yes, shameless self-plug disclaimer)
This issue can be resolved by checking the old and new values before setting the new value to the target view.
Example:
#BindingAdapter("android:text")
fun setText(editText: EditText, value: Int) {
val newVal = if (value == 0) "" else value.toString()
val oldVal = editText.text.toString()
if (oldVal == newVal) {
return
}
editText.setText(newVal)
if (newVal.isNotEmpty()) {
editText.setSelection(newVal.length)
}
}

Why does LiveData better than MutableLiveData?

In many samples i see that:
class DataViewModel{
val data:LivaData<Int>
get() = _data
private val _data = MutableLiveData<Int>()
}
But more simply looks like this:
class DataViewModel{
val data = MutableLiveData<Int>()
}
so, why need this more complicated code construction with 2 fields?
It's a practice designed to restrict modification of the value from outside the class.
LiveData is read-only.
MutableLiveData, as the name implies, allows one to change the value it holds.
If you expose a MutableLiveData directly, like in your second example, any code which can access that data field could also modify the value it holds.
Exposing the ability to change data's content from outside DataViewModel class could make it harder to debug and reason about where data's content is coming from at any given time.
MutableLiveData is essentially a LiveData with public access to two methods setValue() and postValue() for modifying that data.
Therefore, MutableLiveData is needed if you plan to modify the values of the LiveData.
However, in programming, it's a common concept to make your variables immutable or restrict the access of those who can modify the data of an object. You wouldn't want to expose the ability to modify the contents of variables within an object if there's no need to do so.
Therefore, for MutableLiveData, we normally use a getter to get it's parent form, which is LiveData.
By getting only LiveData, we can ensure that those who access the LiveData object can only read the values stored within with no ability to change them.
In a sense, it's just the concept of why you should use private variables with getters.

When to use Android’s LiveData and Observable field?

I’m implementing a MVVM and data-binding and I’m trying to understand when should I use Observable field over LiveData?
I already run through different documentations and discovered that LiveData is lifecycle aware, but in sample codes in Github these two are being used in ViewModel at the same time. So, I’m confused if LiveData is better than Observable field, why not just use LiveData at all?
Both have their use-cases, for instance:
If you want a life-cycle tolerant container for your UI state model, LiveData is the answer.
If you want to make the UI update itself when a piece of logic is changed in your view model, then use ObservableFields.
I myself prefer using a combination of LivaData and ObservableField/BaseObservable, the LiveData will normally behave as a life-cycle aware data container and also a channel between the VM and the View.
On the other hand the UI state model objects that are emitted through the LiveData are themselves BaseObservable or have their fields as ObservableField.
That way I can use the LiveData for total changes of the UI state.
And set values to the UI state model ObservableField fields whenever a small portion of the UI is to be updated.
Edit:
Here is a quick illustration on a UserProfile component for example:
UIStateModel
data class ProfileUIModel(
private val _name: String,
private val _age: Int
): BaseObservable() {
var name: String
#Bindable get() = _name
set(value) {
_name = value
notifyPropertyChanged(BR.name)
}
var age: Int
#Bindable get() = _age
set(value) {
_age = value
notifyPropertyChanged(BR.age)
}
}
ViewModel
class UserProfileViewModel: ViewModel() {
val profileLiveData: MutableLiveData = MutableLiveData()
...
// When you need to rebind the whole profile UI object.
profileLiveData.setValue(profileUIModel)
...
// When you need to update a specific part of the UI.
// This will trigger the notifyPropertyChanged method on the bindable field "age" and hence notify the UI elements that are observing it to update.
profileLiveData.getValue().age = 20
}
View
You'll observe the profile LiveData changes normally.
XML
You'll use databinding to bind the UI state model.
Edit: Now the mature me prefers Immutability instead of having mutable properties as explained in the answer.
You can use LiveData all the time, as long as there is a LifecycleOwner to observe. I prefer to keep bound fields that are only relevant to the ViewModel as Observable and use LiveData for fields whose state changes are also relevant to the Activity or Fragment.
LiveData - use with LifecycleOwner like activity or fragment
Observable - use with data binding
#Ahmed Ashraf's answer is misleading. If we are talking about data binding only, data binding itself is lifecycle aware, because we set the lifecycleOwner when we use data binding, and it already checks when the view is active.
binding.lifecycleOwner = viewLifecycleOwner
As a result, it's not necessary to use live data to hold a BaseObservable object, we can directly use the BaeObervable object inside the view model.
Coming back to the question, in one-way data binding, you can use LiveData or ObservableFields or even Stateflow, but in 2-way data binding, I still prefer ObservableFields, because it's more flexible, you can easily write custom logic in the observable field setter, and when the UI changes, it can call the setter to do some additional stuff.
class LoginViewModel : BaseObservable {
// val data = ...
#Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value
// React to the change.
saveData()
// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me)
}
}
}
And in xml file.
<CheckBox
android:id="#+id/rememberMeCheckBox"
android:checked="#={viewmodel.rememberMe}"
/>

Using LiveData to set visibility of TextView

I want to toggle the visibility of a TextView using LiveData. There have been a few other posts on setting the visibility with databinding, but these use Observables, whereas I want to leverage the (newer) LiveData. In particular, use a LiveData.
Using this documentation, and a few SO posts, I have already learned that you should correctly align your getter of your observable (LiveData) so that the return type matches the type expected by the setter for the View attribute you want to set. Specifically:
setVisibility() of View requires an int, whereas I have a LiveData member (so the getter in my ViewModel will also return this type)
converting this Boolean to View.VISIBLE and VIEW.GONE is possible using a ternary operator. I should also add safeUnbox() in my XML expression to make it a primitive boolean
Using these insights, in my ViewModel class, I have defined:
MutableLiveData<Boolean> textHintVisible;
After pressing a button, I set this value to False:
textHintVisible.postValue(false);
(note, I also tried with setValue())
Then, in my layout XML, I have included:
<TextView
android:visibility="#{(safeUnbox(viewModel.textHintVisible) ? View.VISIBLE : View.GONE)}"
/>
But still, my TextView is always visible. To debug, I have added an observer in my activity, and this confirms that my boolean is correctly toggled between true and false:
mHintsViewModel.getTextHintVisible().observe(this, new Observer<Boolean>() {
#Override
public void onChanged(#Nullable Boolean newInt) {
Log.i(TAG,"onChanged: "+newInt);
}
});
But my TextView stays visible all the time. What am I doing wrong? Is it impossible to use LiveData for this? Should I use an additional Converter? Or is my code in principle correct, but is this a bug in Android Studio? Any help is much appreciated.
One thing I have in mind is - have you set your binding to observe liveData? As per documentation you have to set the binding layout to observe lifecycle binding.setLifecycleOwner(this)

Categories

Resources