Abbas
Abbas

Reputation: 3331

Two-way Databinding with RxJava

I am just learning about RxJava in Android and the (supposed) excellent composition of MVVM, databinding and RxJava. Unfortunately I cannot bind an RxJava Observable directly to a View: need a LiveData.

So, I was wondering is there a way to implement Two-way databinding with RxJava?

So far I've attempted to write a BindingAdapter which adds a listener to the passed View and calls onNext on the Subject.

@BindingAdapter("rxText")
public static void bindReactiveText(EditText view, BehaviorSubject<String> text)
{
    view.setText(text.getValue());

    view.addTextChangedListener(new TextWatcher() {

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

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

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

What this does is updates the Subject/model with any changes in the View, so model always has consistent value with the View. However, the binding is never triggered for any change in the Subject itself (ofcourse we'd have to compare new and old values to stop from looping).

Now, I did try to subscribe to the subject and call setText for each emission, but then I'd have to dispose the Observer. So what I did was also listened for View Attach State change: subscribe in onViewAttachedToWindow and dispose the observer in onViewDetachedFromWindow.

@BindingAdapter("rxText")
public static void bindReactiveText(EditText editText, BehaviorSubject<String> text)
{
    setText(editText, text.getValue());

    editText.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
        private TextWatcher textWatcher = new TextWatcher() {

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

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

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

        private Disposable disposable;

        @Override
        public void onViewAttachedToWindow(View v) {
            editText.addTextChangedListener(textWatcher);

            disposable = text.subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .filter(s -> !s.equals(editText.getText().toString()))
                    .subscribe(s -> setText(editText, text.getValue()));
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            disposable.dispose();
            editText.removeTextChangedListener(textWatcher);
        }
    });
}

And while that does work in the intended way, I'm not sure if this is the best approach to implement Two-way binding via RxJava.

One of the immediate drawback that comes into mind is, Activity/Fragment cannot register a for a callback for most android Views. For instance, if I use the same approach for a Button and its click, and set up a listener in my Activity the binding will stop working.

I am still learning RxJava and its various operators and their uses, so maybe I'm missing something obvious or committing a goof, but I've been trying to working out another way to do this for a few days now, so far have not been able to think of one.

So my question: What is the best approach to implementing Two-way data binding with RxJava Observables.

Upvotes: 2

Views: 2244

Answers (2)

Harry Timothy
Harry Timothy

Reputation: 1156

To bind RxJava Observable directly to a View, you need to use ObservableField:

val name = ObservableField<String>()
//ex: name = "john"

Then put this in your xml:

<data>
     <import type="android.databinding.ObservableField"/>
     <variable 
         name="name" 
         type="ObservableField<String>" />
</data>

<TextView
     android:text="@{name}"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"/>

Reference: https://developer.android.com/topic/libraries/data-binding/observability

EDIT:

In case you need to apply it on a model instead of a single field, you can do this:

class User : BaseObservable() {

    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.firstName)
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.lastName)
        }
}

BR is the name of a class generated by Android Data Binding.

Upvotes: 0

Pavlo Ostasha
Pavlo Ostasha

Reputation: 16699

I would not recommend you to do it like that. While usage of Rx Java with data binding is good(LiveData is better though), your way of implementing it is quite overworked.

This is enough to bind it to the textView.

@BindingAdapter("rxText")
public static void bindReactiveText(EditText view, BehaviorSubject<String> text)
{
    view.setText(text.getValue());

    view.addTextChangedListener(new TextWatcher() {

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

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

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

To subscribe to it and use it you should use a bit different approach. You should use it in view model that should look like this

public class MyViewModel extends ViewModel {
    private CompositeDisposable subscriptions = new CompositeDisposable();
    public BehaviorSubject<String> text = new BehaviorSubject<String>();

    private void bind() {
      subscriptions.add(
        text.subscribe(text -> {
            //do something
        })
      )
    }

    @Override
    public void onCleared(){
      subscriptions.clear()
    }
}

and in layout use

...
    <TextView
        android:id="@+id/text"
        style="@style/TextField.Subtitle3.Primary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/small_margin"
        app:rxText="@{vm.text}"
    />
...

and in fragment

public class MasterFragment extends Fragment {
    private MyViewModel vm;

    public void onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        ViewDataBinding binding = DataBindingUtil.inflate(inflater, layoutRes, container, false);
        model = new ViewModelProvider(requireActivity()).get(MyViewModel.class);
        binding.setVariable(BR.vm, this);
        binding.executePendingBindings();
        vm.bind();
        return binding.root;
    }
}

I would do something like this if I would use RxJava for that. I am using LiveData though.

Hope it helps.

Upvotes: 0

Related Questions