RxJava and Android examples

Some simple examples of what you can do with RxJava in Android.

We will assume that our examples lives in the onCreate method of an Activity in a Android app for now. We will also assume there is a method called toast that just shows a simple toast when called.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button buttonA = (Button) findViewById(R.id.buttonA);
    Button buttonB = (Button) findViewById(R.id.buttonB);
    Button buttonC = (Button) findViewById(R.id.buttonC);
    PublishSubject psA = PublishSubject.create();
    PublishSubject psB = PublishSubject.create();
    ...
    // This is where the examples will live
    ...
}
private void toast(String s) {
    Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
}

1 Button. Emitting a event when clicking a button.

buttonA.setOnClickListener(v -> Observable.just("A").subscribe(s -> toast(s))); // On each click, emit a event with a string "A" in it and then consume the event with a subscriber.

A trivial example perhaps, but we all need to start somewhere.

2 Buttons. Emitting a event once two buttons have been clicked. So here I want to emit a event once both button A and B have been pressed. An example:
The users presses AAABBA. When the user presses B the first time I want one toast and when the user presses A the last time I want a second toast.

buttonA.setOnClickListener(v -> psA.onNext(true));
buttonB.setOnClickListener(v -> psB.onNext(true));

Observable.zip(psA, psB, (st1, st2) -> "A AND B clicked").subscribe(s -> toast(s)); // show the toast

If the user presses AABB there will be two toasts, the first when pressing B the first time is intentional and the second when pressing B the second time is not intentional. The second toast is due to the buffering of events by zip. So how do we fix this?

A bad but working fix for the buffering issue:

buttonA.setOnClickListener(v -> psA.onNext(true));
buttonB.setOnClickListener(v -> psB.onNext(true));
Observable scdf1 = psA.distinctUntilChanged().filter(b -> b); // Only emit changes, the click listeners will only emit "true"
Observable scdf2 = psB.distinctUntilChanged().filter(b -> b); 
Observable.zip(scdf1, scdf2, (st1, st2) -> true) // zip the streams from button A and B, this will only happen once both button A and B have been pressed.
        .doOnEach(s -> {
            psA.onNext(false); // reset both buttons so they can be clicked again.
            psB.onNext(false);
        })
        .subscribe(s -> toast("A AND B clicked")); // show the toast

This is the first working solution I have come up, but I’m not happy with it. It is to complicated and very inconvenient to extend to large amounts of buttons.

Using scan I’m getting a bit closer to my goal:

Observable.merge(psA, psB)
        .scan((previous, current) -> previous + current) // concatenate the chars
        .filter(s -> s.length() >= 2) // ignore the result if shorter than 2 "ignore first click"
        .map(s -> s.substring(s.length() - 2)) // only look at the last 2 chars in the string
        .filter(s -> s.matches("^(?:(.)(?!.*\1))*$")) // check that the 2 chars are different
        .subscribe(s -> toast(s + " clicked")); // show the toast

This takes care of the annoying buffering of events by consuming then, building a string/history of what has happened and then using a regex to determine if the last to button clicks where on different buttons. This does have one problem except for being hard to read, pressing ABA we will get a toast for the AB and then another toast for BA, I would want to avoid the last toast and “reset” the buttons after AB or BA has been clicked.

This can be simplified a bit:

Observable.merge(psA, psB)
        .scan((p, c) -> p.length() > 0 ? p.substring(p.length() - 1) + c : c) // let scan handle string concatenation
        .filter(s -> s.matches("^(?:(.)(?!.*\\1)){2}$")) // let filter do the "ignore first click" (using {2} at the end enforces the need for 2 chars to match and excludes one char long strings from matching.
        .subscribe(s -> Log.d("TAG", (s + " clicked"))); // show the toast

n Buttons. Not much need to change to enable an arbitrary number of buttons to be used here:

