Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly 2-way-bind numeric to Android editText

I must be missing something here. Every example I've seen with Android's 2-way binding is based on a String in the backing data for anything user-enterable, like an EditText.

Handling anything not a String seems somewhat... inelegant. For example, if I have a double in my domain model that needs to be editable, the best binding I've come up with requires a ViewModel with surprisingly a lot of code to interface between the model and the EditText.

Am I missing something key? Should I really need 30 lines of code to interface an EditText with a double? For the sake of discussion, let's consider a currency field, represented as a double, in a two-way bound EditText:

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="@={fragModel.startBucks}"
android:id="@+id/editText2"/>

And here's the ViewModel I've had to construct in order to give the EditText a string to bind to.

@Bindable private String startBucksString;
private double localBucks;
public String getStartBucksString() {
    double domainBucks = cd.getStartBucks();
    // Ignore incoming change of less than rounding error
    if( Math.abs(localBucks - domainBucks) < .001  ) return startBucksString;
    startBucksString = "";
    if( domainBucks != 0)
        startBucksString = String.format("$%.2f", domainBucks);
    return startBucksString;
}

public void setStartBucksString(String inBuckstr) {
    double calcBucks=0;
    inBuckstr = inBuckstr.replaceAll( "[^\\d.]", "" );
    try {
         calcBucks = Double.parseDouble(inBuckstr);
    } catch( NumberFormatException e) {
        return;
    }
    // Neglect outgoing change of less than rounding error
    if( Math.abs(localBucks - calcBucks) < .001  ) return;

    startBucksString = String.format("$%.2f", calcBucks);
    localBucks = calcBucks;
    cd.setStartBucks(calcBucks);
    notifyPropertyChanged(BR.startBucksString);
}

Here, I wrote a simple, compilable example of 2-way binding with a ViewModel. It illustrates the difficulty I had in continuously updating a float in the domain model -- in the end, I decided there's no way to do it without writing a custom TextWatcher for each domain field.

like image 788
Autumn Avatar asked May 26 '16 03:05

Autumn


1 Answers

My approach is to delay notifyPropertyChanged method calling using Handles. In this way while the user is typing, the code don't run, then 2,5 seconds after the user has stopped to type last character, the notificationPropertyChanged will be called.

The visual effect is cool, and the user is free to write numbers as he wants.

See these two examples:

Use can use this compact(?) code for each field:

//
// g1FuelCostYear field
//
private double g1FuelCostYear;

@Bindable
public String getG1FuelCostYear() {
  return Double.valueOf(g1FuelCostYear).toString();
}

private Handler hG1FuelCostYearDelay = null;

public void setG1FuelCostYear(String g1FuelCostYear) {

  // Delayed notification hadler creation
  if (hG1FuelCostYearDelay == null) {
    hG1FuelCostYearDelay = new Handler() {
      @Override
      public void handleMessage(Message msg) {
        notifyPropertyChanged(it.techgest.airetcc2.BR.g1FuelCostYear);
      }
    };
  } else {
    // For each call remove pending notifications
    hG1FuelCostYearDelay.removeCallbacksAndMessages(null);
  }

  // Data conversion logic
  try {
    this.g1FuelCostYear = Double.parseDouble(g1FuelCostYear);
  }
  catch (Exception ex) {
    this.g1FuelCostYear = 0.0;
    log(ex);
  }

  // New delayed field notification (other old notifications are removed before)
  hG1FuelCostYearDelay.sendEmptyMessageDelayed(0,2500);
}

This code instead is useful when you use currency converter or percent converter. The user can write a plain double, the code convert to currency string. If the setter is called with the currency string the code is able to convert it as double too.

//
// g1FuelCostYear field
//
private double g1FuelCostYear;

@Bindable
public String getG1FuelCostYear() {
  NumberFormat nf = NumberFormat.getCurrencyInstance();
  return nf.format(this.g1FuelCostYear);
  //return Double.valueOf(g1FuelCostYear).toString();
}

private Handler hG1FuelCostYearDelay = null;

public void setG1FuelCostYear(String g1FuelCostYear) {
  if (hG1FuelCostYearDelay == null)
  {
    hG1FuelCostYearDelay = new Handler() {
      @Override
      public void handleMessage(Message msg) {
          notifyPropertyChanged(it.techgest.airetcc2.BR.g1FuelCostYear);
      }
    };
  } else {
      hG1FuelCostYearDelay.removeCallbacksAndMessages(null);
  }
  boolean success = false;
  try {
    NumberFormat nf = NumberFormat.getCurrencyInstance();
    this.g1FuelCostYear = nf.parse(g1FuelCostYear).doubleValue();
    success = true;
  }
  catch (Exception ex) {
    this.g1FuelCostYear = 0.0;
    log(ex);
  }
  if (!success) {
    try {
        this.g1FuelCostYear = Double.parseDouble(g1FuelCostYear);
        success = true;
    } catch (Exception ex) {
        this.g1FuelCostYear = 0.0;
        log(ex);
    }
  }
  updateG1FuelConsumption();
  hG1FuelCostYearDelay.sendEmptyMessageDelayed(0,2500);
}
like image 52
Andrea Piovesan Avatar answered Oct 31 '22 03:10

Andrea Piovesan