Two way databinding with custom converter - android

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

Related

Android two way binding with Integer type(Livedata)

I'm having some issue that two way binding with an Integer data type.
ViewModel
var saleType: MutableLiveData<Int> = MutableLiveData(1)
var saleDetail: MutableLiveData<Int> = MutableLiveData(0)
var salePrice: MutableLiveData<Int> = MutableLiveData(0)
var saleRate: MutableLiveData<Int> = MutableLiveData(0)
var printAmount: MutableLiveData<Int> = MutableLiveData(1)
layout.xml
<data>
<import type="com.package.utils.DataBindingConverters" />
<variable
name="viewModel"
type="com.package.ViewModel" />
</data>
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/sale_detail_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:gravity="right"
android:selectAllOnFocus="true"
android:inputType="number"
android:imeOptions="actionDone"
android:singleLine="true"
android:maxLines="1"
app:inputContent="#={DataBindingConverters.convertIntegerToString(viewModel.saleDetail)}"
/>
DataBindingConverters
class DataBindingConverters {
companion object {
#InverseMethod("convertStringToInteger")
#JvmStatic
fun convertStringToInteger(value: String): Int? {
if (TextUtils.isEmpty(value) || !TextUtils.isDigitsOnly(value)) {
return null
}
return value.toIntOrNull()
}
#InverseMethod("convertIntegerToString")
#JvmStatic
fun convertIntegerToString(value: Int?): String {
return value?.toString() ?: ""
}
}
}
It occurs error like this
cannot find method convertIntegerToString(java.lang.String) in class com.package.utils.DataBindingConverters
I thought it would be good to process the value for IntegerToString in two-way binding, but I get an error that can't find a method.
In two-way binding, is there any other way to handle Integer?
I don't know what app:inputContent does, but using android:text on the TextInputEditText should be enough:
<com.google.android.material.textfield.TextInputEditText
...
android:text="#={ viewModel.salePriceString }"/>
Now you wonder what is salePriceString. It should be your 2-way binding liveData, and because android:text requires String, salePriceString typing would be LiveData<String>. Modify your viewModel to:
val salePriceString = MutableLiveData<String?>(0.toString())
Now every time you edit the text (by user input) in the TextInputEditText, your salePricesString will get updated with the new text. And anytime you update your salePriceString through code, the text of your TextInputEditText will update accordingly. For the latter case, if you want to update your salePriceString with a value of 500 for example, you would do salePriceString.postValue(500.toString()). And if you want to get the Int value from salePriceString in your code, just do salePriceString.value?.toIntOrNull().
The next steps are optional, but if you want a convenient way to get the Int value without using salePriceString.value?.toIntOrNull() every time, modify your viewModel a bit further:
import androidx.lifecycle.Transformations
...
val salePriceString = MutableLiveData<String?>(0.toString())
// add conversion to LiveData<Int>
val salePrice = Transformations.map(salePriceString) { priceString ->
priceString?.toIntOrNull()
}
salePrice now contains the Int value of salePriceString, and is automatically updated every time salePriceString changes. salePrice is a LiveData, so you can observe it the same way you would observe salePriceString, but instead of salePriceString, you have the Int value ready in salePrice.
A final step is to observe salePrice somewhere in the code, most logically on the fragment that your viewModel is attached to:
viewModel.salePrice.observe(this, {}) // this, or viewLifecycleOwner, depending on your use case
You need to observe it, even if you then do nothing with the value. It is because Transformation.map is a bit strange one, the LiveData created by it won't go "live" unless there exists an observer for it. That means that until salePrice is observed by some observer, you won't get any value from it, i.e. salePrice.value would always be null.

Android data binding TextView value of String with fall back to #StringRes

context: data binding with a ViewModel, which gets data from a remote source in the form of JSON. I want to display a textual value from that JSON in a TextView, but if the data is absent in the JSON, I want to fall back to a string defined in strings.xml.
android:text="#{viewModel.theText}"
How I currently solved it is with a custom binding adapter that accepts an Any, and checks if the value is an Int or String:
app:anyText="#{viewModel.theText}". The viewModel has something like val theText = json.nullableString ?: R.string.placeholder.
I'm guessing that this is a problem more people deal with, and I was hoping if someone knows a more elegant solution.
You could provide Application context to your ViewModel or Resources and then do something like this:
val theText = json.nullableString ?: resources.getString(R.string.placeholder)
The other option would be keep using binding adapter like you do but I would wrap text input in another object like this:
data class TextWrapper(
val text: String?,
#StringRes val default: Int
)
#BindingAdapter("anyText")
fun TextView.setAnyText(textWrapper: TextWrapper) {
text = textWrapper.text ?: context.getString(textWrapper.default)
}
val theText = TextWrapper(text = json.nullableString, default = R.string.placeholder)
You do not need an adapter to handle this use Null coalescing operator operator ?? in xml.
Try below code:
android:text="#{viewModel.theText?? #string/your_default_text}"
Use case :
The null coalescing operator (??) chooses the left operand if it isn't null or the right if the former is null.
P.S: lean more about DB and expressions here-> https://developer.android.com/topic/libraries/data-binding/expressions

