使用 Android 数据绑定创建双向绑定

Create two-way binding with Android Data Binding

我已经实现了新的Android数据绑定,实现后发现它不支持双向绑定。我曾尝试手动解决此问题,但我正在努力寻找绑定到 EditText 时使用的好的解决方案。 在我的布局中,我有这个视图:

<EditText
android:id="@+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="@{statement.firstName}"/>

另一个视图也在显示结果:

<TextView
style="@style/Text.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{statement.firstName}"/>

在我的片段中,我创建了这样的绑定:

FragmentStatementPersonaliaBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_statement_personalia, container, false);
binding.setStatement(mCurrentStatement);

这有效并将 firstName 的当前值放入 EditText。问题是如何在文本更改时更新模型。我尝试在 editText 上放置一个 OnTextChanged 侦听器并更新模型。这创建了一个循环杀死我的应用程序(模型更新更新 GUI,调用 textChanged 时间无限)。接下来我尝试只在真正发生变化时才通知,如下所示:

@Bindable
public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
        boolean changed = !TextUtils.equals(this.firstName, firstName);
        this.firstName = firstName;
        if(changed) {
            notifyPropertyChanged(BR.firstName);
        }
    }

这样效果更好,但每次我写一封信时,GUI 都会更新,并且由于某种原因,编辑光标会移到前面。

欢迎提出任何建议

@Gober android数据绑定支持双向绑定。因此,您无需手动制作。正如您尝试将 OnTextChanged 侦听器放在 editText 上一样。它应该更新模型。

I tried putting an OnTextChanged-listener on the editText and updating the model. This created a loop killing my app (model-update updates the GUI, which calls textChanged times infinity).

值得注意的是,实现双向绑定的绑定框架通常会为您进行此检查……

这是修改后的视图模型的示例,如果更改源自观察程序,它不会引发数据绑定通知:

让我们创建一个只需要重写一个方法的 SimpleTextWatcher:

public abstract class SimpleTextWatcher implements TextWatcher {

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void afterTextChanged(Editable s) {
        onTextChanged(s.toString());
    }

    public abstract void onTextChanged(String newValue);
}

接下来,我们可以在视图模型中创建一个公开观察者的方法。观察者将配置为将控件的更改值传递给视图模型:

@Bindable
public TextWatcher getOnUsernameChanged() {

    return new SimpleTextWatcher() {
        @Override
        public void onTextChanged(String newValue) {
            setUsername(newValue);
        }
    };
}

最后,在视图中,我们可以使用 addTextChangeListener 将观察者绑定到 EditText:

<!-- most attributes removed -->
<EditText
    android:id="@+id/input_username"
    android:addTextChangedListener="@{viewModel.onUsernameChanged}"/>

下面是解决通知无限的视图模型的实现。

public class LoginViewModel extends BaseObservable {

    private String username;
    private String password;
    private boolean isInNotification = false;

    private Command loginCommand;

    public LoginViewModel(){
        loginCommand = new Command() {
            @Override
            public void onExecute() {
                Log.d("db", String.format("username=%s;password=%s", username, password));
            }
        };
    }

    @Bindable
    public String getUsername() {
        return this.username;
    }

    @Bindable
    public String getPassword() {
        return this.password;
    }

    public Command getLoginCommand() { return loginCommand; }

    public void setUsername(String username) {
        this.username = username;

        if (!isInNotification)
            notifyPropertyChanged(com.petermajor.databinding.BR.username);
    }

    public void setPassword(String password) {
        this.password = password;

        if (!isInNotification)
            notifyPropertyChanged(com.petermajor.databinding.BR.password);
    }

    @Bindable
    public TextWatcher getOnUsernameChanged() {

        return new SimpleTextWatcher() {
            @Override
            public void onTextChanged(String newValue) {
                isInNotification = true;
                setUsername(newValue);
                isInNotification = false;
            }
        };
    }

    @Bindable
    public TextWatcher getOnPasswordChanged() {

        return new SimpleTextWatcher() {
            @Override
            public void onTextChanged(String newValue) {
                isInNotification = true;
                setPassword(newValue);
                isInNotification = false;
            }
        };
    }
}

我希望这就是您正在寻找的并且一定能帮到您。谢谢

编辑 2016 年 5 月 4 日: Android数据绑定现在支持自动绑定两种方式! 只需替换:

android:text="@{viewModel.address}"

与:

android:text="@={viewModel.address}"

例如在 EditText 中,您将获得双向绑定。确保更新到最新版本的 Android Studio/gradle/build-tools 以启用此功能。

(上一个答案):

我尝试了 Bhavdip Pathar 的解决方案,但这未能更新我绑定到同一变量的其他视图。我通过创建自己的 EditText 以不同的方式解决了这个问题:

