android – Two-way data-binding with double value in EditText-ThrowExceptions

Exception or error:

In Android we can use 2-way data-binding with @= in front of variable. But, that variable is a double. So for displaying it in EditText, I need to convert it to String using String.valueOf(pojo.value).

If I attach = in front for two-way data binding it simply just not compile.

If I attach a onTextChanged and set the value there, I looses the cursor. Is there any workaround?

Edit:

It worked with InverseBindingAdapter but doesn’t allow . (period) to be typed.

How to solve:

There is no simple solution to this. But, I went ahead and created a kind of changes which almost work like a 2-way binding.

My EditText was looking like this:

        <EditText
                android:id="@+id/amount"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1.1"
                android:digits="0123456789."
                android:gravity="end"
                android:inputType="numberDecimal|numberSigned"
                android:onTextChanged="@{() -> handler.valueAmountChanged(amount)}"
                android:selectAllOnFocus="true"
                android:text="0"
                android:textColor="@color/selector_disabled_edit_text"
                app:userVal="@{userAmount}" />

handler is the instance of the activity. And it contains the valueAmountChanged(EditText editText) method.

Now in your value amount checked, I am parsing that text string and storing it in the respective variable.

For me, it is looking something like this:

 public void valueAmountChanged(EditText editText) {

            double d = 0.0;
            try {
                String currentString = editText.getText().toString();

                // Remove the 2nd dot if present
                if (currentString.indexOf(".", currentString.indexOf(".") + 1) > 0)
                    editText.getText().delete(editText.getSelectionStart() - 1, editText.getSelectionEnd());

                // Remove extra character after 2 decimal places
                currentString = editText.getText().toString(); // get updated string

                if (currentString.matches(".*\\.[0-9]{3}")) {

                    editText.getText().delete(currentString.indexOf(".") + 3, editText.length());
                }
                d = Double.valueOf(editText.getText().toString());
            } catch (NumberFormatException e) {
            }
            userAmount = d;  // this variable is set for binding
    } 

Now, as we change the userAmount variable it will going to reflect as we have set the binding adapter with app:userVal argument in the EditText.

So, with binding adapter we check if the new value is not the current value, then update the value. Else, leave it as it is. We need to do this, because if user is typing and binding adapter updates the value, then it will loose is cursor position and will bring it to the front. So, this will save us from that.

@BindingAdapter({"userVal"})
    public static void setVal(EditText editText, double newVal) {

        String currentValue = editText.getText().toString();

        try {

            if (Double.valueOf(currentValue) != newVal) {
                DecimalFormat decimalFormat = new DecimalFormat("#.##");
                String val = decimalFormat.format(newVal);
                editText.setText(val);
            }
        } catch (NumberFormatException exception) {
            // Do nothing
        }
    }

This is a bit typical approach, I know. But, couldn’t find any better than this. There is also very less documentation available and others are in the form of blog posts on medium, which should have been added to the official documentation.

Hope it helps someone.

###

Your solution worked partially for me.
I used the @BindingAdaptor and the ‘Amount’ variable value was getting reflected in edit text, but the OnTextChangeListner did not get triggered on value change. So I used TextWatcher in VM. The code for both VM and XML is here. Nothing to be written in Activity.

//VIEW MODEL

@SerializedName("Amount")
private Double Amount ;

@Bindable
public Double getAmount() {
    return (Amount == null)? 0.0 : Amount;
}
public void setAmount(Double amount) {
    Amount = amount;
    notifyPropertyChanged(BR.amount);
}

 public TextWatcher getAmountWatcher() {
    return new SimpleTextWatcher() {
        @Override
        public void onTextChanged(String text) {
            Amount =(text!=null&&text!="")?Double.valueOf(text):0.0;
        }
    };
}

@BindingAdapter({"userVal"})
public static void setVal(EditText editText, double newVal) {

    String currentValue = editText.getText().toString();

    try {

        if (Double.valueOf(currentValue) != newVal) {
            DecimalFormat decimalFormat = new DecimalFormat("#.##");
            String val = decimalFormat.format(newVal);
            editText.setText(val);
        }
    } catch (NumberFormatException exception) {
       exception.printStackTrace();
    }
}

//XML CODE

<com.mycompany.myproj.Widgets.MyEditText
 android:id="@+id/expense_cost"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:inputType="numberDecimal"
 android:maxLength="6"
 android:selectAllOnFocus="true"
 app:addTextChangedListener="@{expense.amountWatcher}"
 app:userVal="@{expense.Amount}" />

Cheers

###

Here’s how I did it.

    //-------------------------------------------------------------------------------------------------//

    @BindingAdapter("app:text")
    fun setDoubleInTextView(tv: TextView, dbl: Double?) {

        try {
            //This will occur when view is first created
            if (tv.text.toString() == "" && dbl == null)
                return

            //Check to see what's already there
            val tvDbl = tv.text.toString().toDouble()
            //If it's the same as what we've just entered then return
            // This is when then the double was typed rather than binded
            if (tvDbl == dbl)
                return

            //If it's a new number then set it in the tv
            tv.text = dbl?.toString()

        } catch (nfe: java.lang.NumberFormatException) {

            //This is usually caused when tv.text is blank and we've entered the first digit
            tv.text = dbl.toString()

        }//catch

    }//setDoubleInTextView

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -//

    @InverseBindingAdapter(attribute = "app:text")
    fun getDoubleFromTextView(editText: TextView): Double? {

        return try {
            editText.text.toString().toDouble()
        } catch (e: NumberFormatException) {
            null
        }//catch

    }//getDoubleFromTextView

    //-------------------------------------------------------------------------------------------------//

    @BindingAdapter("textAttrChanged")
    fun setTextChangeListener(editText: TextView, listener: InverseBindingListener) {
        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) = listener.onChange()

            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                Log.d(TAG, "beforeTextChanged $p0")
            }

            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                Log.d(TAG, "onTextChanged $p0")
            }

        })
    }

    //-------------------------------------------------------------------------------------------------//


  <EditText
    style="@style/TextAppearance.MaterialComponents.Body1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="@string/price"
    android:inputType="numberDecimal"
    app:text="@={viewModel.tempProductPrice}"/>

The EditText has the following setting to force the correct input

    android:inputType="numberDecimal"

Leave a Reply

Your email address will not be published. Required fields are marked *