int n = 3; // n is the number of buttons we want to use.
Observable.merge(psA, psB, psC)
        .scan((p, c) -> p.length() < n ? p + c : p.substring(p.length() - n + 1) + c)
        .filter(s -> s.matches("^(?:(.)(?!.*\\1)){" + n + "}$"))
        .subscribe(s -> toast(s + " clicked")); 

But the problem with ABA yielding 2 toasts remains. Using scan with a seed value (a set) I was able to solve my problem in a proper way, not only does it work like I want it to, it also looks prettier and is easier to read.

PublishSubject stringPublishSubject = PublishSubject.create();
buttonA.setOnClickListener(v -> stringPublishSubject.onNext("A"));
buttonB.setOnClickListener(v -> stringPublishSubject.onNext("B"));
buttonC.setOnClickListener(v -> stringPublishSubject.onNext("C"));
int n = 3; // n is the number of buttons we want to use.
stringPublishSubject
        .scan(new HashSet(), (set, string) -> { // buffer the button clicks in the set
            set.add(string); // add the current button click to the set
            return set;
        })
        .filter(set -> set.size() == n) // if the set contains n elements all buttons must have been pressed.
        .subscribe(set -> {
            set.clear(); // reset the set for next round of clicking.
            toast("ABC Clicked"); // show the toast
        });

