Android two way data binding for custom component?

1.7k views Asked by At

I'm trying to follow this blog post to try and get two way data binding to work for a custom component (A constraint view with an EditText in it).

I'm able to get two standard EditText components to be in sync (both ways) with my model, but I'm having trouble getting the changes in my custom component to flow into my model (although one way data binding works).

My model:

public class Model extends BaseObservable {
    private String value;

    @Bindable
    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
        notifyPropertyChanged(company.com.databinding.BR.value);
    }

    public Model() {
        value = "Value";
    }
}

Activity:

@InverseBindingMethods({
        @InverseBindingMethod(
                type = CustomComponent.class,
                attribute = "value",
                method = "getValue")
})
public class MainActivity extends AppCompatActivity {
    @BindingAdapter("value")
    public static void setColor(CustomComponent view, String value) {
        if (!value.equals(view.getValue())) {
            view.setValue(value);
        }
    }

    @BindingAdapter(
            value = {"onValueChange", "valueAttrChanged"},
            requireAll = false
    )
    public static void setListeners(CustomComponent view,
                                    final ValueChangeListener onValueChangeListener,
                                    final InverseBindingListener inverseBindingListener) {
        ValueChangeListener newListener;
        if (inverseBindingListener == null) {
            newListener = onValueChangeListener;
        } else {
            newListener = new ValueChangeListener() {
                @Override
                public void onValueChange(CustomComponent view,
                                          String value) {
                    if (onValueChangeListener != null) {
                        onValueChangeListener.onValueChange(view,
                                value);
                    }
                    inverseBindingListener.onChange();
                }
            };
        }

        ValueChangeListener oldListener =
                ListenerUtil.trackListener(view, newListener,
                        R.id.textWatcher);

        if (oldListener != null) {
            view.removeListener(oldListener);
        }
        if (newListener != null) {
            view.addListener(newListener);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setModel(new Model());
    }
}

Custom component:

public class CustomComponent extends ConstraintLayout {
    private String value;
    private EditText txt;
    private TextWatcher textWatcher;
    ValueChangeListener listener;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
        if (txt != null) {
            txt.setText(value);
        }
    }

    public CustomComponent(Context context) {
        super(context);
        init(context);
    }

    public CustomComponent(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public CustomComponent(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(Context context) {

    }

    private void init(Context context, AttributeSet attrs) {
        View.inflate(context, R.layout.custom_component, this);
        txt = findViewById(R.id.txt_box);
        final CustomComponent self = this;
        textWatcher = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void afterTextChanged(Editable editable) {
                if (listener != null) {
                    listener.onValueChange(self, editable.toString());
                }
            }
        };
        txt.addTextChangedListener(textWatcher);
    }

    public void addListener(ValueChangeListener listener) {
        this.listener = listener;
    }

    public void removeListener(ValueChangeListener listener) {
        this.listener = null;
    }
}

public interface ValueChangeListener {
    public void onValueChange(CustomComponent view, String value);
}

I think the section "Hooking The Event" in that post has gone completely over my head; I really only needed a simple setter and getter for the component, and so couldn't quite understand what was being done in that BindingAdapter. Of all of them I think it's this line that I don't get at all:

ValueChangeListener oldListener =
            ListenerUtil.trackListener(view, newListener,
                    R.id.textWatcher);

Demo at: https://github.com/indgov/data_binding

1

There are 1 answers

0
George Mount On BEST ANSWER

Sorry that the ListenerUtil was confusing. That's only useful when your component supports multiple listeners. In that case, you can't just set a new listener, you must remove the old one and add the new one. ListenerUtil helps you track the old listener so it can be removed. In your case, it can be simplified:

@BindingAdapter(
        value = {"onValueChange", "valueAttrChanged"},
        requireAll = false
)
public static void setListeners(CustomComponent view,
                                final ValueChangeListener onValueChangeListener,
                                final InverseBindingListener inverseBindingListener) {
    ValueChangeListener newListener;
    if (inverseBindingListener == null) {
        newListener = onValueChangeListener;
    } else {
        newListener = new ValueChangeListener() {
            @Override
            public void onValueChange(CustomComponent view,
                                      String value) {
                if (onValueChangeListener != null) {
                    onValueChangeListener.onValueChange(view,
                            value);
                }
                inverseBindingListener.onChange();
            }
        };
    }

    view.setListener(newListener);
}

and then replace addListener() with setListener() and you don't need the removeListener() because you can always set the listener to null.

The problem you're seeing is in your component:

public String getValue() {
    return value;
}

You're returning the value that was last set by the setter and not the value that is in the EditText. To solve this:

public String getValue() {
    return txt.getText().toString();
}