Create two-way binding with Android Data Binding
I have implemented the new Android data-binding, and after implementing realised that it does not support two-way binding. I have tried to solve this manually but I am struggling to find a good solution to use when binding to an EditText. In my layout I have this view:
<EditText
android:id="@+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="@{statement.firstName}"/>
Another view is also showing the results:
<TextView
style="@style/Text.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{statement.firstName}"/>
In my fragment I create the binding like this:
FragmentStatementPersonaliaBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_statement_personalia, container, false);
binding.setStatement(mCurrentStatement);
This works and puts the current value of firstName in the EditText. The problem is how to update the model when the text changes. 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). Next I tried to only notify when real changes occured like this:
@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);
}
}
This worked better, but everytime I write a letter, the GUI is updated and for som reason the edit-cursor is moved to the front.
Any suggestions would be welcome
EDIT 04.05.16: Android Data binding now supports two way-binding automatically! Simply replace:
android:text="@{viewModel.address}"
with:
android:text="@={viewModel.address}"
in an EditText for instance and you get two-way binding. Make sure you update to the latest version of Android Studio/gradle/build-tools to enable this.
(PREVIOUS ANSWER):
I tried Bhavdip Pathar's solution, but this failed to update other views I had bound to the same variable. I solved this a different way, by creating my own 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);
}}
With this solution you can update the model any way you want (TextWatcher, OnTextChangedListener etc), and it takes care of the infinite update loop for you. With this solution the model-setter can be implemented simply as:
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
This puts less code in the model-class (you can keep the listeners in your Fragment).
I would appreciate any comments, improvements or other/better solutions to my problem
This is now supported in Android Studio 2.1+ when using the gradle plugin 2.1+
Simply change the EditText's text attribute from @{}
to @={}
like this:
<EditText
android:id="@+id/firstname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords|textNoSuggestions"
android:text="@={statement.firstName}"/>
for more info, see: https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/
@Gober The android data-binding support the two way binding. Therefore you do not need to make it manually. As you tried by putting the OnTextChanged-listener on the editText. It should update the model.
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).
It’s worth noting that binding frameworks that implement two-way binding would normally do this check for you…
Here’s the example of modified view model, which does not raise a data binding notification if the change originated in the watcher:
Let’s create a SimpleTextWatcher that only requires only one method to be overridden:
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);
}
Next, in the view model we can create a method that exposes the watcher. The watcher will be configured to pass the changed value of the control to the view model:
@Bindable
public TextWatcher getOnUsernameChanged() {
return new SimpleTextWatcher() {
@Override
public void onTextChanged(String newValue) {
setUsername(newValue);
}
};
}
Finally, in the view we can bind the watcher to the EditText using addTextChangeListener:
<!-- most attributes removed -->
<EditText
android:id="@+id/input_username"
android:addTextChangedListener="@{viewModel.onUsernameChanged}"/>
Here is the implementation of the view Model that resolve the notification infinity.
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;
}
};
}
}
I hope this is what you are looking and sure can help you. Thanks
There is a simpler solution. Just avoid updating field if it hadn't really changed.
@Bindable
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
if(this.firstName.equals(firstName))
return;
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}