Following https://developer.android.com/topic/libraries/data-binding/two-way#converters,
I am trying to implement a data converter for two-way data binding in android.
The functionality of the converter:
Given a 10 digit phone number, add country code to the phone number.
XML code:
<data>
<import type="<package_name>.PhoneNumberStringConverter" />
<variable
name="model"
type="<package_name>.MyViewModel" />
</data>
<androidx.appcompat.widget.AppCompatEditText
android:text="#={PhoneNumberStringConverter.addExtension(model.storeDetailsEntity.storePhoneNumber)}"
... // Other irrelevant attributes are not shown
/>
Converter:
object PhoneNumberStringConverter {
#InverseMethod("addExtension")
#JvmStatic
fun removeExtension(view: EditText, oldValue: String, value: String): String {
return value.substring(3)
}
#JvmStatic
fun addExtension(view: EditText, oldValue: String, value: String): String {
return "+91$value"
}
}
When I add the converter in the XML, the build is failing.
Getting MyLayoutBindingImpl not found. Binding class generation issues.
Note:
1. Two-way data binding is working as expected, the issue is only with the converter.
Already referred:
Two-way data binding Converter
Edit:
Thanks to #Hasif Seyd's solution.
Working code:
PhoneNumberStringConverter:
object PhoneNumberStringConverter {
#JvmStatic
fun addExtension(value: String): String {
return "+91$value"
}
#InverseMethod("addExtension")
#JvmStatic
fun removeExtension(value: String): String {
return if (value.length > 3) {
value.substring(3)
} else ""
}
}
XML:
android:text="#={PhoneNumberStringConverter.removeExtension(model.storeDetailsEntity.storePhoneNumber)}"
Changed addExtension to removeExtension.
There are some issues in the code. Since you are using two way binding convertors,
first issue is you are trying to directly call the inverse binding adapter in the xml , but as per wat i see in ur convertor definition , binding adapter is removeExtension, so u have to assign that in the xml directly.
Another possible reason could be because of having parameters view and oldValue , which are not required , if you remove those two parameters from the Binding Functions , your code would compile successfully
Related
I have an Int which contains a decimal number in units of tenths, so for example my int holds 308 to represent the decimal number 30.8. I want to use data binding to display this number in a TextView along with some other text. My TextView has the following text entry:
android:text="#{String.format("my value: %.1f", #{viewModel.myValue}.toFloat()/10.0f)}"
This fails to build with the following error:
[databinding] {"msg":"cannot find method toFloat() in class java.lang.Integer","file":"app/src/main/res/layout/fragment_you.xml","pos":[{"line0":163,"col0":54,"line1":163,"col1":84}]}
The value in question is defined like so:
private val _myValue = MutableLiveData<Int>()
val myValue: LiveData<Int> = _myValue
I thought this meant it was a Kotlin integer and i could call Kotlin functions on it, but apparently it's a java integer and I can't?
Why can't I call toFloat() there? How can I display this Int as a decimal number?
This might help you.
DataBindingConverter.kt
class DataBindingConverters {
companion object {
#JvmStatic
fun convertIntegerToFloat(value: Int?): Float {
return value/10.0f
}
}
}
XML import
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.package.DataBindingConverter" />
</data>
.....
Bind to View
<EditText
...
android:text="#={DataBindingConverter.convertIntegerToFloat(viewModel.myValue)}" />
In xml if you want to use casting you will have to do it the way you do in JAVA
Here to convert it to float
try using ->
android:text="#{String.format("my value: %.1f", Float.valueOf(#{viewModel.myValue})/10.0f)}"
.toFloat is to be done in kotlin classes. In xml still you have to use JAVA
I think, it will work.
android:text="#{String.format("my value: %.1f", #{(Float)(viewModel.myValue)}/10.0f)}"
I am trying to data bind my XML layout directly with the live data provided by ROOM database. But I see that the query is done infinitely and no value is received in the XML.
Here is my code structure.
My ROOM API in the class DataDao
getMyData(): LiveData<Model?>
My XML View
<com.google.android.material.card.MaterialCardView
android:id="#+id/some_card_view"
style="#styles/CardStyle"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="#{viewModel.dataProvider.getMyData()!=null ? View.VISIBLE : View.GONE}"/>
Here I am just trying to display this card if the data provided by room is not null. Display otherwise. But in the logs I see that it's constantly querying the database and I can see the log printed in the dataProvider class's method.
Am I doing anything wrong here? Is there any other way of handling livedata from room directly in XML databinding?
Create an object in your viewModel:
val myData : LiveData<Model?> = dataProvider.getMyData()
next, you can map it to the desired condition field
val canShowSomeCardView = Transformations.map(myData) {
it != null
}
So now you have the bool to operate within your xml.
I would suggest to create a binding adapter for such things:
#JvmStatic
#BindingAdapter("bindVisibleGone")
fun bindVisibleInvisible(view: View, isVisible: Boolean?) {
view.visibility = if (isVisible == true) View.VISIBLE else View.GONE
}
So now in your xml:
app:bindVisibleGone="#{viewModel.canShowSomeCardView}"
I have two variables inside my layout file :
<data>
<variable name="createExpenseViewModel" type="com.lionosur.dailyexpenses.viewModels.MainViewModel"/>
<variable name="createExpenseConverter" type="com.lionosur.dailyexpenses.converters.createExpenseActivityConverter.Companion"/>
</data>
My view model has an method to return the live data :
fun getAllExpenseItems(): LiveData<List<Expense>> {
return expenseRepository.getAllExpenseItems()
}
I need to observe this data and populate an spinner,
class createExpenseActivityConverter {
// contains all the static methods to convert the data for the ui
companion object {
fun getExpenseCategoryListFromSource(list:List<Source>):ArrayList<String> {
val categoryItems = ArrayList<String>()
categoryItems.addAll(list.map { it.sourceName })
return categoryItems
}
}
}
to populate a spinner I need to supply an array list of string
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="#+id/expense_category"
android:entries="#{()-> createExpenseViewModel.getAllSourceItems(1) }"
app:layout_constraintStart_toStartOf="#+id/textView"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="#+id/textView" app:layout_constraintWidth_percent="0.7"
/>
in android:entries I need to convert the observed data to array list of string, how do I pass the #{()-> createExpenseViewModel.getAllSourceItems(1) } result in to another static method createExpenseViewConverter.getExpenseCategoryListFromSource(sourceList) which would return a array list of string.
in my activity i have setup binding like this
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_expense)
val mainViewModel = DaggerExpenseComponent.builder()
.setContext(this)
.build()
.getExpenseViewModel()
binding.setLifecycleOwner(this)
binding.createExpenseViewModel = mainViewModel
You'll need to use below syntax for that :
android:entries="#{createExpenseConverter.getExpenseCategoryListFromSource(createExpenseViewModel.getAllSourceItems(1))}"
Here, what we've done is accessed your input from MainViewModel object createExpenseViewModel using getAllSourceItems() method;
And then passing it to another class createExpenseActivityConverter object createExpenseConverter using method getExpenseCategoryListFromSource() which returns you ArrayList<String> that your spinner requires.
Edit:
When you use LiveData in DataBinding, Data-binding Compiler takes care of refreshing data just like ObservableFields. All you need to do is provide your LifeCycleOwner to your databinding object.
For Example:
If your activity has ViewDataBinding let's say mActivityBinding using which you provide your ViewModel to set LiveData in xml binding, then after setting your ViewModel consider setting LifecycleOwner like below code :
//Some Activity having data-binding
... onCreate() method of activity
mActivityBinding.setViewModel(myViewModel);
mAcivityBinding.setLifecycleOwner(this); // Providing this line will help you observe LiveData changes from ViewModel in data-binding.
...
Refer here
I want to use databinding with a viewmodel as explained here
So here are excerpts:
layout:
<data class="FragmentEditPersonDataBinding">
<import type="com.unludo.interview.persons.edit.Converter"/>
<variable
name="viewmodel"
type="com.unludo.interview.persons.edit.PersonEditViewModel" />
[...]
<EditText
android:id="#+id/editBirthday"
android:inputType="date"
android:text="#={Converter.dateToString(viewmodel.birthday)}"
converter:
object Converter {
#InverseMethod("stringToDate")
#JvmStatic
fun dateToString(
view: EditText, oldValue: String,
value: Date
): String {
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.FRANCE)
return sdf.format(value)
}
#JvmStatic
fun stringToDate(
view: EditText, oldValue: String,
value: String
): Date {
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.FRANCE)
return sdf.parse(value)
}
}
viewmodel:
class PersonEditViewModel {
var birthday: Date = GregorianCalendar(1993, 5, 19).time
...
Now I get this error when I build:
e: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException:
Found data binding errors.
****/ data binding error ****msg:cannot find method dateToString(java.util.Date)
in class com.unludo.interview.persons.edit.Converter
[...]
- 134:78 ****\ data binding error ****
I am using the latest databinding alpha, so I am wondering if there could be a bug inthe lib.
thx for any help!
--- update
If I write the converter like this then it compiles, but that does not correspond to the documentation. Any idea why?
object Converter {
#InverseMethod("stringToDate")
#JvmStatic
fun dateToString(
value: Date
): String {
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.FRANCE)
return sdf.format(value)
}
#JvmStatic
fun stringToDate(
value: String
): Date {
val sdf = SimpleDateFormat("dd/MM/yyyy", Locale.FRANCE)
return sdf.parse(value)
}
}
Bit of an old thread, but I've also been struggling with 2-way databinding, so for anyone that needs the answer to this, the issue is that Unlundo made their converter the way is documented, where there's a View, and old, and a new value. The documentation for this is not very clear, however.
The arguments in you type converters must also be present in your layout file. For the original binding in the layout, android:text="#={Converter.dateToString(viewmodel.birthday)}", there is only one argument - viewmodel.birthday, which we assume is a date. Therefore, our type converter and inverse converter only get 1 argument.
If you reuse the same converter for multiple bindings and want to be able to see what view the user changed, you can pass the view as an argument by using it's ID in the layout. This will pass in the birthday, and the view that the user was editing:
<EditText
android:id="#+id/editBirthday"
android:inputType="date"
android:text="#={Converter.dateToString(edtBirthday, viewmodel.birthday)}"
This will also mean your type converter and inverse converter both need an additional argument in the beginning for the EditText. The library does seem to be smart enough to get the type of view correct, and not just give you a View as your argument, at least.
Also, if you're struggling with the converter only firing in the direction to string, make sure you actually set the binding variable. If the variable the layout is binding from is null, it will convert default values to display, but it won't be able to bind anything back
I'm a newbie for android data binding.
I want to bind multiple SeekBars to a collection of float, such as
SeekBar1.progress <---> modelArray[0]
SeekBar2.progress <---> modelArray[1]
...
Since the progress of SeekBar is a Int type, I think it would be better to use Converter, and below is the converter code:
import android.databinding.InverseMethod
import android.util.Log
import kotlin.math.roundToInt
class Converter {
#InverseMethod("progressToFloat")
fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
fun progressToFloat(value: Int): Float {
return value.toFloat()/100.0f
}
}
and the model structure looks like:
class Model {
val params by lazy {
ObservableArrayMap<Int, Float>()
}
//....
}
and my xml is following:
<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="MyBinding">
<variable name="Converter" type="com.mypackage.model.Converter"></variable>
<variable
name="Converter"
type="com.mypackage.model.Converter"></variable>
<variable
name="model"
type="com.mypackage.model.Model"></variable>
</data>
...
<SeekBar
android:id="#+id/seekBar"
android:layout_width="100dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:progress="#={Converter.floatToProgress(model.params[0])}"/>
The problem is, every time I build it, it shows :
Found data binding errors.
****/ data binding error ****
msg:The expression converter.floatToProgress(modelParams0) cannot be inverted:
There is no inverse for method floatToProgress, you must add an #InverseMethod
annotation to the method to indicate which method should be used when using
it in two-way binding expressions
I already refer to lots of website, includes following:
https://medium.com/androiddevelopers/android-data-binding-inverse-functions-95aab4b11873
https://developer.android.com/topic/libraries/data-binding/two-way
But I still cannot find out what problem is. Could anyone give me some suggestion?
My development environment
macOS High Sierra,
Android Studio 3.2.1, with compileSdkVersion 28 and gradle 3.2.1
Update:
I also try to write Converter as following :
object Converter { // change to object
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int { // Add static
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float { // Add static
return value.toFloat()/100.0f
}
}
It still does not work.
Solution
Ok, I found the problem: My project does not use Kapt.
After add it to build.gradle, all works fine.
apply plugin: 'kotlin-kapt'
Also, I update my converter body as following:
object Converter {
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float {
val result = value.toFloat()/100.0f
Log.i("MODEL", "convert $value to $result(Float)")
return result
}
}
If the #JVMStatic removed, the project can compile successfully, but the binding does not work.
Thanks everyone who give my suggestion. I think I just ask a silly question.
Refer to:Databinding annotation processor kapt warning
Solution
Ok, I found the problem: My project does not use Kapt. After add it to build.gradle, all works fine.
apply plugin: 'kotlin-kapt'
Also, I update my converter body as following:
object Converter {
#InverseMethod("progressToFloat")
#JvmStatic fun floatToProgress(value: Float): Int {
val result = (value * 100.0f).roundToInt()
Log.i("MODEL", "convert $value to $result(Int)")
return result
}
#JvmStatic fun progressToFloat(value: Int): Float {
val result = value.toFloat()/100.0f
Log.i("MODEL", "convert $value to $result(Float)")
return result
}
}
If the #JVMStatic removed, the project can compile successfully, but the binding does not work.
Thanks everyone who give my suggestion. I think I just ask a silly question.
Refer to:Databinding annotation processor kapt warning
In my case the problem was that I was trying to make a custom TwoWay DataBinding Adapter
but my field in the ViewModel class was LiveData instead of MutableLiveData