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

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)
}
}

Related

What is the difference between LiveData<String>() and LiveData<String?>()

I am new to LiveData thing in general and I am having a hard time understanding the difference between LiveData<String>() and LiveData<String?>(). I used them interchangeably and nothing seams to break. I know that LiveData.getValue() is marked with #Nullable in Java, so we end up getting String? anyway. So what makes LiveData<String?>() different from LiveData<String>()?
This ended up a bit long but I hope it covers everything!
A LiveData is meant to be observed. The observer receives data, and the LiveData's type says what type that data is. A LiveData<String> will only supply non-null Strings to its observers. A LiveData<String?> can supply Strings and nulls.
Which of those you want depends on what you're doing! Do you need to supply nulls, e.g. for some kind of missing value or whatever? Should they be part of your data? If not, like in any other situation, avoid making the type nullable unless it needs to be.
When an observer first observes a LiveData, it receives the current value. That way it can immediately handle the current data, update to display the current state, etc. But it's possible for a LiveData to have no value initially:
// non-null
val liveDataWithValue = MutableLiveData<String>("hi")
val emptyLiveData = MutableLiveData<String>()
// nullable
val nullableLiveDataWithValue = MutableLiveData<String?>(null)
val emptyNullableLiveData = MutableLiveData<String?>()
The first one there has an initial value. If you observe it, and that value hasn't been updated, the observer will immediately be called with "hi" for its parameter.
The second one has no value. If you observe that, the observer won't be called until a value is set on it. This is useful when you don't actually have any initial data - you can still set up your observer, and nothing will happen until some data is actually pushed.
The third one is the same as the first - it's a nullable String? but with a value of null. That's still a value so if you observe it, the observer will immediately be called with that null. It's still a piece of data your observer has to react to and process.
The last one is nullable but with no initial value. Like the second one, this means there's nothing for the observer to receive at first - but when it does have a value set on it, it could be a null. null is just another kind of value!
But if you go poking around at the LiveData's value property, instead of interacting with it through observe, then that no value state is represented internally by null. Java (or at least the version Android targets) doesn't really have a representation of no value separate from null, so that's just how they have to do things. It just doesn't publish anything until you explicitly set a value on it.
So for each of these:
val emptyLiveData = MutableLiveData<String>()
val nullableLiveDataWithValue = MutableLiveData<String?>(null)
val emptyNullableLiveData = MutableLiveData<String?>()
if you read their value in this state, it will be null for all of them. One explicitly has a value of null set on it, the others are both empty. This also means that even though emptyLiveData's type is non-null, its value property can be null, just because of this "can be empty" situation which is true for all LiveData objects. The nullability of the type is purely about what gets passed to observers.
You generally shouldn't be reading value anyway, except internally (wherever you're actually setting the value). Everything else should be interacting with that LiveData by observing it and reacting to the values that are published, and those values will be whatever type (nullable or not) that you specified
LiveData<String?>() meens that livedata can store null, read please this article to be fully informed: https://kotlinlang.org/docs/null-safety.html

Using mutableStateOf instead of observeAsState

I'm working with Jetpack Compose in an Android app and had the problem that my uiState (LiveData) was set to its initial value on every recomposition, since I've initialized it like
val authUiState: AuthUIState by authenticationViewModel.uiState.observeAsState(AuthUIState.Loading)
It was set to Loading on every recomposition before it was set to the correct value.
When I tried to Remember the value, I learned that we can't use observeAsState within the remember block and finally changed it to
val authUiState = remember{ mutableStateOf(authenticationViewModel.uiState.value) }.value
This works, but I'm not quite sure, if this is the common and good way to solve this.
What do you think? Should I do it differently? Do you need more information?
See if the uiState inside your viewmodel is something like a LiveData Object, (which is kinda what it seems like from the code), the recommended way is to store it in the viewmodel itself as mutable state.
var uiState by mutableStateOf (initialValue)
private set //Do not allow external modifications to maintain consistency of state
fun onUiStateChange(newValue: Any){
uiState = newValue
}
You just need to initialise it as a MutableState, in the rest of the code, to update, delete or whatever you want to do with it, just treat it as a regular variable. Compose will trigger recomposition every time the value is updated.
The following code snippet below will almost certainly not work, because here, the state is whatever you wrap inside mutableStateOf(), which is just a simple value which will be fetched once from the viewmodel and then remembered throughout recompositions, so no code change will be triggered here
val authUiState by remember{ mutableStateOf(authenticationViewModel.uiState.value) }
Storing state in the viewmodel as mutableState, is as far as my knowledge extends, the best practice in compose. You will see the same in the 'State Codelab' from Android developers
Good luck

How to force jetpack compose to recompose?

Say that, I'm building a custom compose layout and populating that list as below
val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
list.forEach { item ->
key(item){
listItemCompose( data = item,
onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
}
}
This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following
{index1,index2 -> list[index1].propertyName = true}
Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.
You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.
The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.
Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.
This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.
you can recreate an entire composition using this
val text = remember { mutableStateOf("foo") }
key(text.value) {
YourComposableFun(
onClick = {
text.value = "bar"
}
) {
}
}
call this
currentComposer.composition.recompose()

How to set default value in BehaviourSubject

Probably a noob question. How do I set a default value to a BehaviourSubject.
I have an enum with 2 different values
enum class WidgetState {
HIDDEN,
VISIBLE
}
And a behaviour subject which emits the states
val widgetStateEmitter: BehaviorSubject<WidgetState> = BehaviorSubject.create()
My emitter starts emitting when the view logic is written. However it's HIDDEN by default. How do I set the default value as WidgetState.HIDDEN to my emitter widgetStateEmitter?
There's a static BehaviorSubject.createDefault(T defaultValue) factory method that allows to set the initial value.
The Javadoc for the defaultValue parameter says:
defaultValue - the item that will be emitted first to any Observer
as long as the BehaviorSubject has not yet observed any items from
its source Observable
So you just have to create your BehaviorSubject as follows:
val widgetStateEmitter: BehaviorSubject<WidgetState> =
BehaviorSubject.createDefault(HIDDEN)
In your constructor or onCreate (or similar) just call widgetStateEmitter.onNext(HIDDEN)
When Subscribing to this Subject you can use Start with Operator
widgetStateEmitter.startWith(HIDDEN)
//continue your chain

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

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.

Categories

Resources