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
Related
I have made a binding adapter available statically inside my Fragment which basically change my button appearance from "Stop" to "Play" and vice-versa.
companion object {
#BindingAdapter("playState")
fun Button.setPlayState(item: UIState) {
item.let {
if (it.isPlaying) {
setText("Stop")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorStop))
} else {
setText("Play")
setBackgroundColor(ContextCompat.getColor(context, R.color.colorPlay))
}
}
}
}
Here is my layout file. I have provided a data class for it.
<?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>
<!-- stuff here -->
<variable
name="viewmodel"
type="com.mypackage.ui.ViewModel"/>
<variable
name="uistate"
type="com.mypackage.ui.UIState" />
</data>
<!-- layout, buttons, and more stuff here. Just pay attention to this following button -->
<Button
android:id="#+id/play_button"
android:layout_width="150sp"
android:layout_height="75sp"
android:layout_marginTop="20sp"
android:onClick="#{() -> viewmodel.onPlayClicked()}"
android:text="#string/play_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/minus_layout"
app:layout_constraintVertical_bias="0.026"
app:playState="#{uistate}"/>
</layout>
UIState itself is pretty self-explanatory.
data class UIState(var isPlaying: Boolean)
and the () -> viewmodel.onPlayClicked() flips the Boolean at UIState.
After compiling, Data Binding Compiler throws this error:
Cannot find a setter for <android.widget.Button app:playState>
that accepts parameter type 'com.mypackage.ui.UIState'
I have tried:
Rebuilding the project by removing .gradle folder
Looking for answer here and here.
Removed #JvmStatic annotation at the extension function
Moved the extension function to top level instead of Fragment's companion object.
I think you missed to add kotlin plugin in your gradle
apply plugin: 'kotlin-kapt'
You don't have to use #JvmStatic because you are using Kotlin extension feature.
You need to add the view reference as a paramater to your BindingAdapter method.
#BindingAdapter("playState")
fun setPlayState(button:Button,item: UIState) {
//do your work here
}
Your namespace
xmlns:app="http://schemas.android.com/apk/res-auto"
is wrong for custom binding adapters. Please use the namespace
xmlns:app="http://schemas.android.com/tools"
since app:playState is not in the namespace you have given its not working properly
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.
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.
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.
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