Search code examples
androidkotlindata-bindingandroid-databinding

Android Two-Way Data Binding with Double (Kotlin)


I have a ViewModel class defined as follows:

class StockLoadTaskModel : ViewModel() {
    ....
    ....
 
    var d: Double = 10.0
}

That is bound to the following layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <import type="android.view.View" />
        <import type="it.kfi.lorikeetmobile.extras.Converter" alias="Converter"/
        <variable
            name="viewModel"
            type="it.kfi.lorikeetmobile.stock.models.StockLoadTaskModel" />
        <variable
            name="view"
            type="it.kfi.lorikeetmobile.stock.ui.movements.StockLoadTaskFragment
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        ...
    
        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="8dp">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_code"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_et_item_code"
                android:text="@={viewModel.itemCode}" />
        </com.google.android.material.textfield.TextInputLayout>
    
        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="8dp">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_quantity"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="numberDecimal"
                android:text="@={Converter.doubleToString(d)}"
                android:hint="@string/quantity" />
        </com.google.android.material.textfield.TextInputLayout>
    
        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="4dp"
            android:layout_marginEnd="8dp">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_note"
                android:lines="3"
                android:scrollbars="vertical"
                android:overScrollMode="ifContentScrolls"
                android:gravity="top"
                android:inputType="textMultiLine"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_et_note"
                android:text="@={viewModel.selectedItem.detail.note}"/>
        </com.google.android.material.textfield.TextInputLayout>
        
        ...
    </LinearLayout>

And I have also the following Converter object:

object Converter {
    
    @JvmStatic
    @InverseMethod("stringToDouble")
    fun doubleToString(value: Double?): String? {
    
        if (value == null) {
            return null
        }
        return DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).format(value)
    }
    
    @JvmStatic
    fun stringToDouble(value: String?): Double? {
    
        if (value == null) {
            return null
        }
        val v = DecimalFormat(ClientConfiguration.currentConfig.decimalFormat).parse(value)
        return v.toDouble()
    }
}

If I set: android:text="@={Converter.doubleToString(d)}" (two-way databinding), in the EditText with id et_quantity I get the following error:

...error: cannot find symbol

If I change it into a one-way databinding like: android:text="@{Converter.doubleToString(d)}", it works. It looks like the binding manager is not able to recognize the inverse method.

Can anybody help me? Thank you.


Solution

  • Why the error happens?

    When you define two-way data binding like you have in your example android:text="@={Converter.doubleToString(d)}" the question is: what function/object will receive data that you get back passed from EditText as user types data in? Should data be passed to Converter.doubleToString or maybe some other static function of Converter? Maybe to the result of Converter.doubleToString(d) or to d variable?

    You must be precise.

    You expect it is d, the compiler expects it is the result of Converter.doubleToString(d). Actually, neither will work.

    Another issue is that EditText does operate with characters. It knows nothing about double, int, float, byte, short, boolean or anything else that is not a string.

    It means that in order to implement two-way data binding your source:

    • must return value of type String;
    • must be assignable.

    How to fix the issue?

    Android architecture components introduce us with ObservableField class. There are ready to use ObservableBoolean, ObservableChar, ObservableFloat and a few others. If you open the link from the previous sentence you should see all of the classes Observable... on the left pane.

    There is no ObservableString but ObservableField accepts a generic type. So you can define a variable that is a part of data binding to be ObservableField<String>("defaultValueHere").

    So what you should have is:

    class StockLoadTaskModel : ViewModel() {
        ....
        ....
        var d: Double = 10.0
        var dataBindingVariable = ObservableField<String>(d.toString())
    }
    

    The dataBindingVariable will always return you the contents of an EditText you bound it to. You can get that value and safely convert to double.

    class StockLoadTaskModel : ViewModel() {
        ....
        ....
        var d: Double = 10.0
        var dataBindingVariable = 
            object: ObservableField<String>(d.toString()) {
                override fun set(value: String?) {
                    super.set(value)
                    // a value has been set
                    d = value.toDoubleOrNull() ?: d
                }
            }
    }
    

    Layout declaration will look like that for input field:

            <com.google.android.material.textfield.TextInputLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="4dp"
                android:layout_marginEnd="8dp">
    
                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/et_quantity"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="numberDecimal"
                    android:text="@={viewModel.dataBindingVariable}"
                    android:hint="@string/quantity" />
            </com.google.android.material.textfield.TextInputLayout>
    

    And there will be no need for object Converter.

    There is another way of doing two-way data binding I'm not talking about here because it was already answered. Here it is.