Android two-way data binding working one way only - android

I'm having this problem for a while, hope someone can help me
I'm trying to implement two-way data binding for the first time, but I'm facing a weird problem.
The problem here is that every time that I set a value of a live data from my view model, the UI changes, but when I change the edit text value on the UI, it does not reflect on the view model live data value.
Seems like the two-way data binding is working one-way only, when the value is set from the view model
I have my view model, something like this:
class CreateAssignmentViewModel(): ViewModel() {
val assignment = MutableLiveData<String>()
}
then my activity:
class CreateAssignmentActivity: AppCompatActivity() {
private val createViewModel: CreateAssignmentViewModel by viewModel()
private lateinit var viewBinding: ActivityCreateAssignmentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_create_assignment)
viewBinding.lifecycleOwner = this
viewBinding.createViewModel = createViewModel
}
}
and then on my activity 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>
<import type="android.view.View" />
<variable
name="createViewModel"
type="com.marcelo.tasks.assignments.create.CreateAssignmentViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".assignments.create.CreateAssignmentActivity">
<EditText
style="#style/Base.EditText"
android:text="#{createViewModel.assignment}" />
</LinearLayout>
</layout>

Actually you didn't set two way data binding in your view. You have to use #={} for two way data binding. Check below:
Use
<EditText
android:text="#={createViewModel.assignment}" />
Instead of
<EditText
android:text="#{createViewModel.assignment}" />

Related

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.

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

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.

Android Studio 3.4 Kotlin BindingAdapter crashes the ap

I have been trying to use BindingAdapter in Android Studio 3.4 (the last update) with Kotlin for days now and nothing seems to work.
I first tried with the following tutorial: https://codelabs.developers.google.com/codelabs/android-databinding/#7
And it was outputing an error as soon as I reached the 8th step.
Furthermore I tried the simple example possible with an Empty Application, a single Activity, a single ViewModel, and a single BindingAdapter. Here is the XML code.
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewmodel"
type="com.example.testbindingadapter.DataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:greetings="#{viewmodel.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/textView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Now here is the ViewModel with the BindingAdapter
class DataViewModel : ViewModel() {
private val _name = MutableLiveData<String>()
val name : LiveData<String> = _name
init {
_name.value = "Amath"
}
}
#BindingAdapter("greetings")
fun setName(view: TextView, text: String) {
view.text = "Welcome, $text"
}
I have also enabled dataBinging in my Graddle. I added apply plugin: 'kotlin-kapt' as suggested in the following thread Cannot find the setter for attribute in Data binding. At first I had an error msg:Cannot find the setter for attribute databinding subsequently the error disappeared, but the app simply crashed.
Can you help ?
You never set the viewmodel into databinding:
binding.viewmodel = viewModel

Android mvvm livedata and databinding

I am experimenting the new architecture components from Google trying to achieve more reactive code using ViewModel, LiveData and DataBinding.
Basically my idea around ViewModel is to have only one field of type Model(user for the record since we are representing a user profile scree). So my ViewModel class is :
class ViewModel : ViewModel() {
var model = MutableLiveData<User>()
and my Model class is :
class User(var name: String, var lastName: String, var age: Int)
In my layout file, I am trying to bind the fields on my Model into the view using DataBinding plugin. The problem is that since my ViewModel has a MutableLiveData<User> I can't access (from xml binding) the fields inside the User class (name, lastName...).
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.github.andromedcodes.mvvmtutorial.ViewModel" />
</data>
<RelativeLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="#{viewModel.user.name}"/>
</RelativeLayout>
</layout>
Is it even possible to do that? And which is better, having a ViewModel with separate Fields (String, Int, Whatever...) or re-using a Model?
You can see my repository where I did everything using DataBinding and MVVM pattern . Just visit https://github.com/xyarim/android-architecture

Databind LiveData to CustomView

I have a ViewModel with a List auf MutableLiveData<Data> in my Fragment Layout I set the data variable of my CustomView with one of the data elements from the List.
This works fine when it first loads but it doesn't update when I change a value in my data object.
Not really sure how to do this, until now I just used two-way data binding with EditText and MutableLiveData for example.
CustomView Layout:
<data>
<variable
name="data"
type="androidx.lifecycle.LiveData<Data>"/>
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardBackgroundColor="#{data.color}"
app:cardCornerRadius="16dp">
Class:
var data: MutableLiveData<Data>? = null
set(value) {
binding.data = value
}
Fragment Layout:
<data>
<variable
name="viewModel"
type=".ViewModel" />
</data>
<CustomView
.
.
.
app:data="#{viewModel.data[1]}" />
The reason for the update only happening the first time the screen is loaded is that the XML is used to inflate the View and then the initial item is used and set to the CustomView.
Then when the item in the list is updated, it does not trigger an update in the CustomView.
What you might be looking for is #BindingAdapter
#BindingAdapter("enableButton")
internal fun Button.enableButton(enabled: Boolean?) = enabled?.let { isEnabled = it } ?: also { isEnabled = false }
And then using it in the following way:
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button Text"
app:enableButton="#{viewModel.observeStatus()}" /> // <- Observes Boolean
A good walk-through might be at the following link: BindingAdapter
Note: The example is only for a Boolean observation, but it can simply be changed to match whatever object is observed.

Categories

Resources