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)
Related
I would like to show a snackbar in the MainActivity (root composable) from any child #Composable.
My first thought was to provide the SnackbarHostState using CompositionLocalProvider but that doesn't seem to work (or I'm doing it incorrectly).
val mainSnackBarHostState = remember { SnackbarHostState() }
val SnackBarHostStateProvider = compositionLocalOf<SnackbarHostState> { mainSnackBarHostState }
CompositionLocalProvider(SnackBarHostStateProvider provides mainSnackBarHostState) {
MainScreenNavigationConfigurations(navController)
}
My child #Composable can't seem to find/access SnackBarHostStateProvider.
Any thoughts?
The best way, I'd say, is to store the state of the snackbar (visible/invisible) in your viewmodel, and let the snackbar read from there. Whenever and wherever from you want to toggle the state, just change the value in the viewmodel, and that should do it
If you are unfamiliar with viewmodel, it is the recommended and standard way to build apps, and remember, in Compose, the recommended way is to store state in the viewmodel not as regular variables, but as stateholders.
For instance, in your use case, you can store the visibility status of your snackbar as mutableStateOf(false), for am initial visibility value of false.
This assumes that you have access to your viewmodel from all over your app, which usually developers do, wherever they need to update state, so, best of luck
Probably you need to declare the variable SnackBarHostStateProvider as a package-level variable, to be able to access it from the provider and the children.
See also this related answer: https://stackoverflow.com/a/69905470/293878
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.
In my data binding layouts, I set long click listeners via:
android:onLongClick="#{ ..binding expression.. }"
The code runs as expected, but the android:onLongClick attribute is flagged as 'unknown' in the xml file. Additionally, there is no auto-complete for it.
The binding adapter for this attribute is included with the data binding library in ViewBindingAdapter.java.
As said here you can use: android:onLongClick="#{() -> handler.onLongClicked()}"
but if you want to remove warning you can use below code instead of above:
app:onLongClickListener="#{() -> handler.onLongClicked()}"
if you use app:onLongClickListener data binding will find setOnLongClickListener in the View class and will use that method
There is a difference between onLongClickListener and onLongClick : We have a method in view called setOnLongClickListener but we do not have a method like this: setOnLongClick and when you use an attribute atr in data binding that has a method like setAtr data binding will find and use that method automatically not needing any adapter. Thus onLongClickListener do not need any adapter (if there is an adapter it will be used instead of setOnLongClickListener) but onLongClick always needs adapter.
Thanks to Bahman for the helpful answer. Here's some more details and options.
If one uses the native xml binding app:onLongClickListener then the viewModel must return Boolean or android compilation crashes with cannot generate view binders java.lang.StackOverflowError
So this crashes the compiler: app:onLongClickListener="#{() -> viewModel.onLongClickRowNoReturn()}" assuming the viewModel method does not return Boolean. If it returns a boolean it works. The Boolean is required by View.OnLongClickListener see View.OnLongClickListener
Alternatively we can use our own custom adapter
#BindingAdapter("onLongClick")
fun setOnLongClickListener(view: View, listener: Runnable) {
view.setOnLongClickListener { listener.run(); true }
}
XML: app:onLongClick="#{() -> viewModel.onLongClickRowNoReturn()}"
Here's the docs as Bahman also linked, though they are not very informative regarding this issue.
My viewModel example:
override fun onLongClickRow():Boolean {
Toast.makeText(context, "LongClick", Toast.LENGTH_SHORT).show()
return true
}
override fun onLongClickRowNoReturn() {
Toast.makeText(context, "LongClick without return", Toast.LENGTH_SHORT).show()
}
onLongClick and onLongClickListener do the exact same thing because there is a BindingMethod that connects onLongClick to setOnLongClickListener in ViewBindingAdapter.java.
It seems like the IDE just complains about any android: prefixed attributes that don't exist in the framework. This is why it doesn't complain about the app: versions. However, they are not always freely interchangeable because for example android:text does some performance optimizations under the hood whereas app:text would just call setText directly.
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)
}
}
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.