This Post Has 2 Comments

  1. Kotlin code for this Tutor
    Front of MainActivity:

    package com.homanhuang.rxjavademo

    import android.content.Context
    import android.support.v7.app.AppCompatActivity
    import android.os.Bundle
    import android.view.View
    import android.widget.Button
    import android.widget.TextView
    import android.widget.Toast

    import java.util.Arrays
    import java.util.HashSet

    import io.reactivex.Observable
    import io.reactivex.ObservableEmitter
    import io.reactivex.ObservableOnSubscribe
    import io.reactivex.Observer
    import io.reactivex.disposables.Disposable
    import io.reactivex.subjects.PublishSubject
    import com.homanhuang.rxjavademo.R.id.buttonB
    import com.homanhuang.rxjavademo.R.id.buttonA
    import io.reactivex.functions.BiFunction
    import com.homanhuang.rxjavademo.R.id.buttonB
    import com.homanhuang.rxjavademo.R.id.buttonA

    class MainActivity : AppCompatActivity() {

    var rxTextView: TextView ?= null
    var buttonA: Button ?= null
    var buttonB: Button ?= null
    var buttonC: Button ?= null

    companion object {
    /* Toast shortcut */
    fun msg(context: Context, message: String) {
    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    rxTextView = findViewById(R.id.rxTextView) as TextView
    buttonA = findViewById(R.id.buttonA) as Button
    buttonB = findViewById(R.id.buttonB) as Button
    buttonC = findViewById(R.id.buttonC) as Button

    /*
    Here is your test ground
    */
    Test with zip:

    /*
    Here is your test ground
    */

    val data1 = Observable.just(“one”, “two”, “three”, “four”, “five”)
    val data2 = Observable.just(“one”, “two”, “three”, “four”, “five”)
    val data3 = Observable.just(“one”, “two”, “three”, “four”, “five”)
    val mObList = Arrays.asList(data1, data2, data3)

    /*
    It should output
    Observable.zip(data1, data2, data3, (a, b, c) -> a + b + c);
    */Observable.zip(mObList) { args1 -> args1 }.subscribe { arg ->
    var i = 1for (o in arg) {
    output += i.toString() + “_” + o.toString() + ” : ”
    i++
    }
    }

    //non null assert: !!
    rxTextView!!.setText(output)
    }

    }
    Test with two buttons, zip part 1:

    /*
    Here is your test ground
    */
    // On each click, emit a event with a subscriber.

    val psA = PublishSubject.create()
    val psB = PublishSubject.create()

    //insert non null assert !! for the buttons
    buttonA!!.setOnClickListener { v -> psA.onNext(true) }
    buttonB!!.setOnClickListener { v -> psB.onNext(true) }

    //Error: BiFunction type mismatch with String from
    //Observable.zip(psA, psB,
    //{ st1, st2 -> “A AND B clicked” })
    //.subscribe { s -> msg(this, s as String) }
    //correct line:
    Observable.zip(
    psA,
    psB,
    BiFunction { t1, t2 -> “A AND B clicked” } )
    .subscribe { s -> msg(this, s as String) }

    Two buttons test, zip part 2:

    /*
    Here is your test ground
    */
    // On each click, emit a event with a subscriber.
    val psA = PublishSubject.create()
    val psB = PublishSubject.create()

    buttonA!!.setOnClickListener { v -> psA.onNext(true) }
    buttonB!!.setOnClickListener { v -> psB.onNext(true) }

    //use filter and distinctUntilChanged to avoid duplication
    val scdf1 = psA.distinctUntilChanged().filter { b -> b as Boolean }
    val scdf2 = psB.distinctUntilChanged().filter { b -> b as Boolean }

    // zip the streams from button A and B,
    // this will only happen once both button A and B have been pressed.
    Observable.zip(
    scdf1,
    scdf2,
    BiFunction{st1, st2 -> true })
    .doOnEach { s ->
    psA.onNext(false) // reset so they can be clicked again.
    psB.onNext(false)
    }
    .subscribe { s -> msg(this, “A AND B clicked”) } // show the toast
    Two button, merge part 1:

    /*
    Here is your test ground
    */
    // On each click, emit a event with a subscriber.
    val psA = PublishSubject.create()
    val psB = PublishSubject.create()

    // On each click, emit a event with a subscriber.
    buttonA!!.setOnClickListener { v -> psA.onNext(“a”) }
    buttonB!!.setOnClickListener { v -> psB.onNext(“b”) }

    Observable.merge(psA, psB)
    .scan { previous, current -> previous + current }
    // concatenate the chars
    .filter { s -> s.length >= 2 }
    // ignore the result if shorter than 2 “ignore first click”
    .map { s -> s.substring(s.length – 2) }
    // only look at the last 2 chars in the string
    .filter { s -> s.matches(“.*(ab|ba).*”.toRegex()) }
    // check that the 2 chars are different
    .subscribe { s -> msg(this, s + ” clicked”) }
    // show the toast
    Two buttons, merge part 2:

    /*
    Here is your test ground
    */
    // On each click, emit a event with a subscriber.
    val psA = PublishSubject.create()
    val psB = PublishSubject.create()

    // On each click, emit a event with a subscriber.
    buttonA!!.setOnClickListener { v -> psA.onNext(“a”) }
    buttonB!!.setOnClickListener { v -> psB.onNext(“b”) }

    Observable.merge(psA, psB)
    .scan { p, c ->
    if ( p.length > 0) p.substring(p.length – 1) + c else c }
    // concatenate the chars
    .filter { s -> s.matches(“ab|ba”.toRegex()) }
    // check that the 2 chars are different
    .subscribe { s -> msg(this, s + ” clicked”) }
    // show the toast
    Three buttons:

    /*
    Here is your test ground
    */
    // On each click, emit a event with a subscriber.
    val stringPublishSubject = PublishSubject.create()
    //insert non null assert !! for the buttons
    buttonA!!.setOnClickListener { v -> stringPublishSubject.onNext(“A”) }
    buttonB!!.setOnClickListener { v -> stringPublishSubject.onNext(“B”) }
    buttonC!!.setOnClickListener { v -> stringPublishSubject.onNext(“C”) }

    val n = 3 // n is the number of buttons we want to use.
    stringPublishSubject
    .scan(HashSet()) { // buffer the button clicks in the set
    set, string ->
    set.add(string) // add the current button click to the set
    set
    }
    .filter { set -> set.size == n }
    // if the set contains n elements all buttons must have been pressed.
    .subscribe { set ->
    set.clear() // reset the set for next round of clicking.
    msg(this, “ABC Clicked”) // show the toast
    }

    1. Thanx for supplying the corresponding Kotlin code!

Leave a Reply

Close Menu