How Do We Get Data Binding To Use Saved Instance State? - android

TL;DR: If a layout used with data binding has an EditText, and there is a binding expression for android:text, the binding expression overwrites the saved instance state value... even if we do not explicitly trigger a binding evaluation. What the user typed in before the configuration change gets wiped out. How do we work around this, so that on a configuration change, the saved instance state value is used?
We have a silly Model:
public class Model {
public String getTitle() {
return("Title");
}
}
And we have a layout that references that Model:
<?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="model"
type="com.commonsware.databindingstate.Model" />
</data>
<android.support.constraint.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.commonsware.databindingstate.MainActivity">
<EditText android:id="#+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
Note that this layout has no binding expressions; we'll get to that in a bit.
The layout is used in a dynamic fragment:
public class FormFragment extends Fragment {
#Nullable
#Override
public View onCreateView(LayoutInflater inflater,
#Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
return(MainBinding.inflate(inflater, container, false).getRoot());
}
}
Note that we are not calling setModel() anywhere to push a Model into the binding. The MainBinding (for the main.xml layout shown above) is just used to inflate the layout.
This code (with a suitable FragmentActivity to set up the FormFragment) correctly uses the saved instance state. If the user types something into the EditText, then rotates the screen, the newly-recreated EditText shows the entered-in text.
Now, let's change the layout to add a binding expression for android:text:
<?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="model"
type="com.commonsware.databindingstate.Model" />
</data>
<android.support.constraint.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.commonsware.databindingstate.MainActivity">
<EditText android:id="#+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="text"
android:text="#{model.title}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
Now, if the user types something into the EditText and rotates the screen, the newly-recreated EditText is empty. The binding expression overwrites whatever the framework restored from the saved instance state.
This comes despite the fact that I am not calling setModel() on the binding. I can certainly see where if I called setModel() on the binding where that would replace the EditText contents with the data from the model. But I am not doing that.
I can reproduce this behavior on both official devices (Google Pixel, Android 8.0) and ecosystem devices (Samsung Galaxy S8, Android 7.1).
This can be worked around "manually" by saving the state ourselves and restoring it at some point. For example, multiple comments have suggested two-way binding, but that runs counter to other design objectives (e.g., immutable model objects). This seems like a rather fundamental limitation of data binding, so I am hoping that there's something that I missed that I can configure to have the saved instance state be used automatically.

I thought that ianhanniballake had a reference to a relevant answer, but maybe there is more to it. Here is my interpretation of how that reference can be applied to these circumstances.
Using the XML that you presented, the following code will alternately restore from the saved instance state and restore from the model. When the saved instance state is restored then, presumably, there is not model instantiated to restore from. That is when mCount is even. If a model exists, then the saved instance state is basically ignored and the binding takes over. There is a little more logic here than we want, but it is less than saving and restoring explicitly.
mCount is just an artifice for the sake of the argument. A flag or other indication of whether the model exists or not would be used.
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private int mCount;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mCount = (savedInstanceState == null) ? 0 : savedInstanceState.getInt("mCount", 0);
if (mCount % 2 == 1) {
// 1st, 3rd, 5th, etc. rotations. Explicitly execute the bindings and let the framework
// restore from the saved instance state.
binding.executePendingBindings();
} else {
// First creation and 2nd, 4th, etc. rotations. Set up our model and let the
// framework restore from the saved instance state then overwrite with the bindings.
// (Or maybe it just ignores the saved instance state and restores the bindnings.)
Model model = new Model();
binding.setModel(model);
}
mCount++;
}
#Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putInt("mCount", mCount);
}
}

Related

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

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.

How to manage Android MVVM, Android Architecture component (LiveData) with DataBinding for many UI fields?

I know Android MVVM, LiveData and DataBinding. But, I have a scenario in which I have many input UI fields such as Email, Password, Confirm Password and etc. I can map those fields with ViewModel.
public class LoginViewModel extends ViewModel {
public MutableLiveData<String> email = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
.
.
.
}
I bound this LoginViewModel with the following XML layout.
<?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="viewModel.LoginViewModel" />
</data>
<RelativeLayout
android:id="#+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".view.MainActivity">
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textEmailAddress"
android:text="#={loginViewModel.email}" />
<EditText
android:id="#+id/editText2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/editText"
android:ems="10"
android:inputType="textPassword"
android:text="#={loginViewModel.password}" />
</RelativeLayout>
</layout>
Actually, I have more UI fields so which ideal approach should I follow? Whether to declare the exact same LiveData in ViewModel based on my UI. Like 10 UI fields should have 10 LiveData in ViewModel.
Your view model should expose 10 data fields if your UI can show it (don't overload UI). It's how MVVM works. But! You should expose differed types of fields depends on field behaviour:
LiveData for read-only fields (e.g. TextView)
MutableLiveData for mutable fields, two way databinding (e.g. EditText)
non LiveData type for constant (read-only) data. If you know data not changed during view model life cycle, you can expose data without LiveData. In this case your data will be binded once when you setup view model variable.

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.

Why does my bound button, goes unpressed itself?

I made a button and bound it's pressed state to some observable.
In sample below, observable is just constant.
Nevrtheless, this button goes unpressed either after some time itself, or after application switch
Activity code is follows:
public class TryResearchButtonUnpress extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_try_research_button_unpress);
ActivityTryResearchButtonUnpressBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_try_research_button_unpress);
binding.setActivity(this);
}
public final ObservableBoolean pressed = new ObservableBoolean(true);
}
layout code is follows
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="activity" type="com.inthemoon.tryresearchbuttonunpress.TryResearchButtonUnpress"/>
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/activity_try_research_button_unpress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:gravity="center">
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#null"
android:pressed="#{activity.pressed}"
android:src="#drawable/record_selector"
/>
</RelativeLayout>
</layout>
Resources and full code are in GitHub: https://github.com/dims12/TryResearchButtonUnpress
I suspect this is because I bound values to the view itself. This was nowhere described, but I tried it on my own risk.
UPDATE
I have put the observable into separate class, but button is unpressing nevertheless.
See github commit 690fbee.
UPDATE 2
I was unable to set "bidirectional binding" with the following code
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#null"
android:pressed="#={model.pressed}"
android:src="#drawable/record_selector"
/>
due to the following error
Error:(8, 58) error: package
com.inthemoon.tryresearchbuttonunpress.databinding does not exist
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
java.lang.RuntimeException: Found data binding errors.
****/ data binding error ****msg:Cannot find the getter for attribute 'android:pressed' with value type boolean on
android.widget.ImageButton.
file:D:\Users\Dims\Design\TryResearchButtonUnpress\app\src\main\res\layout\activity_try_research_button_unpress.xml
loc:18:8 - 24:13
****\ data binding error ****
Button - this is UI component, which provide feedback. I think you need create two-way databinding to recive and store pressed (unpressed) state. I.e.
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#null"
android:pressed="#={activity.pressed}"
android:src="#drawable/record_selector"/>
Next, you need to implement #InverseBindingAdapter for reverse binding from view:
#InverseBindingAdapter(attribute = "android:pressed")
public static boolean getPressedState(ImageButton button){
return button.isPressed();
}
Also, you need to save button state using override of onSaveInstanceState() menhod and restore it in override of onRestoreInstanceState().
i'm not sure if the twoway binding works , because i never used it (tried but never worked)
So the workaround is to
<Button
...
android:onClick="#{activity.onClicked}"/>
and on the activity (which is working as your ViewModel)
have a method
public void onCliked(Button view){
// update datamodel pressed state
model.setPressed(view.isPressed());
}
//Disclaimer .. this is just pseudo code, the idea is this

Categories

Resources