50

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

4
  • Where is your getter. Did you add @Bindable annotation to it? Commented Nov 9, 2015 at 16:41
  • Yes. Added the getter to the description now. Commented Nov 10, 2015 at 14:20
  • You are always calling this.firstName = firstName, despite the boolean above it. Have you looked into that logic? Commented Nov 10, 2015 at 14:51
  • It doesn't really affect the binding-part but I see your point. In my solution below the boolean is removed. Commented Nov 11, 2015 at 8:24

6 Answers 6

96

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

Sign up to request clarification or add additional context in comments.

6 Comments

Are you actually set the text to the edit box using data binding.? I see in your actual problem
I suppose you also need to save isInititalized member in the View's BaseSavedState.
I wonder why google doesnt have this on their document page developer.android.com/topic/libraries/data-binding/index.html ? Can you share with me some documents on Android data-binding ?
@nmtuan another commenter posted this which helped me: halfthought.wordpress.com/2016/03/23/…
@Gober after adding this line android:text="@={viewModel.address}" the assignment operator i was like images.clipartbro.com/9/…
|
24

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/

1 Comment

It feels like Angular is. Two way binding eases development.
8

@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

4 Comments

Thank you for a thorough answer. It looked like a viable solution, but unfortunately it did not work for my use-case. First of all, Android Data Binding does not support two-way binding in itself as both you and I are clearly implementing this ourselves. Secondly, the problem I have with this solution is that other views also bind to the same variable. I have an EditText that updates the data and a TextView that outputs the same data. With this solution the TextView is not updated. But if you only have one binding to a variable this is a good solution!
Hi! The Android Studio said that "unknown attribute android:addTextChangedListener" when I add it to the layout xml. Is there anything else to do to make it work? Thanks.
I tried the same and it goes into a loop, I set the model class from text watcher, it will update the UI which in turn again call the onTextChanged() method of texwatcher?
Android Studio 2.1 preview 3 (or better) is supporting it now. Look at halfthought.wordpress.com/2016/03/23/…
2

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

3 Comments

Please see bottom part of my original question. This was my first try, but everytime there is a change (ie writing each letter) the cursor jumps to the front of the edit-text input.
The solution I ended up using was basically this, but moving the cursor to the end again afterwards
Inside recylclerview? Well, unfortunatelly no it seems
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);

}

Layout:

 <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}"/>

Watcher:

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

}

1 Comment

Code dumps are discouraged -- please include some explanations to help improve the quality of this answer.
1

I struggled to find a full example of 2-way databinding. I hope this helps. The full documentation is here: 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
    }

}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.