使用Android Data Binding创建双向绑定

50

我已经实现了新的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}"/>

在我的Fragment中,我创建绑定的方式如下:

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

这段代码能够将firstName的当前值放入EditText中。问题在于,如何在文本改变时更新模型。我尝试在editText上添加OnTextChanged监听器并更新模型,但这会导致应用程序崩溃(模型更新会更新GUI,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都会更新,并且由于某种原因,编辑光标会移动到前面。

欢迎提出任何建议。


你的getter方法在哪里?你是否添加了@Bindable注解? - IgorGanapolsky
是的。现在已向描述中添加getter函数。 - Gober
你总是在调用this.firstName = firstName,尽管上面有布尔值。你是否查看过这个逻辑? - IgorGanapolsky
这并不会真正影响绑定部分,但我理解你的观点。在我下面的解决方案中,布尔值被移除了。 - Gober
6个回答

95

编辑于04.05.16: 现在,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);
}

这将减少模型类中的代码量(您可以将监听器保留在Fragment中)。

我将非常感激任何关于我的问题的评论、改进或其他/更好的解决方案。


1
你是否真正使用数据绑定将文本设置到编辑框中?我在你的实际问题中看到了这一点。 - Bhavdip Sagar
我想你也需要在“View”的“BaseSavedState”中保存“isInitialized”成员。 - WindRider
4
我不知道为什么谷歌的文档页面上没有这个 https://developer.android.com/topic/libraries/data-binding/index.html ? 你能否分享一些关于Android数据绑定的文档? - rocketspacer
1
@nmtuan,另一位评论者发布了这篇文章,对我很有帮助:https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/ - Gober
1
@Gober 在添加了 android:text="@={viewModel.address}" 这行代码后,我就像这个表情包一样:http://images.clipartbro.com/9/gallery-for-amp-gt-troll-face-happy-crying-9296.jpeg - Shubham AgaRwal
显示剩余2条评论

24

当使用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/


似乎Angular是的。双向绑定简化了开发。 - Juan Mendez

8

@Gober,Android的数据绑定支持双向绑定。因此,您不需要手动制定它。正如您尝试在editText上放置OnTextChanged-listener一样。它应该更新模型。

我尝试在editText上放置OnTextChanged监听器并更新模型。这创建了一个循环,导致我的应用程序崩溃(模型更新会更新GUI,从而无限次调用textChanged)。

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

以下是修改后的视图模型示例,如果更改源于监听器,则不会引发数据绑定通知:

让我们创建一个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;
            }
        };
    }
}

我希望这是你需要的,并且可以帮助你。谢谢。

2
谢谢您的详细回答。它看起来是一个可行的解决方案,但不幸的是它并没有适用于我的用例。首先,Android 数据绑定本身不支持双向绑定,因为您和我都在明显地自己实现这个功能。其次,我对这个解决方案的问题是其他视图也绑定到了同一个变量上。我有一个 EditText 来更新数据和一个 TextView 来输出相同的数据。使用这个解决方案后,TextView 没有被更新。但如果您只有一个变量的绑定,那么这是一个好的解决方案! - Gober
1
你好!当我将"android:addTextChangedListener"添加到布局xml中时,Android Studio显示了"unknown attribute android:addTextChangedListener"的错误提示。请问还需要做些什么才能让它正常工作呢?谢谢。 - wangzhangjian
我尝试了相同的方法,但它进入了一个循环。我从文本监视器中设置了模型类,它将更新UI,进而再次调用texwatcher的onTextChanged()方法? - Vishnu Prabhu
1
Android Studio 2.1预览版3(或更高版本)现在已经支持它了。请查看https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/。 - marciowb

2
有一种更简单的解决方案。如果字段没有真正改变,那就避免更新它。
@Bindable
public String getFirstName() {
    return firstName;
}

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

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

1
请查看我原问题的底部。这是我的第一次尝试,但每次有更改(例如写每个字母)时,光标都会跳到编辑文本输入框的前面。 - Gober
我最终采用的解决方案基本上就是这个,但之后要将光标再次移动到结尾。 - Gober
在RecyclerView内部?很遗憾,似乎不是这样。 - Vlado Pandžić

1
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;
        }
    }

}

2
不建议仅粘贴代码--请包含一些解释以帮助提高答案的质量。 - CubeJockey

1

我曾经苦苦寻找一个完整的双向数据绑定示例,希望这可以帮到你。 完整的文档在这里: 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
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接