Android Two-way data binding data converter not working

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

Android databinding and LiveData: Can't bind to value in LiveData property

I'm trying out databinding for a view that's supposed to display data exposed through a LiveData property in a viewmodel, but I've found no way to bind the object inside the LiveData to the view. From the XML I only have access to the value property of the LiveData instance, but not the object inside it. Am I missing something or isn't that possible?
My ViewModel:
class TaskViewModel #Inject
internal constructor(private val taskInteractor: taskInteractor)
: ViewModel(), TaskContract.ViewModel {
override val selected = MutableLiveData<Task>()
val task: LiveData<Task> = Transformations.switchMap(
selected
) { item ->
taskInteractor
.getTaskLiveData(item.task.UID)
}
... left out for breivety ...
}
I'm trying to bind the values of the task object inside my view, but when trying to set the values of my task inside my view I can only do android:text="#={viewmodel.task.value}". I have no access to the fields of my task object. What's the trick to extract the values of your object inside a LiveData object?
My task class:
#Entity(tableName = "tasks")
data class Task(val id: String,
val title: String,
val description: String?,
created: Date,
updated: Date,
assigned: String?)
For LiveData to work with Android Data Binding, you have to set the LifecycleOwner for the binding
binding.setLifecycleOwner(this)
and use the LiveData as if it was an ObservableField
android:text="#{viewmodel.task}"
For this to work, Task needs to implement CharSequence. Using viewmodel.task.toString() might work as well. To implement a two-way-binding, you'd have to use MutableLiveData instead.
why are you using two way binding for TextView
android:text="#={viewmodel.task.value}"
instead use like this android:text="#{viewmodel.task.title}"

Kotlin: Java Util Date to String for Databindings

I want to use the Date value of my Data class in view via Databinding.
If I use the toString() method on the Date field it works. But I want to customize the Date value.
So I created the Utils object with Method. This is the Util object
object DateUtils {
fun toSimpleString(date: Date) : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(date)
}
}
But if I want to use this method in the xml like this
<data>
<import type="de.mjkd.journeylogger.Utils.DateUtils"/>
<variable
name="journey"
type="de.mjkd.journeylogger.data.Journey"/>
</data>
...
android:text="#{DateUtils.toSimpleString(journey.date)}"
I get an error cannot find method toSimpleString(java.util.Date) in class ...
This is my Dataclass:
data class Journey(var title: String, var date: Date?, var destination: String)
Whats wrong with this code?
Using the reserved word object in kotlin, that you really doing is declare a single instance. the equivalent in java is something more or less like:
class DataUtils {
static DataUtils INSTANCE;
public String toSimpleString()...
}
then when you call it you do a DateUtils.INSTANCE.toSimpleString()
You should capable to use DateUtils.INSTANCE.toSimpleString() in your xml
In order to make toSimpleString accessible from static context, you have to flag the method with#JvmStatic
object DateUtils {
#JvmStatic
fun toSimpleString(date: Date) : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(date)
}
}
Using extension function(doc)
#file:JvmName("DateUtils")//Use this to change your class name in java, by default is <the file name>Kt (DateUtilsKt in your case)
fun Date.toSimpleString() : String {
val format = SimpleDateFormat("dd/MM/yyy")
return format.format(this)
}
Then you can use it directly in xml as you are already doing:
android:text="#{DateUtils.toSimpleString(journey.date)}"
Why don't you just use a top-level function which is static by default? A top-level function is not defined in any class.
fun main(args: Array<String>){
println(toSimpleString(Date()))
}
fun toSimpleString(date: Date?) = with(date ?: Date()) {
SimpleDateFormat("dd/MM/yyy").format(this)
}
Also, notice how Jouney's date is nullable in your example and your toSimpleString only accepts a non-nullable Date!
I changed it, so that it will return the string of the current date in case null is passed.
More easy way would be to make a getDateString in model class.
android:text="#{journey.dateString)}"
class Journey {
lateinit var date: Date
fun getDateString(){
return DataUtils.toSimpleString(date)
}
}
I like this way because I don't need to import any class in this case.

Categories

Resources