I'm using rxAndroid and rxKotlin in my Android app to handle network requests asynchronously. Now I would like to retry a failed network request only after click on Snackbar button.
My code now:
val citiesService = ApiFactory.citiesService
citiesService.cities()
.subscribeOn(Schedulers.newThread()) // fetch List<String>
.flatMap { Observable.from(it) } // convert to sequence of String
.flatMap { city ->
citiesService.coordinates(city) // fetch DoubleArray
.map { City(city, it) } // convert to City(String, DoubleArray)
}
.toList()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
listView.setOnItemClickListener { adapterView, view, position, id ->
onItemClick(it[position])
}
}
.map { it.map { it.getName(activity) } }
.subscribe(
{ listAdapter = setupAdapter(it) },
{ showErrorSnackbar() } // handle error
)
fun showErrorSnackbar() {
Snackbar.make(listView, getString(R.string.not_available_msg), Snackbar.LENGTH_INDEFINITE)
.setAction(getString(R.string.snack_retry_btn), {
// retry observable
})
.show()
}
Cities interface for retrofit:
interface CitiesService {
#GET("api/v1/cities")
fun cities(): Observable<List<String>>
#GET("api/v1/cities/{city}/coordinates")
fun coordinates(#Path("city") city: String): Observable<DoubleArray>
}
Api factory:
object ApiFactory {
val citiesService: CitiesService
get() = retrofit.create(CitiesService::class.java)
private val retrofit: Retrofit
get() = Retrofit
.Builder()
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
}
How can I restart the observable in such way?
I can suggest you truly reactive way instead of imperative way.
Insert this code right after subscribe() method:
.retryWhen(retryHandler ->
retryHandler.flatMap(nothing -> retrySubject.asObservable()))
.subscribe()
Where update subject is just:
#NonNull
private final PublishSubject<Void> retrySubject = PublishSubject.create();
And on snackbar click call this method:
public void update() {
retrySubject.onNext(null);
}
Everything above the retryWhen method will be literally redone.
Though with this approach error will never go down to the subscriber, you can add error conditioning to the retryHandler flat map, but this is another story.
P.S. sorry, this was Java code with retrolambdas, but you'll easily convert this to Kotlin.
Related
I am trying to use RxJava with FirebaseRemoteConfig but not sure how to make the two work, tried using Completable but I get an error The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with.
I have this problem where I need to fetch from RemoteConfig then initialize the String variable from MySingleton.class with this latest config to be use later. The said String variable must not be null or empty so the flow would be.
During Splash
Call fetchAndActivate
Listen for both OnSuccess and OnFailure
If success initialize the static String variable with the latest config
If failed try to use old/cached configs
Cache might not exist yet specially on first run so check if the static String variable is empty
If empty show AlertDialog for retry.
If not proceed to main activity.
What I am trying to do is to use RxJava and listen for OnSuccess or OnFailure listeners, which I can probably apply as well when getting just a single document when using Firebase Firestore in the future.
How can I do this?
So far this is what I got
class RemoteConfig {
companion object {
private val remoteConfig: FirebaseRemoteConfig by lazy {
FirebaseRemoteConfig.getInstance()
}
private val remoteConfigSettings: FirebaseRemoteConfigSettings by lazy {
FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(1800)
.build()
}
fun init(context: Context): Completable {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Completable.create {
fetchConfig(context)
}
}
private fun fetchConfig(context: Context): Completable {
return Completable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
emitter.onComplete()
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
emitter.onError(it.cause!!)
FirebaseCrashlytics.getInstance().recordException(it)
}
}
}
private fun assignSource(context: Context) {
Singleton.staticVariable=
remoteConfig.getString(context.getString(R.string.key))
}
}
}
Splash activity
Completable.mergeArray(
RemoteConfig.init(this).subscribeOn(Schedulers.io()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(5, TimeUnit.SECONDS)
.subscribe({
proceedToMain()
}, {
if (Singleton.staticVariable.isEmpty())
AlertDialog.Builder(this)
.setMessage(it.message)
.setPositiveButton("Retry"
) { dialog, _ ->
run {
dialog.dismiss()
fetchConfig()
}
}
.setNegativeButton("Exit"
) { dialog, _ ->
dialog.dismiss()
finish()
}
.setCancelable(false)
.show()
else
proceedToMain()
})
Manage to make it work it seems all I need is to use tryOnError since it will automatically handle the case when the emitter get disposed/canceled or no longer available. Instead of Completable using just Observable is okay too.
fun init(context: Context): Observable<Boolean> {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Observable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
Log.wtf("CONFIG", "SUCCESS")
emitter.onNext(it)
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
Log.wtf("CONFIG", "FAILED")
emitter.tryOnError(it.cause!!) // Try to throw an error if emitter still available or if the sequence is not cancelled/disposed
FirebaseCrashlytics.getInstance().recordException(it)
}.addOnCompleteListener {
emitter.onComplete()
Log.wtf("CONFIG", "COMPLETE")
}
}
}
Splash
cryptonHadItsChance = RemoteConfig.init(this)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
proceedToMain()
}, {
//Only show dialog if necessary field is not available and activity still running
if (Singleton.staticVariable.isEmpty() && !isFinishing && !isDestroyed)
alertDialog.show()
else
proceedToMain()
})
When the response came with empty body, Retrofit BodyObservable calls onNext() with null value. In RxJava2 that leads to NPE, but that is not an issue, and there's a recommended way to deal with it using Completable or Optional converter (https://github.com/square/retrofit/issues/2242).
The problem I have encountered that in some scenarios onError() handler is not triggered at all, so that observable silently fails when getting NPE (making it harder to spot and fix). I've managed to isolate this case:
data class Item(val name: String)
class Retrofit2WithRxJava2TestCase {
#Rule
#JvmField
public val server: MockWebServer = MockWebServer()
private var disposable: Disposable? = null
interface Service {
#GET("/{path}")
fun getItem(
#Path(value = "path", encoded = true) path: String?
): Observable<Item>
#DELETE("/{path}")
fun deleteItem(
#Path(value = "path", encoded = true) path: String?
): Observable<Item>
}
#Test
#Throws(Exception::class)
fun test() {
val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val example = retrofit.create(Service::class.java)
server.enqueue(
okhttp3.mockwebserver.MockResponse()
.addHeader("Content-Type", "text/json")
.setBody("""{"name": "Name"}""")
)
server.enqueue(
okhttp3.mockwebserver.MockResponse()
.setResponseCode(204)
)
val observable: Observable<Item> = example.getItem("hello")
val observable2: Observable<Item> = example.deleteItem("hello")
val countDownLatch = CountDownLatch(1)
disposable = observable
.flatMap {
observable2
//.doOnError { // <- This one would be triggered
// println("doOnError triggered")
//}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { // <- Never reaches here
countDownLatch.countDown()
println("doFinally")
}
.subscribe({ next ->
println("onNext")
}, { error -> // <- Never reaches here
println("onError $error")
})
val result = countDownLatch.await(7, TimeUnit.SECONDS)
Assert.assertTrue(result)
}
}
So, the question is why the NPE is not propagated to onError() in this case, when observable2 inside .flatMap{} gets empty body, and hence triggers onNext() with null value?
Thank you for all suggestions.
EDIT:
Simplifying test observable to make sure unit test thread won't exit before observable completion:
observable.flatMap {
//Observable.error<Throwable>(NullPointerException()) //<- Would reach onError()
observable2
}
.subscribeOn(Schedulers.trampoline())
.observeOn(Schedulers.trampoline())
.doFinally { // <- Never reaches here
println("doFinally")
}
.blockingSubscribe({ next ->
println("onNext")
}, { error -> // <- Never reaches here
println("onError $error")
})
There are cases when I need to chain RxJava calls.
The simplest one:
ViewModel:
fun onResetPassword(email: String) {
...
val subscription = mTokenRepository.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(
//UI update calls
)
...
}
My Repository:
fun resetPassword(email: String): Single<ResetPassword> {
return Single.create { emitter ->
val subscription = mSomeApiInterface.resetPassword(email)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({
emitter.onSuccess(...)
}, { throwable ->
emitter.onError(throwable)
})
...
}
}
My Question
Do I need to Add:
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
for both calls to avoid any app freeze? or the second one for API call is enough?
No, you don't need to add
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
for the repo and the viewmodel.
.observeOn usually should be called right before handling the ui rendering. So usually, you'll need it in the ViewModel right before updating the ui or emitting the LiveData values.
Also, you properly don't need to subscribe to mSomeApiInterface in your repo, I think it would be better off to just return in as it's from your method up the chain, somthing like this:
fun resetPassword(email: String): Single<ResetPassword> {
return mSomeApiInterface.resetPassword(email);
}
and if you have any mapping needed you can chain it normally
fun resetPassword(email: String): Single<ResetPassword> {
return mSomeApiInterface.resetPassword(email)
.map{it -> }
}
This way you can write your ViewModel code as follow
fun onResetPassword(email: String) {
...
// note the switcing between subscribeOn and observeOn
// the switching is in short: subscribeOn affects the upstream,
// while observeOn affects the downstream.
// So we want to do the work on IO thread, then deliver results
// back to the mainThread.
val subscription = mTokenRepository.resetPassword(email)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
//UI update calls
)
...
}
This will run the API request on the io thread, will returning the result on the mainThread, which is probably what you want. :)
This artical has some good examples and explanations for subscribeOn and observeOn, I strongly recommend checking it.
Observable<RequestFriendModel> folderAllCall = service.getUserRequestslist(urls.toString());
folderAllCall.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(result -> result.getRequested())
.subscribe(this::handleResults, this::handleError);
private void handleResults(List<Requested> folderList) {
if (folderList != null && folderList.size() != 0) {
usersList.addAll(folderList);
}
adapter.notifyDataSetChanged();
}
}
private void handleError(Throwable t) {
Toast.makeText(getContext(),t.getMessage(),Toast.LENGTH_LONG).show();
}
in interface:
#Headers({ "Content-Type: application/json;charset=UTF-8"})
#GET
Observable<RequestFriendModel> getUserRequestslist(#Url String url);
POJO model :
public class RequestFriendModel {
#SerializedName("requested")
#Expose
private List<Requested> requested = null;
public List<Requested> getRequested() {
return requested;
}
public void setRequested(List<Requested> requested) {
this.requested = requested;
}
}
I am new to Kotlin and I am making a method that makes a call to an interface of Endpoints and uses one of the methods present there. I am using Observable<> instead of Call<> into the response. I wanted to know how to obtain the response body() in the "result" above. This is my method
private fun refreshUser(userLogin: String) {
executor.execute {
// Check if user was fetched recently
val userExists = userDao.hasUser(userLogin, getMaxRefreshTime(Date())) != null
// If user have to be updated
if (!userExists) {
disposable = endpoints.getUser(userLogin)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> /*Get the response body() HERE*/},
{ error -> Log.e("ERROR", error.message) }
)
}
}
}
It all depends on how you have defined the Retrofit interface. In order to get the Response you need to return something from the interface that looks like:
fun getUsers() : Observable<Response<User>>
Then inside { result -> /*Get the response body() HERE*/}, you will get something of the form Response<User>, which has the response's body.
Also to note, you do not need to enclosing executor if you leverage Room for the dao interactions; it has RxJava support. You can use RxJava operators to combine the dao lookup with the server call.
See this tutorial
https://medium.freecodecamp.org/rxandroid-and-kotlin-part-1-f0382dc26ed8
//Kotlin
Observable.just("Hello World")
.subscribeOn(Schedulers.newThread())
//each subscription is going to be on a new thread.
.observeOn(AndroidSchedulers.mainThread()))
//observation on the main thread
//Now our subscriber!
.subscribe(object:Subscriber<String>(){
override fun onCompleted() {
//Completed
}
override fun onError(e: Throwable?) {
//TODO : Handle error here
}
override fun onNext(t: String?) {
Log.e("Output",t);
}
})
if you wanna use retrofit 2 and rxjava 2
https://medium.com/#elye.project/kotlin-and-retrofit-2-tutorial-with-working-codes-333a4422a890
interface WikiApiService {
#GET("api.php")
fun hitCountCheck(#Query("action") action: String,
#Query("format") format: String,
#Query("list") list: String,
#Query("srsearch") srsearch: String):
Observable<Model.Result>
}
Observable is the class response.
private fun beginSearch(srsearch: String) {
disposable =
wikiApiServe.hitCountCheck("query", "json", "search", srsearch)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> showResult(result.query.searchinfo.totalhits) },
{ error -> showError(error.message) }
)
}
If, as you mentioned to #Emmanuel, the return type of your getUser() method is Observable<Response<User>> then calling result.body() will yield the resulting User.
{ result ->
val user: User = result.body()
}
If however, you are looking for the the raw response, you can instead call result.raw().body(); which will return an okhttp3.ResponseBody type.
{ result ->
val body: ResponseBody = result.raw().body()
val text: String = body.string()
}
I am using Fuel and Rxjava to make network calls. I have set my base URL to localhost, which at isn't serving anything. I want to be able to handle network errors so I can show some sort of error message on the UI to the user.
Here is an example of my GET request
fun getRandom(take: Int, responseHandler: (result: WikiResult) -> Unit?) {
Urls.getRandomURl(take)
.httpGet()
.timeout(timeout)
.timeoutRead(readTimeout)
.rx_object(WikipediaDataDeserializer())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
val statusCode = result.component2()?.response?.statusCode
when(statusCode) {
-1 -> e(statusCode.toString(), result.component2()?.cause.toString())
else -> {
val (data, _) = result
responseHandler.invoke(data as WikiResult)
}
}
}, {
error -> e(error.cause.toString())
})
}
And on my fragment I am calling the above function in a async task
private fun getRandomArticles() {
refresher?.isRefreshing = true
wikiManager?.getRandom(15, { wikiResult ->
adapter.currentResults.clear()
adapter.currentResults.addAll(wikiResult.query!!.pages)
onUiThread {
adapter.notifyDataSetChanged()
refresher?.isRefreshing = false
}
})
}
private fun reportException(e: Throwable) {
refresher?.isRefreshing = false
val builder = AlertDialog.Builder(activity)
builder.setMessage(e.message).setTitle("Error")
val dialog = builder.create()
dialog.show()
}
So I get a network error java.net.ConnectException: Failed to connect to localhost/127.0.0.1:80
I want to be able to get this on my fragment and display an error on the fragment. Not sure what the best approach is for this.
You can see the full project code on here
https://github.com/limpep/android-kotlin-wikipedia
under branch feature/rxjava
any help would be much appreciated
It is hard to say how to write good code for this in your structure because your code is not very clear in its separation and it is not necessary to use AsyncTask and runOnUIThread when you are already using .subscribeOn() and .observeOn() on your observable.
Maybe this would be a better basis for structure:
fun getRandom(take: Int): Single<WikiResult> {
return Urls.getRandomURl(take)
.httpGet()
.timeout(timeout)
.timeoutRead(readTimeout)
.rx_object(WikipediaDataDeserializer())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.map{ if(it.component2()!=null) throw it.component2() else it.component1() as WikiResult }
}
private fun getRandomArticles() {
refresher?.isRefreshing = true
getRandom().doOnCompleted{refresher?.isRefreshing = false}
.subscribe(this::handleResponse,this::reportException)
}
private fun handleResponse(wikiResult:WikiResult){
adapter.currentResults.clear()
adapter.currentResults.addAll(wikiResult.query!!.pages)
adapter.notifyDataSetChanged()
}