Android RxBinding

Filed Under: Android

In the previous tutorials, we discussed RxJava and some of its operators. Today we will discuss the RxBinding library in our Android Application.

RxBinding

We know that RxJava is a reactive event-based programming paradigm.
RxBinding is a set of support libraries to make the implementation of user interaction with the UI elements in Android easier.

To use RxBinding in your application you must include:


implementation  'com.jakewharton.rxbinding2:rxbinding:2.0.0'

Using this along with the RxJava dependency:


implementation  'io.reactivex.rxjava2:rxjava:2.1.9'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

To use RxBinding with Appcompat and other submodules we simply need to import their respective rxbinding libraries:

  • implementation com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:2.0.0'
  • implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
  • implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
  • implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
  • implementation 'com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:2.0.0'
  • compile 'com.jakewharton.rxbinding2:rxbinding-leanback-v17:2.0.0'

we can use RxBinding features in our application.

RxView.click()

Typically, to set click listener events on a Button, the following is the piece of code that we write:


Button b = (Button)findViewById(R.id.button);
b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(),"Button clicked",Toast.LENGTH_SHORT).show();
            }
        });

Now with RxBinding we can do:


Disposable d = RxView.clicks(button).
                subscribe(new Consumer<Object>() {
                    @Override
                    public void accept(Object o) {
                     Toast.makeText(getApplicationContext(),"Button clicked",Toast.LENGTH_SHORT).show();
                    }
                });

Inside RxView.clicks we pass the View instance which is to be clicked.

If you are new to RxJava2, Disposable is equivalent to Subscription.
For more information on changes in RxJava2 refer here.

On the Disposable instance we can unsubscribe from the event by calling d.dispose();.
The Disposable instance holds a reference to the view. So if this code is defined outside the context of the Activity, you must unsubscribe from the event to prevent memory leaks.

RxView.click() returns an Observable. So we can add RxJava operators to perform transformations and chain implementations on it.

EditText TextChanges

Typically to add text change listeners on an EditText we need to implement the TextWatcher methods:


EditText editText = findViewById(R.id.editText);
editText.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) {
   
   }
});

Using RxBinding we can do:


Disposable d2 = RxTextView.textChanges(editText)
                .subscribe(new Consumer<CharSequence>() {
                    @Override
                    public void accept(CharSequence charSequence) throws Exception {
                        //Add your logic to work on the Charsequence
                    }
                });

We pass the EditText inside RxTextView.textChanges.

Now let’s see how RxJava operators and transformations give us leverage.

Using Map operator

Using map operator we can change the data that is being sent.
For example, in the EditText we can change the CharSequence to a String


Disposable d2 = RxTextView.textChanges(editText)
                .map(charSequence -> charSequence.toString())
                .subscribe(new Consumer<CharSequence>() {
                    @Override
                    public void accept(String string) throws Exception {
                        //Add your logic to work on the Charsequence
                    }
                });


Using debounce operator

Using a debounce operator we can delay the event action.

For example, on the Button click, we can set a debounce of 2 seconds. This will run the operation after 2 seconds:


RxView.clicks(button).debounce(2, TimeUnit.SECONDS).
                observeOn(AndroidSchedulers.mainThread()).
                map(o -> button.getText().toString()).
                subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String o) throws Exception {
                        Toast.makeText(getApplicationContext(),o + "was clicked",Toast.LENGTH_SHORT).show();
                    }
                });

A debounce operation doesn’t run on the UI thread. It runs on the computational thread.
Hence you must call the main thread in the observeOn method after that as done above.

A debounce operator is commonly used in EditText, especially in a SearchView to let the user stop typing for a few seconds before running the action/requests.


Using throttleFirst

Unlike debounce which delays the action, throttleFirst operator is used in preventing repeated actions within a certain time interval.
ThrottleFirst is useful when it comes to preventing double actions when a Button is repeatedly clicked.


RxView.clicks(button).throttleFirst(2, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread()).
                        subscribe(new Consumer<Object>() {
                            @Override
                            public void accept(Object o) {
                                Toast.makeText(getApplicationContext(), "Avoid multiple clicks using throttleFirst", Toast.LENGTH_SHORT).show();
                            }
                        });

In the above code, the Button won’t show a Toast again until after 2 seconds.


Merging Multiple Button click actions

We can merge RxView.click() Observables in the following way:


Button button1 = findViewById(R.id.button1);
Button button2 = findViewById(R.id.button2);
Observable<Object> observable1 = RxView.clicks(button1);
Observable<Object> observable1 = RxView.clicks(button2);

Observable.merge(observable1, observable2)
                        .subscribe(new Consumer<Object>() {
                            @Override
                            public void accept(Object o) {
                                //This common logic would be triggered when either of them are clicked
                            }
                        });


Multiple Click Listeners on a Button


