Android Data Binding with parameter passing to method - android

First, this question is not a case of 'onClick' event parameter passing.
I have DateUtil class with one method as below :
public static String formatDate(long date) {
SimpleDateFormat dateFormat;
dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
Calendar c = Calendar.getInstance();
dateFormat.setTimeZone(TimeZone.getDefault());
c.setTimeInMillis(date);
return dateFormat.format(c.getTimeInMillis());
}
My model CommentEntity has following attributes :
private int id;
private int productId;
private String text;
private Date postedAt;
Now in one of layout I'm displaying the Comments.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="comment"
type="com.example.entity.CommentEntity"/>
<variable
name="dateUtil"
type="com.example.util.DateUtil"/>
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
//This line gives error for data binding
android:text="#{dateUtil.formatDate(comment.postedAt.time)}"/>
</layout>
The error I'm getting is :
cannot find method
formatDate(com.example.util.DateUtil) in class
long
Now for the same if I modify formatDate() method as it will take current time by default, hence removing the parameter passing in data binding, it will work perfectly.
So am I doing something wrong or is it a bug?
Please provide solution for problem to pass the parameter to method in data binding.

Try below approach:
Don't take your DateUtil class object direct from data binding in xml
Make one BindingAdapter method in your CommentEntity model class like this:
#BindingAdapter("android:text")
public static void setPaddingLeft(TextView view, long date) {
view.setText(DateUtil.formatDate(long));
}
Then use like below for xml:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
android:text="#{comment.postedAt.time}"/>
Explanation :
When you wanted to apply some custom logic for your view based on data model, you need to use BindingAdapter to do that job. So, you can provide some custom tag or use any default android: tag on which you need to set logic.
I denied you using DateUtil for binding adapter because, you might be using it's methods to somewhere else too. So suggested to make new method in your model instead for that logic, so that core logic remains untouched. (You can use your DateUtils for this logic though, you just need to make it as BindingAdapter).

since you want to use a static method in DateUtil you should import it:
<data>
<variable ... />
<import type="foo.bar.DateUtil"/>
</data>
and in TextView:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_alignParentRight="true"
android:layout_below="#id/item_comment_text"
//use DateUtil directly-
android:text="#{DateUtil.formatDate(comment.postedAt.time)}"/>
your error was trying to use it as a variable - this tells databinding to expect this class instance to be bound somewhere in your UI class (Fargment/Activity)

Related

How to bind to an Android Spinner using a ViewModel

I am trying to find out how to bind both the list items, and the selected value/index of an Android Spinner (I am pretty new to Android / Kotlin)
I have the following
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.app.Modes" />
<variable
name="viewModel"
type="com.example.app.MainActivityViewModel" />
</data>
....
<Spinner
android:layout_row="17"
android:layout_column="2"
android:id="#+id/spinner1"
android:layout_width="1200px"
android:entries="#{viewModel.devicesDescriptions}"
app:selectedValue="#={viewModel.devicePosition}"
android:layout_height="wrap_content"
android:background="#android:drawable/btn_dropdown"
android:spinnerMode="dropdown"/>
and in the View Model
val devicesDescriptions = ObservableArrayList<String>()
var devices = listOf<MidiDeviceInfo>()
fun setFoundDevices(d: MutableList<MidiDeviceInfo>) {
devices = d
for (dev in devices)
devicesDescriptions.add(dev.toString())
}
By trial and error I could set just strings to the Spinner items (the MidiDeviceInfo would have been better, but string will do)
However, I cannot get a binding to get the selectedItem to work.
I have tried many things, but with the above, I have the error
Found data binding error(s):
[databinding] {"msg":"Cannot find a getter for \u003candroid.widget.Spinner app:selectedValue\u003e that accepts parameter type \u0027java.lang.String\u0027\n\nIf a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.","file":"app\\src\\main\\res\\layout\\activity_main.xml","pos":[{"line0":334,"col0":4,"line1":343,"col1":39}]}
Anyone know a way to do this?
Try using android:selectedItemPosition="#={viewModel.devicePosition}" instead of app:selectedValue="#={viewModel.devicePosition}".

How to properly use two way binding in Kotlin-Multiplatform?

I'm trying to use a String variable to bind it into my view.
When I use a model object with a String property, it works well. But if I use the String variable alone, it only works with one way binding.
ViewModel:
class SampleModel(var data : String = "")
var myModel : SampleModel = SampleModel()
var myVariable : String = ""
XML:
<data>
<variable
name="model"
type="MyViewModel.SampleModel" />
<variable
name="variable"
type="String" />
</data>
<!-- Two way works fine -->
<EditText
android:text="#={model.data}"/>
<!-- Only one way works -->
<EditText
android:text="#={variable}"/>
The string in the SampleModel works well with two way binding but the String variable does not.
I think it is because the imported String in xml is java.lang.String but the String in the model is kotlin.String. And I'm unable to use the kotlin.String in xml.
Is there any solution to fix this? Or is there any proper way of two way binding in Kotlin-Multiplatform projects?
It looks like you have added a wrong variable in the xml file. In your view model you have created a variable named myVariable of type String but in your xml file you are creating one more variable here :-
<variable
name="variable"
type="String" />
so these both variables are different. You don't need to import anything in your xml file just create a viewModel variable which you have already done here :-
<variable
name="model"
type="MyViewModel.SampleModel" />
and now simply use this like :- android:text="#={model. myVariable}"
UPDATE :- Here in this you need to use the String variable which i created in your viewModel because it used kotlin.String and in xml you have java.lang.String. You can simply use the variable which is created in your viewModel For eg :- android:text="#={viewModel.yourVariable}"

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

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.

Categories

Resources