problems using databinding: val vs var and the use invalidateAll() - android

This is actually 2 questions.
I noticed that databinding doesn't work if in the Person data class I set the name parameter to be val instead of var. The code will break with the following error:
error: cannot find symbol
import com.example.android.aboutme.databinding.ActivityMainBindingImpl;
^
symbol: class ActivityMainBindingImpl
location: package com.example.android.aboutme.databinding
Why does it happen?
Why do I need to call invalidateAll() in doneClick()? The documentation says that it "Invalidates all binding expressions and requests a new rebind to refresh UI". Isn't the purpose of databinding to connect data and views in such a way that an update to the data immediately updates the views?
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val person = Person("Bob")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.person = person
binding.apply {
btnDone.setOnClickListener { doneClick(it) }
}
}
private fun doneClick(view: View) {
binding.apply {
person?.nickname = etNickname.text.toString()
invalidateAll()
etNickname.visibility = View.GONE
tvNickname.visibility = View.VISIBLE
btnDone.visibility = View.GONE
}
hideKeybord(view)
}
private fun hideKeybord(view: View) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}
Person:
class Person(var name: String, var nickname: String? = null)
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="person"
type="com.example.android.aboutme.Person" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="#dimen/padding"
android:paddingEnd="#dimen/padding">
<TextView
android:id="#+id/tv_name"
style="#style/NameStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={person.name}"
android:textAlignment="center" />
<EditText
android:id="#+id/et_nickname"
style="#style/NameStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="#string/what_is_your_nickname"
android:inputType="textPersonName"
android:textAlignment="center" />
<Button
android:id="#+id/btn_done"
style="#style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="#dimen/layout_margin"
android:fontFamily="#font/roboto"
android:text="#string/done" />
<TextView
android:id="#+id/tv_nickname"
style="#style/NameStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={person.nickname}"
android:textAlignment="center"
android:visibility="gone" />
<ImageView
android:id="#+id/star_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/layout_margin"
android:contentDescription="#string/yellow_star"
app:srcCompat="#android:drawable/btn_star_big_on" />
<ScrollView
android:id="#+id/bio_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="#dimen/layout_margin">
<TextView
android:id="#+id/bio_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="#dimen/line_spacing_multiplier"
android:text="#string/bio"
android:textAppearance="#style/NameStyle" />
</ScrollView>
</LinearLayout>
</layout>

Qustion 1:
I noticed that databinding doesn't work if in the Person data class I set the name parameter to be val instead of var.
Why does it happen?
Because you're using two-way databinding.
In your layout you have this:
<TextView
android:id="#+id/tv_name"
style="#style/NameStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={person.name}"
android:textAlignment="center" />
The #= in android:text="#={person.name}", specifically, tells databinding "I want to set the TextView's text to the person's name value and I want to update the person's name when the TextView text changes".
When you use the #= databinding will look for a setter for the attribute you're assingning. In this case, it's looking for a setter for the name attribute on the Person class. In Kotlin, this means having a property named name that is a var.
If you do not intend to update the person's name attribute when the TextView changes (which I assume you don't, you'd generally do that with an EditText), then change that line to just # (android:text="#{person.name}"). Then you can make name a val because you're only reading from it for databinding.
Question 2:
Why do I need to call invalidateAll() in doneClick()?
You actually don't ...
The documentation says that it "Invalidates all binding expressions and requests a new rebind to refresh UI". Isn't the purpose of databinding to connect data and views in such a way that an update to the data immediately updates the views?
Yes, but: databinding is not magic. If the UI is to update it must be told to do so and changing your data does not magically tell databinding that it has to update. Something has to tell databinding that a) it's time to update and b) what it needs to update.
So what you have right now with invalidateAll() is the shotgun approach. You updated the one nickname field and then you yelled at databinding "hey, update everything!", so it rebinds all views based on the current state of Person which of course includes "nickname" so that view gets updated.
What you want to do is update only the fields that are bound to nickname because that is the one thing that changed and, preferably, you want to do it automatically when nickname changes. For that, you need to observe the state of the nickname field and react to it changing.
You can do this in a few ways:
Use LiveData
In this approach you have the fields of the model you want to bind be LiveData objects (val nickname = MutableLiveData<String>()) and you add a LifeCycleOwner to the binding so it can observe the LiveData objects.
Databinding is set up to use LiveData so your xml does not need to change. But now the properties are observable and when you update the name on Person (person?.nickname?.value = "New Nickname") databinding will be notified automatically and will update the state of the associated view.
You will not have to call invalidateAll().
Use Observable Fields
This is conceptually the same as #1 but this came before LiveData was introduced. Nowadays you can consider this deprecated and use the LiveData approach, but I'll mention it for completeness.
Again, instead of having a regular property of type String you wrap that property in an observable data structure (val nickname = ObservableString()) that will notify databinding when the value has changed. Again, databinding is set up to work with this so you don't have to change your XML.
Use Observable Objects
With this option, you make your Person class (or preferably a ViewModel) extend Observable and manage notifying databinding yourself as the fields change. You would go this route if you have special logic that has to happen when updating some fields and a simple "set and notify" is not enough. This option is far more complicated and I'll leave it as an exercise to the reader to read the docs to see how this option works. For the vast majority of cases you should be able to do what you need with option #1.
Parting thought on this line:
person?.nickname = etNickname.text.toString()
If you set up databinding correctly, this should not be necessary. :)
If you set up etNickname to use two-way binding and make person.nickname properly observable, the person.nickname attribute will automatically update to the text value in etNickname when it changes!
That is the beauty of databinding.
Hope that helps!

