Tar_Tw45 Tar_Tw45 - 5 months ago 43
Android Question

PublishSubject's onNext call in different thread after I update to Retrofit 2.0

I have a following class that my coworker created while we were using Retrofit 1.9

public class SomeApiCallAction {

private Subscription subscription;
private NoInternetConnectionInterface noInternetConnectionInterface;

public interface NoInternetConnectionInterface {
PublishSubject<Integer> noInternetConnection(Throwable throwable);
}

public void execute(Subscriber subscriber, NoInternetConnectionInterface noInternetConnectionInterface) {
this.noInternetConnectionInterface = noInternetConnectionInterface;
this.subscription = retrofit.someService().someApiCall()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
.retryWhen(retryFunction);
}

public void cancel() {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
}

private Func1<Observable<? extends Throwable>, Observable<?>> retryFunction = new Func1<Observable<? extends Throwable>, Observable<?>>() {
@Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable.flatMap(new Func1<Throwable, Observable<?>>() {
@Override
public Observable<?> call(final Throwable throwable) {
if (noInternetConnectionInterface!= null && (throwable instanceof IOException || throwable instanceof SocketTimeoutException)) {
return noInternetConnectionInterface.noInternetConnection(throwable);
}else{
return Observable.error(throwable);
}
}
});
}
}


SomeApiCallAction is just a simple class that wrap retrofit api call inside, the only thing special is its retry function. The retry function will check if throwable is kind of IOException or SocketTimeoutException or not, if it is, it will call the interface so that we can present retry dialog to user to ask whether they want to retry the operation or not. Our usage is similar to following snippet

public class SomeActivity implement NoInternetConnectionInterface {

@OnClick(R.id.button)
public void do(View v) {
new SomeApiCallAction().execute(
new Subscriber(),
this
)
}

@Override
public PublishSubject<Integer> noInternetConnection(final Throwable throwable) {
Log.i("Dev", Thread.currentThread() + " Error!");
final PublishSubject<Integer> subject = PublishSubject.create();

runOnUiThread(new Runnable() {
@Override
public void run() {
NoInternetDialogFragment dialog = NoInternetDialogFragment.newInstance();
dialog.setNoInternetDialogFragmentListener(new NoInternetDialogFragmentListener{
@Override
public void onUserChoice(boolean retry, NoInternetDialogFragment dialog) {
Log.i("Dev", Thread.currentThread() + " Button Click!");
if (retry) {
subject.onNext(1);
} else {
subject.onError(throwable);
}

dialog.dismiss();

}
});
dialog.show(getSupportFragmentManager(), NoInternetDialogFragment.TAG);
}
});
return subject;
}
}


When we were using Retrofit 1.9.0, this implementation was working perfectly. We test by turn on Airplane Mode and press the button to execute api call.


  • first execution fail and I got UnknownHostException in retry function.

  • so, I call the interface (Activity) to present retry dialog

  • I press retry button while still on Airplane mode to repeat the execution

  • as expected, every execution that happen after user press retry button failed to, I always get UnknownHostException in retry function.

  • If I keep pressing the retry button, retry dialog will appears forever until I turn off the airplane mode.



But after we update our dependencies to

'com.squareup.retrofit2:retrofit:2.0.2'
'com.squareup.retrofit2:adapter-rxjava:2.0.2'


We try again but this time the behaviour change,


  • first execution fail and I got UnknownHostException in retry function same as before.

  • so, I call the interface (Activity) to present retry dialog

  • I press retry button while still on Airplane mode to repeat the execution

  • But this time, in the retry function, instead of receiving UnknowHostException like what it was, I got NetworkOnMainThreadException instead

  • so the condition is not match, interface not gets call, and result as only 1 retry dialog presented to user.



Following is the log from above code

Thread[android_0,5,main] Error!
Thread[main,5,main] Button Click!


Do you have any idea what would cause this? Any suggestion, comment will be very appreciate.

Note : Following are other dependencies that we been using and might related. But they are not recently updated, been using these version since the beginning of this project.

'com.jakewharton:butterknife:8.0.1'

'io.reactivex:rxandroid:1.1.0'
'io.reactivex:rxjava:1.1.0'

'com.google.dagger:dagger-compiler:2.0'
'com.google.dagger:dagger:2.0'
'javax.annotation:jsr250-api:1.0'


More Info

I just reset my code back to the point when we were using Retrofit 1.9, I found the the print log is different

Thread[Retrofit-Idle,5,main] Error!
Thread[main,5,main] Button Click!


Not sure if this relevant to the issue or not, but clearly that in 1.9.0 I call interface in different thread compare to 2.0.0

Final Edit

After reading the answer from @JohnWowUs and follow to the link he provide I found that in Retrofit 2, network call will be synchronous by default

To resolve my issue, there are 2 ways to do this

1.) Do as @JohnWowUs suggest by specify the thread for retryFunction

this.subscription = retrofit.someService().someApiCall()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
.retryWhen(retryFunction, Schedulers.io());


2.) When create retrofit object, specify thread when create RxJavaCallAdapterFactory

retrofit = new Retrofit.Builder()
.baseUrl(AppConfig.BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(getGson()))
.addCallAdapterFactory(
RxJavaCallAdapterFactory.createWithScheduler(
Schedulers.from(threadExecutor)
)
)
.build();

Answer

I think the problem is that when you resubscribe you're subscribing on the main thread as a consequence of using the default trampoline scheduler in retryWhen. Retrofit 1.9 handled the scheduling for you so using subscribeOn was pointless. The issue discussion is here. In Retrofit 2 I believe this has changed so you should try something like

this.subscription = retrofit.someService().someApiCall()
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(subscriber)
      .retryWhen(retryFunction, Schedulers.io());
Comments