public class BindableEditText extends EditText{

public BindableEditText(Context context) {
    super(context);
}

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

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

private boolean isInititalized = false;

@Override
public void setText(CharSequence text, BufferType type) {
    //Initialization
    if(!isInititalized){
        super.setText(text, type);
        if(type == BufferType.EDITABLE){
            isInititalized = true;
        }
        return;
    }

    //No change
    if(TextUtils.equals(getText(), text)){
        return;
    }

    //Change
    int prevCaretPosition = getSelectionEnd();
    super.setText(text, type);
    setSelection(prevCaretPosition);
}}

使用此解决方案,您可以以任何方式更新模型(TextWatcher、OnTextChangedListener 等),它会为您处理无限更新循环。使用此解决方案,模型-setter 可以简单地实现为:

public void setFirstName(String firstName) {
    this.firstName = firstName;
    notifyPropertyChanged(BR.firstName);
}

这会减少模型中的代码-class(您可以将侦听器保留在片段中)。

对于我的问题的任何评论、改进或other/better解决方案,我将不胜感激

POJO:

public class User {
    public final ObservableField<String> firstName =
            new ObservableField<>();
    public final ObservableField<String> lastName =
            new ObservableField<>();

    public User(String firstName, String lastName) {
        this.firstName.set(firstName);
        this.lastName.set(lastName);

    }


    public TextWatcherAdapter firstNameWatcher = new TextWatcherAdapter(firstName);
    public TextWatcherAdapter lastNameWatcher = new TextWatcherAdapter(lastName);

}

布局:

 <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName,  default=First_NAME}"/>
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName, default=LAST_NAME}"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/editFirstName"
            android:text="@{user.firstNameWatcher.value}"
            android:addTextChangedListener="@{user.firstNameWatcher}"/>
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/editLastName"
            android:text="@{user.lastNameWatcher.value}"
            android:addTextChangedListener="@{user.lastNameWatcher}"/>

观察者:

public class TextWatcherAdapter implements TextWatcher {

    public final ObservableField<String> value =
            new ObservableField<>();
    private final ObservableField<String> field;

    private boolean isInEditMode = false;

    public TextWatcherAdapter(ObservableField<String> f) {
        this.field = f;

        field.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback(){
            @Override
            public void onPropertyChanged(Observable sender, int propertyId) {
                if (isInEditMode){
                    return;
                }
                value.set(field.get());
            }
        });
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        //
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        //
    }

    @Override public void afterTextChanged(Editable s) {
        if (!Objects.equals(field.get(), s.toString())) {
            isInEditMode = true;
            field.set(s.toString());
            isInEditMode = false;
        }
    }

}

有一个更简单的解决方案。如果它没有真正改变,就避免更新字段。

@Bindable
public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
     if(this.firstName.equals(firstName))
        return;

     this.firstName = firstName;
     notifyPropertyChanged(BR.firstName);
}

当使用 gradle 插件 2.1+

时,Android Studio 2.1+ 现在支持此功能

只需将 EditText 的文本属性从 @{} 更改为 @={},如下所示:

<EditText
android:id="@+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="@={statement.firstName}"/>

有关详细信息,请参阅:https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/

我努力寻找 2 向数据绑定的完整示例。我希望这有帮助。 完整的文档在这里: https://developer.android.com/topic/libraries/data-binding/index.html

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="item"
            type="com.example.abc.twowaydatabinding.Item" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={item.name}"
            android:textSize="20sp" />


        <Switch
            android:id="@+id/switch_test"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="@={item.checked}" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="change"
            android:onClick="button_onClick"/>

    </LinearLayout>
</layout>

Item.java:

import android.databinding.BaseObservable;
import android.databinding.Bindable;

public class Item extends BaseObservable {
    private String name;
    private Boolean checked;
    @Bindable
    public String getName() {
        return this.name;
    }
    @Bindable
    public Boolean getChecked() {
        return this.checked;
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }
    public void setChecked(Boolean checked) {
        this.checked = checked;
        notifyPropertyChanged(BR.checked);
    }
}

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    public Item item;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        item = new Item();
        item.setChecked(true);
        item.setName("a");

        /* By default, a Binding class will be generated based on the name of the layout file,
        converting it to Pascal case and suffixing “Binding” to it.
        The above layout file was activity_main.xml so the generate class was ActivityMainBinding */

        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setItem(item);
    }

    public void button_onClick(View v) {
        item.setChecked(!item.getChecked());
        item.setName(item.getName() + "a");
    }
}

build.gradle:

android {
...
    dataBinding{
        enabled=true
    }

}