This is actually 2 questions.
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?
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:
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()
.
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With