CompositeDisposable compositeDisposable = new CompositeDisposable();

        Observable<Button> clickObservable = RxView.clicks(button).map(o -> button).share();

        Disposable buttonShowToast =
                clickObservable.subscribe(new Consumer<Button>() {
                    @Override
                    public void accept(Button o) throws Exception {
                        Toast.makeText(getApplicationContext(), "Show toast", Toast.LENGTH_SHORT).show();

                    }
                });
        compositeDisposable.add(buttonShowToast);

        Disposable changeButtonText =
                clickObservable.subscribe(new Consumer<Button>() {
                    @Override
                    public void accept(Button o) throws Exception {

                        o.setText("New text");
                    }
                });
        compositeDisposable.add(changeButtonText);

Using the share operator on the RxView.click() observable, both the Disposables created below are run when the button is clicked.

Let’s merge the above concepts in our Android Application with a few more operators:

Project Structure

android rxbinding project

Code

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter here!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.35000002" />

    <TextView
        android:id="@+id/txtBelowEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="16dp"
        android:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editText" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button2"
        app:layout_constraintBaseline_toBaselineOf="@+id/button2"
         />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Button2"
        app:layout_constraintEnd_toStartOf="@+id/button"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/txtBelowButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button2" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp"
        android:layout_marginTop="8dp"
        android:src="@android:drawable/ic_input_add"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
         />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Simulateneous Actions"
        app:layout_constraintBottom_toTopOf="@+id/fab"
        android:layout_margin="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
         />

</android.support.constraint.ConstraintLayout>


MainActivity.java


package com.journaldev.androidrxbinding;

import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;

import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;

public class MainActivity extends AppCompatActivity {


    Button button, button2, button3;
    FloatingActionButton fab;
    TextView txtBelowEditText, txtBelowButton;
    EditText editText;

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

        button = findViewById(R.id.button);
        button2 = findViewById(R.id.button2);
        button3 = findViewById(R.id.button3);
        fab = findViewById(R.id.fab);
        txtBelowEditText = findViewById(R.id.txtBelowEditText);
        txtBelowButton = findViewById(R.id.txtBelowButton);
        editText = findViewById(R.id.editText);


        Observable<Object> observable1 = RxView.clicks(button2).map(o -> button2);
        Observable<Object> observable2 = RxView.clicks(fab).map(o -> fab);


        Disposable d1 = Observable.merge(observable1, observable2).throttleFirst(2, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread()).
                        subscribe(new Consumer<Object>() {
                            @Override
                            public void accept(Object o) {
                                Toast.makeText(getApplicationContext(), "Avoid multiple clicks using throttleFirst", Toast.LENGTH_SHORT).show();
                                if (o instanceof Button) {
                                    txtBelowButton.setText(((Button) o).getText().toString() + " clicked");
                                } else if (o instanceof FloatingActionButton) {
                                    txtBelowButton.setText("Fab clicked");
                                }
                            }
                        });


        Disposable d = RxView.clicks(button).debounce(5, TimeUnit.SECONDS).
                observeOn(AndroidSchedulers.mainThread()).
                map(o -> button.getText().toString()).
                subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String o) throws Exception {
                        txtBelowButton.setText(o + " was clicked");
                    }
                });


        Disposable d2 = RxTextView.textChanges(editText)
                .filter(s -> s.toString().length() > 6)
                .debounce(2, TimeUnit.SECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<CharSequence>() {
                    @Override
                    public void accept(CharSequence charSequence) throws Exception {

                        txtBelowEditText.setText(charSequence);
                    }
                });


        CompositeDisposable compositeDisposable = new CompositeDisposable();

        Observable<Button> clickObservable = RxView.clicks(button3).map(o -> button3).share();

        Disposable buttonShowToast =
                clickObservable.subscribe(new Consumer<Button>() {
                    @Override
                    public void accept(Button o) throws Exception {
                        Toast.makeText(getApplicationContext(), "Show toast", Toast.LENGTH_SHORT).show();

                    }
                });
        compositeDisposable.add(buttonShowToast);

        Disposable changeButtonText =
                clickObservable.subscribe(new Consumer<Button>() {
                    @Override
                    public void accept(Button o) throws Exception {

                        o.setText("New text");
                    }
                });
        compositeDisposable.add(changeButtonText);


        

    }


}

In the EditText, we’ve set a filter operator which doesn’t set the input text onto the TextView until the length crosses a threshold.

To clear all the Disposables, instead of calling disposable() on each of them separately, we can do the following as well:


        CompositeDisposable clearAllDisposables = new CompositeDisposable();
        clearAllDisposables.add(d1);
        clearAllDisposables.add(d2);
        clearAllDisposables.add(d);

        clearAllDisposables.clear();


The output of the application in action is given below:

android rxbinding output

Notice how many times the FloatingActionButton was clicked. But the Toast was displayed just once.

This brings an end to this tutorial. You can download the project from the link below:

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages