Why does my bound button, goes unpressed itself? - android

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

Related

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.

How to use Two way data binding with radio button in android

I am trying to use two way data binding with the radio button. It is working fine with one way like below,
android:checked="#{registration.gender.equals(Gender.FEMALE.getValue())}".
But My problem is that, I need to set the value of selected radio button in my 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" xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.callhealth.customer.comman.enums.Gender" />
<import type="android.text.TextUtils" />
<import type="java.lang.Integer" />
<variable name="callback" type="com.callhealth.customer.usermanagement.callback.RegistrationCallback" />
<variable name="registration" type="com.callhealth.customer.usermanagement.model.request.LoginAndRegistrationRequestModel" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="#string/label_gender" android:textSize="15sp" />
<RadioGroup android:id="#+id/gender_group" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal">
<android.support.v7.widget.AppCompatRadioButton android:layout_width="wrap_content" android:checked="#={registration.gender.equals(Gender.MALE.getValue())}" android:layout_height="wrap_content" android:text="#string/label_male" />
<android.support.v7.widget.AppCompatRadioButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginStart="20dp" android:checked="#={registration.gender.equals(Gender.FEMALE.getValue())}" android:text="#string/label_female" />
</RadioGroup>
</LinearLayout>
</layout>
My Model Class
public class LoginAndRegistrationRequestModel extends BaseObservable {
private Integer gender;
#Bindable
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
notifyPropertyChanged(BR.gender);
}
}
When i am trying to use
android:checked="#={registration.gender.equals(Gender.FEMALE.getValue())}"
Gradel is throwing an error
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
> android.databinding.tool.util.LoggedErrorException: Found data binding errors.
****/ data binding error ****msg:The expression registrationGender.equals(com.callhealth.customer.comman.enums.Gender.MALE.getValue()) cannot be inverted: There is no inverse for method equals, you must add an #InverseMethod annotation to the method to indicate which method should be used when using it in two-way binding expressions
file:S:\Umesh\android\android_studio_workspace\CallHealth\app\src\main\res\layout\content_user_registration.xml
loc:148:48 - 148:97
****\ data binding error ****
Try it
After some hours I found an easy way: two-way databinding in android. Its a base skeleton with livedata and Kotlin. Also, you can use ObservableField()
Set your viewmodel to data
Create your radiogroup with buttons as you like. Important: set all
radio buttons id !!!
Set in your radio group two-way binding to checked variable (use
viewmodel variable)
Enjoy)
layout.xml
<data>
<variable
name="VM"
type="...YourViewModel" />
</data>
<LinearLayout
android:id="#+id/settings_block_env"
...
>
<RadioGroup
android:id="#+id/env_radioGroup"
android:checkedButton="#={VM.radio_checked}">
<RadioButton
android:id="#+id/your_id1"/>
<RadioButton
android:id="#+id/your_id2" />
<RadioButton
android:id="#+id/your_id3"/>
<RadioButton
android:id="#+id/your_id4"/>
</RadioGroup>
</LinearLayout>
class YourViewModel(): ViewModel {
var radio_checked = MutableLiveData<Int>()
init{
radio_checked.postValue(R.id.your_id1)//def value
}
//other code
}
You are binding it to a boolean EXPRESSION. It has no idea what method to call when setting it. I would try making your property a boolean (e.g. isFemale). It sounds like there is also a way to indicate the setter with the #InverseMethod annotation, but I haven't used that and it seems to me that the boolean approach would be more straightforward. You could always implement the boolean properties in terms of the Integer gender field if you wanted to refer to the Integer elsewhere in java code.
You can either create a #InverseMethod for the custom converter (the error says it can't convert equals to gender basically) or just use a boolean observable for the changes and the setter will work out of the box.
for example if you have
val isFemale = ObservableBoolean()
in your ViewModel this will work out of the box
android:checked="#={viewModel.isFemale}"
of course you need to provide ViewModel variable into the layout binding.
To "fix" what you have there you need to create your own inverse method and setter for checked.
#InverseMethod("toGender")
public static int isFemaleGender(Integer gender) {
return gender.equals(Gender.FEMALE.getValue());
}
public static Integer toGender(boolean checked) {
if (checked)
return Gender.FEMALE;
else
return Gender.MALE;
}
Both of these approaches will only work if you have 2 options as you do true false basically. It is either you create an Observable for each option or go with the solution provided by #Serega Maleev

Referencing properties of Observable class in Android Data Binding layout

What is the type of the Observable class property which getter is annotated as #Bindable in the Android Data Binding framework?
For example, let the Observable class be defined as follows:
class Localization() : BaseObservable() {
var translation: (key: String) -> String by Delegates.observable(defaultTranslation) { _, _, _ ->
notifyPropertyChanged(BR.translation)
}
#Bindable get
}
The layout XML will be then something like this:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="translation"
type="WHAT IS THE TYPE OF TRANSLATION?" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{translation.invoke(stringKey)}" />
</FrameLayout>
</layout>
The question is, what to put in the type attribute of variable "translation".
I've tried:
type="kotlin.jvm.functions.Function1<String, String>"
It compiles, but the TextView is not updated when translation property changes.
I can achieve the desired behavior by introducing localization variable in the layout XML and then calling localization.translation.invoke() in the binding expression. I am just not comfortable with this and want to know if I can reference translation directly.
The Localization extends BaseObservable while Function1 is not observable at all. So using the Localization gives you an interface for observing the changes to the properties.
If you bind the translation, it's a simple field that gets set. If you want to update it, you'd have to call setTranslation() again.

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

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);
}
}

Categories

Resources