I'm trying to make a network request using RxKotlin, but keep getting a NetworkOnMainThreadException I'm subscribing on the main thread, so I'm not sure why it's not taking it off of the UI thread.
Here is where I subscribe to the Observable
weatherInteractor.getWeather(lat, lng)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{response ->
try {
val jsonData = response.body().string()
val currentWeather = getCurrentWeatherData(jsonData)
view!!.displayCurrentWeather(currentWeather)
} catch (e: JSONException) {
Log.d("Present JSON Exception", e.message)
} catch (e: IOException) {
Log.d("Present IO Exception", e.message)
}
},
{
error ->
error.printStackTrace()
}
)
}
Here is where I create my Observable
fun getWeather(lat: Double, lng: Double): Observable<Response> {
val URL = ""
val client = OkHttpClient()
val request = Request.Builder()
.url(URL)
.build()
return Observable.create { em ->
try {
val response = client.newCall(request).execute()
em.onNext(response)
em.onComplete()
} catch (err: IOException) {
err.printStackTrace()
em.onError(err)
}
}
}
It seems that you confuse subscribeOn and observeOn methods.
subscribeOn specifies the scheduler observable will be created on and will operate on. (You specify it once, position doesn't matter).
observeOn changes the scheduler for every action you type after it.
You can use it multiple times and each set of actions will be executed on specified scheduler.
Here is an example:
Observable
.just("test")
.subscribeOn(Schedulers.io())
.map(s -> { //this and all Observable code is executed on Schedulers.io()
return s;
})
.observeOn(Schedulers.computation())
.map(s -> { // executed on Schedulers.computation()
return s;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> { // executed on Android main thread
}, throwable -> {
});
Related
I've written one function with Flow collector which is as shown below,
private fun callSocket(
eventEmmit: String,
eventOn: String,
request: JSONObject
): Flow<SocketCallback<JSONObject>> =
flow {
try {
if (socket.connected()) {
var response = JSONObject()
Log.e("EMIT", JSONObject(Gson().toJson(request)).toString())
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
**this.emit(SocketCallback.OnSuccess(response))**
}.emit(
eventEmmit,
request
)
emit(SocketCallback.OnSuccess(response))
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
emit(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
emit(SocketCallback.OnError(e.toString()))
}
}.flowOn(Dispatchers.IO)
But when I write this.emit(SocketCallback.OnSuccess(response))(enclosed in ** in code) in on method it shows me the error "Suspension functions can be called only within coroutine body".
Any solution for this?
Thanks in advance.
You are trying to emit events to flow outside of coroutineScope. socket.on() function probably has signature:
fun on(ev: String, block: (args: String) -> Unit) {
}
in that case, inside lambda block: (args: String) -> Unit) you are outside of scope and you can not invoke suspending functions.
You have only 2 solutions:
Every time new event approach - create new coroutine with coroutine builder launch:
socket.on(
eventOn
) { args ->
response = args[0] as JSONObject
Log.e("ON", response.toString())
launch {
emit(SocketCallback.OnSuccess(response))
}
}.emit(
eventEmmit,
request
)
Use callbackFlow to avoid creation of new coroutine on each event. Please check especially this post.
Here's how I solved it by using the CallBackFlow
private fun callOnSocket(
eventOn: String
): Flow<SocketCallback<JSONObject>> =
callbackFlow<SocketCallback<JSONObject>> {
try {
if (socket.connected()) {
Log.e("ON", "Started")
var response = JSONObject()
socket.on(
eventOn
) {
response = it[0] as JSONObject
Log.e("ON", response.toString())
trySend(SocketCallback.OnSuccess(response))
}
} else {
Log.e("SOCKET_ERROR", "Socket connection failed")
trySend(SocketCallback.OnError("Socket connection failed"))
}
} catch (e: SocketException) {
trySend(SocketCallback.OnError("Socket connection failed"))
}
awaitClose { cancel() }
}.flowOn(Dispatchers.IO)
i use Rx in android below
repository.getUserList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
val userList = response.body()!!
view.showUser(userList)
}, { throwable ->
handlerException(view, throwable)
}).addToDisposable()
It works.
But sometimes response.body() is null, maybe network error or server not response,
then app will crash.
so i have to add try catch like this
repository.getUserList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
try {
val userList = response.body()!!
view.showUser(userList)
} catch (e: Exception) {
handleException(view, e)
}
}, { throwable ->
handleException(view, throwable)
}).addToDisposable()
But I think there should be a better way to handler error in rx.
can anyone help me, thanks.
you can use filter operator
.filter { it != null }
this was RX way but you can use kotlin null safety like this:
.subscribe({ response ->
response.body()?.let{
view.showUser(userList)
}
}
hope it helped 👍🏻
I'm trying to chain a completable into my Rx chain and when I do so the chain never finishes in the onError or onComplete.
When I step through the code, my completables code is executed. I can even add logging and see it log in it's own doOnComplete()
The below will log "I Completed" but will not go into the the error or complete callback.
profileRepo.getLocalProfileIfAvailableElseRemote()
.flatMapCompletable { profile ->
userRoutingRepo.disableRule(profile.account_uid, userRoutingRule.id)
.doOnComplete {
Log.i("I COMPLETED", "I COMPLETED")
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = { error ->
//do error
},
onComplete = {
//do success
}
).addTo(disposable)
if I instead use flatMap and use the andThen to return a boolean observable, it will work
profileRepo.getLocalProfileIfAvailableElseRemote()
.flatMap { profile ->
userRoutingRepo.disableRule(profile.account_uid, userRoutingRule.id)
.doOnComplete {
Log.i("I COMPLETED", "I COMPLETED")
}.andThen(Observable.just(true))
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = { error ->
//do error
},
onNext = {
//do next
}
).addTo(disposable)
I've tried adding a "andThen" to the flatMapCompletable version and calling Completable.complete() but that doesn't work either?
I can't figure out why my completable is completing, but refuses to work with flatMapCompletable?
EDIT: This is an update of my complete attempt that does not work
Note userRoutingService.disableRule(accountUid, ruleId) is the retrofit interface
profileRepo.getLocalProfileIfAvailableElseRemote()
.flatMapCompletable { profile ->
userRoutingRepo.disableRule(profile.account_uid, userRoutingRule.id)
.andThen(Completable.complete())
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = { error ->
Log.i("TAG", "ERROR")
},
onComplete = {
Log.i("TAG", "COMPLETE")
}
).addTo(disposable)
override fun disableRule(accountUid: String, ruleId: String): Completable {
return activeStateToggler(userRoutingSourceApi.disableRule(accountUid, ruleId),
ruleId,
false)
}
override fun disableRule(accountUid: String, ruleId: String): Completable {
return userRoutingService.disableRule(accountUid, ruleId)
.doOnError { error ->
authenticationValidator.handleAuthenticationExceptions(error)
}
}
private fun activeStateToggler(completable: Completable,
ruleId: String,
stateOnSuccess: Boolean
): Completable {
return completable
.doOnSubscribe {
stateTogglingInProgress.add(ruleId)
}
.doOnComplete {
stateTogglingInProgress.remove(ruleId)
getLocalUserRule(ruleId)?.active = stateOnSuccess
stateTogglingInProgressPublishSubject.onNext(UserRoutingStateToggleSubjectType.Success)
}
.doOnError {
stateTogglingInProgress.remove(ruleId)
stateTogglingInProgressPublishSubject.onNext(UserRoutingStateToggleSubjectType.Error(
it))
}
}
This is what flatMapCompletable does:
Maps each element of the upstream Observable into CompletableSources,
subscribes to them and waits until the upstream and all
CompletableSources complete.
When using flatMapCompletable, the Completable that you return will wait for the upstream's Observable terminal event (onComplete).
When using flatMapCompletable, use it only if you are sure that everything up in the chain completes.
In your case it doesn't work because your source Observable is hot and never completes.
When using flatMapCompletable, you need to return Completable.complete() yourself.
edit:
profileRepo.getLocalProfileIfAvailableElseRemote()
.flatMap { profile ->
userRoutingRepo.disableRule(profile.account_uid, userRoutingRule.id)
.doOnComplete { Log.i("I COMPLETED", "I COMPLETED") } }
.flatMapCompletable { () -> { Completable.complete() } }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = { error ->
//do error
},
onNext = {
//do next
}
).addTo(disposable)
edit 2: since disposableRule is a Completable
profileRepo.getLocalProfileIfAvailableElseRemote()
.flatMapCompletable { profile ->
userRoutingRepo.disableRule(profile.account_uid, userRoutingRule.id)
.doOnComplete { Log.i("I COMPLETED", "I COMPLETED") }
.andThen(Completable.complete().doOnCompleted { Log.i("comp2", "comp2")) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = { error ->
//do error
},
onNext = {
//do next
}
).addTo(disposable)
edit 3: working sample
Observable.just(1)
.flatMapCompletable { profile ->
Completable.complete()
.doOnComplete { Log.i("I COMPLETED", "I COMPLETED") }
.andThen(Completable.complete().doOnComplete { Log.i("I COMPLETED", "I COMPLETED 2") })}
.subscribeBy(
onError = { error ->
},
onComplete = {
Log.d("I COMPLETED", "I COMPLETED 3")
})
I just want to ask if it is possible to get the response of another observable after encountering an error from the another observable?
for example I am calling a two api Avatar and Attachment using a combineLatest.
val avatar: Observable<ResponseBody> = api().getAvatar()
val attachment: Observable<ResponseBody> = api().getAttachment()
val obs = Observables.combineLatest(avatar, attachment)
.map { it ->
if (it.first is Exception) {
Log.e(TAG, "getAvatar failed")
} else {
updateAvatar()
}
if (it.second is Exception) {
Log.e(TAG, "getAttachment failed")
} else {
updateAttachment()
}
if (it.first !is Exception && it.second !is Exception) {
Log.i(TAG, "success first=${it.first}, second=${it.second}")
updateAll()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
disposable.add(obs)
I just want to get the avatar response if the attachment error and I want to get the attachment response if the avatar error.
Thanks.
Yes, my friend. You can handle error for each observable that you combine by calling onErrorReturn() method. You can use empty ResponseBody for detecting error. Final code
val avatar: Observable<Optional<ResponseBody>> = api().getAvatar().onErrorReturn{ Optional.empty }
val attachment: Observable<Optional<ResponseBody>> = api().getAttachment().onErrorReturn{ Optional.empty }
val obs = Observables.combineLatest(avatar, attachment) {avatar, attachment ->
if (!avatar.isPresent()) {
//logic
}
if (!attachment.isPresent()) {
//logic
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
If you use java 7 or lower in you project, you can write your own Optional
class Optional<T>(val value: T?) {
companion object {
fun <T> empty(): Optional<T> = Optional(null)
}
fun isPresent() = value != null
}
I am using retorift to hit getAricle api and get list of articles related to the user. getArticle api will throw error if token passed is expired if so then I have to call refreshToken api to get new token then again I have to call the getArticle api
ApiController.createRx().getArticle(token)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response -> toast(response.body().url) }, { e ->
println(e.printStackTrace())
if(e is HttpException && e.code() in arrayOf(401,403)){
//Here I want to call refresh tolken api
toast("Auth error")
}
else
toast(R.string.something_went_wrong)
})
Edit
Even though given answers showed some direction but those are not a direct answer to my question. This is how solved it but I feel this can be refactored into much better code
ApiController.createRx().getArticle(Preference.getToken())
.flatMap { value ->
if (value.code() in arrayOf(403, 401)) {
ApiController.refreshToken()
ApiController.createRx().getArticle(Preference.getToken())
} else Observable.just(value)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response -> println("Success") }, { e ->
e.printStackTrace()
toast(R.string.something_went_wrong)
})
fun refreshToken() {
val token:String?=ApiController.createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst()?.body()?.token
if (token != null) Preferences.setAuthToken(token)
}
EDIT
I refactored my code to little more cleaner version
Observable.defer { ApiController.createRx().getArticle(Preferences.getToken()) }
.flatMap {
if (it.code() in arrayOf(401, 403)) {
ApiController.refreshToken()
Observable.error(Throwable())
} else Observable.just(it)
}
.retry(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({println("Success") }, {
it.printStackTrace()
toast(R.string.something_went_wrong)
})
fun refreshToken() {
var token: String? = null
try {
token = createRx().refreshToken(Preferences.getRefreshToken()).blockingFirst().body()!!.token
} catch (e: Exception) {
throw e
}
println("saving token")
if (token != null) Preferences.setAuthToken(token)
}
EDIT
Please check my answer for the final refactored code
I have implemented this exact thing. Here is a slightly modified version of that code:
private Observable<Object> refreshTokenIfNotAuthorized(Observable<? extends Throwable> errors) {
final AtomicBoolean alreadyRetried = new AtomicBoolean(false);
return errors.flatMap(error -> {
boolean isAuthorizationError = /* some logic analyzing each error*/ ;
if (isAuthorizationError && !alreadyRetried.get()) {
try {
alreadyRetried.set(true);
String newToken = federatedTokenRefresher.refreshToken()
.toBlocking()
.first();
setLogin(newToken);
return Observable.just(null);
} catch (Exception e) {
return Observable.error(error);
}
}
return Observable.error(error);
});
}
You can use this method like so:
doSomethingRequiringAuth().retryWhen(this::refreshTokenIfNotAuthorized);
What kind of error you will received?. It´s seems like you could use onErrorResumeNext operator.
This operator once that receive a throwable, allow you to return an Observable instead the throwable in the onError
#Test
public void observableOnErrorResumeException() {
Integer[] numbers = {0, 1, 2, 3, 4, 5};
Observable.from(numbers)
.doOnNext(number -> {
if (number > 3) {
try {
throw new IllegalArgumentException();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
})
.onErrorResumeNext(t -> Observable.just(666))
.subscribe(System.out::println);
}
You can see more examples here https://github.com/politrons/reactive/blob/master/src/test/java/rx/observables/errors/ObservableExceptions.java
I will give you another option using groupBy operator
/**
* In this example we create a response code group.
*/
#Test
public void testGroupByCode() {
Observable.from(Arrays.asList(401,403, 200))
.groupBy(code -> code)
.subscribe(groupByCode -> {
switch (groupByCode.getKey()) {
case 401: {
System.out.println("refresh token");
processResponse(groupByCode);
break;
}
case 403: {
System.out.println("refresh token");
processResponse(groupByCode);
break;
}
default: {
System.out.println("Do the toast");
processResponse(groupByCode);
}
}
});
}
private void processResponse(GroupedObservable<Integer, Integer> groupByCode) {
groupByCode.asObservable().subscribe(value -> System.out.println("Response code:" + value));
}
I solved my problem after reading more about RxJava and this is how I implemented it.
First of all will retrofit throw 4xx error to onError or onNext\onSuccess depends on how we define it.
Ex:
#GET("content")
fun getArticle(#Header("Authorization") token: String):Single<Article>
this will throw all the 4xx errors to onError and instead of Single<Article> if you define it as Single<Response<Article>> then all the response from server including 4xx will go to onNext\onSuccess
Single.defer { ApiController.createRx().getArticle(Preferences.getAuthToken())}
.doOnError {
if (it is HttpException && it.code() == 401)
ApiController.refreshToken()
}
.retry { attempts, error -> attempts < 3 && error is HttpException && error.code() == 401 }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({println("Success") }, {
it.printStackTrace()
toast(R.string.something_went_wrong)
})
I am using defer as a wrapper around my actual Observable because I want to recreate the article fetch observable on retry after token refresh because I want Preferences.getAuthToken() to be called again as my refresh token code stores newly fetched token in preference.
retry returns true if the HttpException is 401 and not attempted retry more than 2 times