EditTextに対する初期値のある双方向バインディング


初期値が要らなければシンプルですが、要る場合の実装がなんとなく面倒だったため試行錯誤しています。

初期値がない場合

EditTextに対する双方向バインディングについてはTextViewBindingAdapterが最初から存在するため、android:onTextChangedにTextViewBindingAdapter.OnTextChangedをバインドするだけでOKでしょう。

LayoutXML(抜粋)

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{viewModel.textChanged}"/>
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.text}" />

ViewModel

 public class MainViewModel extends BaseObservable {
    private String text;

    public MainViewModel() {
    }

    public TextViewBindingAdapter.OnTextChanged textChanged = new TextViewBindingAdapter.OnTextChanged() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            setText(s.toString());
        }
    };

    @Bindable
    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
        notifyPropertyChanged(BR.text);
    }
}

初期値がある場合

上記の方法だけだと当然EditText側には初期値が入りません。

「初期値」を表すフィールドをもう1つViewModelに生やして、EditTextの
android:textに片方向バインドすれば事足れりですが、そのためだけに項目を増やすのもちょっと…と悩みました。

単純に、EditTextにidを振ってFragment側でviewModel.editText.setText("foobar")みたいにするのでもいいかもしれませんが、SQLite等から値を取ってくるような場合では不適切でしょう。

よくあるのがTextWatcherカスタムクラスを利用した方法だと思います。前はこれを利用していましたが、TextViewBindingAdapterをよく見たら既にTextWatcherを利用しているためこれを拡張してみました。

DataBindingAdapter

    @BindingAdapter(value = {"initialText", "android:beforeTextChanged", "android:onTextChanged", "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final String initialText, final TextViewBindingAdapter.BeforeTextChanged before,
                                      final TextViewBindingAdapter.OnTextChanged on, final TextViewBindingAdapter.AfterTextChanged after,
                                      final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    if (before != null) {
                        before.beforeTextChanged(s, start, count, after);
                    }
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        textAttrChanged.onChange();
                    }
                }

                @Override
                public void afterTextChanged(Editable s) {
                    if (after != null) {
                        after.afterTextChanged(s);
                    }
                }
            };
        }
        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, com.android.databinding.library.baseAdapters.R.id.textWatcher);
        if (oldValue == null && TextUtils.isEmpty(view.getText())) {
            // TextWatcherが現在存在せず、かつテキストも入力されていない場合のみ初期値を投入する
            view.setText(initialText);
        }
        if (oldValue != null) {
            view.removeTextChangedListener(oldValue);
        }
        if (newValue != null) {
            view.addTextChangedListener(newValue);
        }
    }

LayoutXML(抜粋)

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{viewModel.textChanged}"
    app:initialText="@{viewModel.text}"/>
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.text}" />

あとはFragment側でviewModel.setText("foobar")すればいいだけです。

カーソル位置が先頭だと不親切なのでもう少し手を加える必要はありそうですが、ひとまずこれでやりたいことはできました。