-->

How to use the state of one observable to skip val

2019-07-14 14:10发布

问题:

This is best explained with a short example.
let's say this is my source observable that I want to filter

Observable.interval(1, TimeUnit.SECONDS)

I use a checkbox to handle the filter state. When the box is not checked, I want to skip all values.
I use RxAndroid to get an observable for the checkbox like this:

RxCompoundButton.checkedChanges(checkBox)

here is my code:

    Observable.combineLatest(
            RxCompoundButton.checkedChanges(checkBox)
            , Observable.interval(1, TimeUnit.SECONDS)
            , (isChecked, intervalCounter) -> {
                if (!isChecked) {
                    return null;
                } else {
                    return intervalCounter;
                }
            }
    ).filter(Objects::nonNull)

and I get the desired output

interval       1--2--3--4--5--6--7--8--9  
checkbox       0-----1--------0-----1---
combineLatest
result         ------3--4--5--------8--9

I am just getting started with RxJava and my "solution" does not feel right, because:
You can see that the combine function returns a magic value null and then the filter will skip these nulls.
That means that the filter function is called even though I already know, that I want to skip this data.

  • maybe I should use another operator
  • is there a way to use the checkbox-observable in a filter function
  • maybe there is some way in the combine function to signal that I want to skip this data

回答1:

Your pictures of the desired output is not correct. In your implementation combineLatest is combining a null into the stream :

interval       1--2--3--4--5--6--7--8--9  
checkbox       0-----1--------0-----1---
combineLatest  N--N--3--4--5--N--N--8--9
filter(NonNull)------3--4--5--------8--9 

IMHO, using null as a signal is not good in Rx Stream, developers can easily fall into NullPointerException.

To avoid the use of null, there are two measures. The first one is to transform the result to a Pair, and apply filter & map later.

A very simple Pair Class:

public class Pair<T, U> {
    T first;
    U second;

    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }
}

Then the whole implementation would like this:

Observable.combineLatest(
        RxCompoundButton.checkedChanges(checkBox)
        , Observable.interval(1, TimeUnit.SECONDS)
        , (isChecked, intervalCounter) -> new Pair<>(isChecked,intervalCounter)
).filter(pair -> pair.first)
 .map(pair -> pair.second)  // optional, do this if you only need the second part

The data flow:

interval          1      2      3      4
                  |      |      |      |
checkbox        F |  T   |   F  |  T   F
                  |  |   |   |  |  |   |
combineLatest    F1  T1  T2  F2 F3 T3  F4
                     |   |         |
filter(first=T)      T1  T2        T3
                     |   |         | 
map(second)          1   2         3

Or if you can Java 8 in your project, use Optional to avoid null, which is very similar to your solution but it gives the awareness for others developers that the Stream signal is Optional.

Observable<Optional<Integer>> o1 = Observable.combineLatest(
        RxCompoundButton.checkedChanges(checkBox)
        , Observable.interval(1, TimeUnit.SECONDS)
        , (isChecked, intervalCounter) -> {
            if (!isChecked) {
                return Optional.empty();
            } else {
                return Optional.of(intervalCounter);
            }
        }
)

Observable<Integer> o2 = o1.filter(Optional::isPresent).map(Optional::get)

Some easier approach which is not same with combineLatest but identical to your desired result pictures.

// Approach 1
Observable.interval(1, TimeUnit.SECONDS).filter( i -> checkBox.isEnabled())

// Approach 2
Observable.interval(1, TimeUnit.SECONDS)
  .withLatestFrom(
    RxCompoundButton.checkedChanges(checkBox),
    (isChecked, intervalCounter) -> {
            if (!isChecked) {
                return Optional.empty();
            } else {
                return Optional.of(intervalCounter);
            }
     }).filter(Optional::isPresent).map(Optional::get)