Val = Inmutable
Var = mutable
Full answer
Val and Var in Kotlin
It's because the properties have no built-in mechanisms to notify the UI that they've changed. So you have to invoke it manually. A solution for this problem is using LiveData or MutableLiveData.

Related

Android Two Way DataBinding Problem of Ternary Operator Must be Constant

My EditText is like this:
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:text="#={viewModel.isAddCase? ``: `` + viewModel.currentStudent.age}" //problem here
android:inputType="number" />
I want the EditText not to show anything (empty String) based on the isAddCase variable, which is a MutableLiveData<Boolean> initilized when the ViewModel class object is created (inside the init{} block).
This is the error I got:
The expression '((viewModelIsAddCaseGetValue) ? ("") : (javaLangStringViewModelCurrentStudentAge))' cannot be inverted, so it cannot be used in a two-way binding
Details: The condition of a ternary operator must be constant: android.databinding.tool.writer.KCode#37a418c7
UPDATE
Even this doesn't work, shows the same error:
android:text="#={viewModel.currentStudent.age == 0? ``: `` + viewModel.currentStudent.age}"
I guess ternary operation just doesn't work well with two-way DataBinding.
Ok, I've figured out the perfect solution after these days:
1. Create BindingAdapter Function:
object DataBindingUtil { //place in an util (singleton) class
#BindingAdapter("android:text", "isAddCase") //custom layout attribute, see below
#JvmStatic //required
fun setText(editText: EditText, text: String, isAddCase: Boolean) { //pass in argument
if (isAddCase) editText.setText("") else editText.setText(text)
}
}
Passing multiple arguments from layout to BindingAdapter function:
How can I pass multiple arguments via xml for a custom setter when using Android data binding
2. Apply Custom Attribute in View:
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:inputType="number"
android:text="#={`` + viewModel.currentStudent.age}" //two-way binding as usual
app:isAddCase="#{viewModel.isAddCase}" /> //here
The BindingAdapter function is triggered only when using EditText and custom attribute At The Same Time.

Why doesn't data bing use LiveData or Observable fields?

