23

I got a weird issue in one of my activities. When coming back from taking a picture / video, in my onActivityResult I am showing a dialog that lets the user name the camera. Once the user presses OK, I send onNext() to a subject with the requested file name that copies the file (and shows progress dialog).

For some reason the map() function that does the copy is always called on the main thread, even though I call subscribeOn(Schedulers.io()).

@Override
protected void onActivityResult(final int requestCode, int resultCode, Intent intent) {
    ...

    final PublishSubject<String> subject = PublishSubject.create();`

    mSubscription = subject
            .subscribeOn(Schedulers.io())
            .map(new Func1<String, String>() {
                @Override
                public String call(String fileName) {
                    Log.I.d(TAG,"map");
                    return doSomeIOHeavyFuncition();
                }
            })
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Action1<String>() {
                @Override
                public void call(final String fullPath) {
                    Log.d(TAG,"onNext");
                    doSomethingOnUI(fullPath);

                    subject.onCompleted();
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    ...
                }
            }, new Action0() {
                @Override
                public void call() {
                    ...
                }
            });

    final AlertDialog dialog = new AlertDialog.Builder
    ....
    .create()
            .show();

    dialog.getButton(DialogInterface.BUTTON_POSITIVE)
            .setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    String someString = getStringFromDialog(dialog);

                    dialog.dismiss();
                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(input.getWindowToken(), 0);

                    showProgressDialog();
                    subject.onNext(someString);
                }
            });
}

Changing the subscribeOn(Schedulers.io()) call to observeOn(Schedulers.io()) solved the issue. Still I would like to know why it didn't work...

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Yoshkebab
  • 770
  • 1
  • 7
  • 14

1 Answers1

56

subscribeOn and observeOn is the mostly confused operators there are. The former makes sure that subscription side effects happen on the specified scheduler (thread), but that doesn't mean that values will pop up on that thread as well.

For example, if your Observer opens a network connection when one subscribes to it, you don't want that to run on the main thread, therefore, you need subscribeOn to specify where that subscription and thus the network connection will be created.

When data finally arrives, the emitting thread can be anything, one of the schedulers or a background plain old thread. Since we don't know or don't like that thread, we want to move the observation of the data to another thread. This is what observeOn does: makes sure operators after it will execute their onNext logic on the specified scheduler. Android devs use it already to move the observation of values back to the main thread.

What's rarely explained though is what happens when you want some extra computation off the main thread before the final result lands on the main thread again: use multiple observeOn operators:

source
.observeOn(Schedulers.computation())
.map(v -> heavyCalculation(v))
.observeOn(Schedulers.io())
.doOnNext(v -> { saveToDB(v); })
.observeOn(AndroidSchedulers.mainThread())
...
akarnokd
  • 69,132
  • 14
  • 157
  • 192
  • 3
    That's something new for me. Using RxJava for some time I've been sure that `subscribeOn` thread applies to all downstream operators up to the first `observeOn` or an operator with a specified scheduler. Is it not true? In this case what is the rule for the thread selection after `subscribeOn`? – AndroidEx Feb 22 '16 at 13:43
  • 2
    `subscribeOn` effects go upstream and closer to the source of events. Think of it as this. you have a source but you want to subscribe to it on a different thread. You'd write `new Thread(() -> source.subscribe(s)).start();`. Something like this happens in `subscribeOn`. You chose the scheduler based on what happens when one subscribes to that source. If it immediately opens a network connection or starts reading a long file, you go for `Schedulers.io()`. If it start heavily computing someting, you do `computation()`. – akarnokd Feb 22 '16 at 14:02
  • 4
    Thank you. Though I was more interested in what happens after subscribeOn, what thread the next operator is applied on. From your words I've got an impression that it's not guaranteed to be the thread specified in the subscribeOn. – AndroidEx Feb 22 '16 at 14:09
  • @AndroidEx I believe, to say it is not guaranteed is not correct, the thread normally changes (specially returns to the main thread) when a callback function is called. Since normal background workers call their callbacks on the main thread (to let us do our ui works on them) . During our work with rx if we happen to use one of them the task returns to the mainthread after it. – Hossein Shahdoost Jul 12 '18 at 08:30