In my mind, One-way or Two-way data bing use either LiveData or Observable fields.
The following code is from the project https://github.com/enpassio/Databinding
The attribute android:text="#={viewModel.toyBeingModified.toyName}" of the control android:id="#+id/toyNameEditText" bind to viewModel.toyBeingModified.toyName with Two-way data bing.
I'm very strange why viewModel.toyBeingModified is neither LiveData or Observable fields, could you tell me?
fragment_add_toy.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="AddToyBinding">
<variable
name="viewModel"
type="com.enpassion.twowaydatabindingkotlin.viewmodel.AddToyViewModel" />
<import type="com.enpassion.twowaydatabindingkotlin.utils.BindingUtils"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/margin_standard">
<androidx.cardview.widget.CardView
android:id="#+id/cardEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/margin_standard"
app:cardBackgroundColor="#color/skin_rose"
app:cardCornerRadius="#dimen/card_corner_radius"
app:cardElevation="#dimen/card_elevation"
app:contentPadding="#dimen/padding_standard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/toyNameLayout"
style="#style/Widget.Enpassio.TextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/toy_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="#+id/guidelineET"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteY="418dp">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/toyNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:text="#={viewModel.toyBeingModified.toyName}"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
AddToyViewModel.kt
class AddToyViewModel(private val mRepo: ToyRepository, private val chosenToy: ToyEntry?) : ViewModel() {
val toyBeingModified: ToyEntry
private var mIsEdit: Boolean = false
init {
if (chosenToy != null) {
//This is edit case
toyBeingModified = chosenToy.copy()
mIsEdit = true
} else {
/*This is for adding a new toy. We initialize a ToyEntry with default or null values
This is because two-way databinding in the AddToyFragment is designed to
register changes automatically, but it will need a toy object to register those changes.*/
toyBeingModified = emptyToy
mIsEdit = false
}
}
private fun insertToy(toy: ToyEntry) {
mRepo.insertToy(toy)
}
...
}
ToyEntry.kt
data class ToyEntry(
var toyName: String,
var categories: Map<String, Boolean>,
var gender: Gender = Gender.UNISEX,
var procurementType: ProcurementType? = null,
#PrimaryKey(autoGenerate = true) val toyId: Int = 0
): Parcelable{
/*This function is needed for a healthy comparison of two items,
particularly for detecting changes in the contents of the map.
Native copy method of the data class assign a map with same reference
to the copied item, so equals() method cannot detect changes in the content.*/
fun copy() : ToyEntry{
val newCategories = mutableMapOf<String, Boolean>()
newCategories.putAll(categories)
return ToyEntry(toyName, newCategories, gender, procurementType, toyId)
}
}
In fact, we use LiveData or Observable fields when we need to do something as soon as they changed, search bar can be a good example. But in this case, we don't care when the user is changing the properties of the selected toy (I haven't seen the UI but I'm assuming there is a Save button or something like that). In other words, we don't want to do anything while user is typing b, bo, boa and finally boat.
We just need that data to be once set while the viewmodel is set to binding, let the user change it to whatever and when we want to do the saving process, we want our field to be what user had entered.
In addition, if you use LiveData in your binding (as long as the lifecycleOwner is set) you're adding an observer to you LiveData which can be a point of concern for some geeks 😂.
TL;DR
We use LiveData when we want to observe it (which is not required in the example you provided). It's an option not a must. Data binding can set/get data for nearly everything.
I would suggest to start with 1-way data binding first and as soon as this works, extend it to 2-way data binding. What you are doing wrong right now is the following:
android:text="#={viewModel.toyBeingModified.toyName}"
This line of code means that you pass a ToyEntry object to a setText() method of the TextView. That means the TextView would need to have a method with the signature: setText(entry: ToyEntry).
Of course, this method does not exist (yet). So to make this data binding work, you have to define this method yourself by creating a BindingAdapter:
#BindingAdapter("toyEntry")
fun setToyEntry(textView: TextView, toyEntry: ToyEntry) {
// in here you define what to do with the textView. For example:
textView.text = toyEntry.toyName
}
You can create this BindingAdapter in any file without the need to put it into a class.
You can give this method any name you want
The first parameter of this method is the kind of View in the xml that you want to bind the toyEntry to
The second parameter of this method os the object that you set in your xml via #{...}
Now when you write a 1-way databinding like this: binding:toyEntry="#{viewModel.toyBeingModified.toyName}"
The binding namespace can be craeted by AndroidStudio automatically. You can name this anything you want (but not android, since this is already defined)
The toyEntry is what connects this line of xml to your BindingAdapter from the previous step (it corresponds to the same string that you set in the annotation #BindingAdapter(...)
Now, the generated code knows about your binding adapter and calls its method setToyEntry when it computes this data binding. You can also delete the line android:text="#={viewModel.toyBeingModified.toyName}", because it is not used anymore.
Go from there to setup 2-way data binding. Here you also have to create #InverseBindingAdapter as explained here: https://developer.android.com/reference/android/databinding/InverseBindingAdapter
Some more comments: Depending on your gradle version, you have to enable databinding and also make sure to have all dependencies and gradle plugins setup.
More on that here: https://developer.android.com/jetpack/androidx/releases/databinding?hl=en

2 way data binding error for a livedata variable with proper xml binding

I have a loginViewModel for my login Activity, in activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="loginViewModel"
type="com.example.test.ui.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/login_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="#+id/mobile_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:editable="true"
android:ems="10"
android:hint="#string/mobile_number_string"
android:inputType="phone"
android:textAlignment="center"
android:text="#={loginViewModel.phoneNumber}"
app:layout_constraintBottom_toTopOf="#+id/otp_input"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my loginViewModel i have defined my livedata as
class LoginViewModel : ViewModel(){
private val _phoneNumber = MutableLiveData<String>()
val phoneNumber : LiveData<String>
get() = _phoneNumber
}
Now, while building I am getting the following error
The expression \u0027loginViewModelPhoneNumber.getValue()\u0027 cannot be inverted, so it cannot be used in a two-way binding\n\nDetails: There is no inverse for method getValue, you must add an #InverseMethod annotation to the method to indicate which method should be used when using it in two-way binding expressions
All articles that I am reading suggesting this way to implement.
Can someone tell me what am I doing wrong here?
Unfortunately for two-way data binding you need to use MutableLiveData.
You should remove private on _phoneNumber.
Then change xml to use it android:text="#={loginViewModel._phoneNumber}".
change from
private val _phoneNumber = MutableLiveData<String>()
to
public val _phoneNumber = MutableLiveData<String>()
You are binding phoneNumber which is a LiveData that does not have any interface for writing value.
Consider removing phoneNumber, and using Kotlin-based approach, with just a public property
There are several issues with the code.
you try to use val phoneNumber that is not mutable in two way binding, thus is has only getter so binding class can only read value from field, to receive data from the UI - binding class wants to use setter, but there is no setter present, thus the phoneNumber won't change.
for the property that binds phoneNumber you try to use LiveData which is not mutable - you should use MutableLiveData in case you need it to be able to change.
if you want to listen to the changes of phoneNumber you need to add LiveData Observer like
phoneNumber.observe{
val value = it
}
Hope it helps.

Is BindingAdapter really saving our time

I am working on new project and just started using Databinding. I heard people talking abaout code reduction.I have a RecyclerView in fragment. I have a simple ConstraintLayout in which I have 3 TextViews. This is in my Adapter for RecyclerView.
That aditional +" nazv" is just a sample and I could use String Extension.
fun bind(item: Mkdo) {
binding.txtMkdoNaziv.text = item.nazv+" nazv"
binding.txtMkdoPost.text = item.post
binding.txtmkdoPostNaziv.text = item.postNaziv
}
ConstraintLayout for single RecyclerView row (deleted positioning properties)
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"/>
<TextView
android:id="#+id/txtMkdoPost"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This works just fine.
If I want to convert it to Databinding I have to add:
fun bind(item: Mkdo) {
binding.mkdo=item
binding.executePendingBindings()
}
And BindingAdapter
#BindingAdapter("setNazv")
fun TextView.setNazv(item:Mkdo?){
item?.let {
text="${item.nazv} nazv"
}
}
ConstraintLayout for single RecyclerView row (deleted positioning properties) with Databinding
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintRowMkdo"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txtMkdoNaziv"
app:setNazv="#{mkdo}""/>
<TextView
android:id="#+id/txtMkdoPost"
android:text="#{mkdo.post}"/>
<TextView
android:id="#+id/txtmkdoPostNaziv"
android:text="#{mkdo.postNaziv}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
This makes constraintRowMkdo nonreusable and in my opinion adds more work than without it.
Also if I have some string formatting I need to have it on two places string Extension and BindingAdapter. I can reuse Extension but it is still on who places.
What are your toughts on this?
Am I missing something?
Are there any aditional benefits of using it?
Thanks

Data binding - access individual properties contained in LiveData

Is there a way to do something like this with LiveData and data binding?
ViewModel has this property:
val weather: LiveData<UnitSpecificCurrentWeatherEntry>
What I'm trying to do in the layout:
<TextView
android:id="#+id/textView"
android:text="#{viewmodel.weather.value.someProperty}"... />
Is this possible in any way or do I have to split the object contained in LiveData into multiple ones for each property of the contained object?
From the point of view of MVVM pattern it's not entirely correct. In your example view require know about property path to display data. Preferable to provide target data directly from ViewModel. If your property is depend from another, you can use Transformations:
val weather: LiveData<UnitSpecificCurrentWeatherEntry> = //suppose, we have instantiation here
val someProperty: LiveData<SomePropertyType> = Transformations.map(weather) { it.someProperty }
Now, you can use it in your xml:
<TextView
android:id="#+id/textView"
android:text="#{viewmodel.someProperty}"/>

Categories

Resources