I am trying to implement a refresh token flow in RxJava2 and Kotlin and I have trouble handling errors. There are several requests that need to be done in sequence but the sequence differs in case there are some errors.
Basically, if I try to use my refresh token and receive 400 - Bad Requestresponse because the token is not valid, I need to terminate the flow and do not execute the next switchMap (and ideally, I would like to return the final Observable<ApplicationRoot>). But I am not sure how to achieve this.
If I use onErrorReturn, I will just pass the returned result to the next switch map. And doOnError just executes the step when the request fails but the whole sequence continues.
fun refreshToken(): Observable<ApplicationRoot> {
// try to use the refresh token to obtain an access token
return authRepository.refreshToken(token)
.switchMap { response ->
// process response here
userRepository.getUser() // fetch user details
}.doOnError {
// TODO - return final result, do not jump to next switchMap
// refresh token is not valid -> go to login screen
Observable.just(ApplicationRoot.LOGIN) // not working
}.switchMap { response -> // excpects response of type UserResponse
// save user details here
}
}
Does anyone know who to jump out of the sequence of switch maps if some error occurs?
Probably, you should do something like this:
fun refreshToken(): Observable<ApplicationRoot> {
return authRepository.refreshToken(token)
.switchMap { response ->
userRepository.getUser()
}
.switchMap { response -> // do something with user response
}
.map { ApplicationRoot.ROOT_THAT_MEANS_SUCCESS }
.onErrorResumeNext(Observable.just(ApplicationRoot.LOGIN))
}
I am not aware about implementation of authRepository.refreshToken and how do responses look like, but in case if response is your custom object rather than retrofit2.Response<T> it should work.
Related
I have been facing this issue for quite sometime and would like to know a better approach to solve this problem. If you are aware of anything about how to solve it then please let me know.
I am building a project which takes data from an API and then following MVVM architecture I take the Retrofit instance to Repository, and then to ViewModel and further observe it from my fragment.
Now what I am working on currently is Login feature. I will send a number to the API and in response I will receive if the number is registered or not. If it is registered then I would move to the next screen.
Now the problem is that using one of the function in ViewModel I send that number to the API to get the response. And using a variable I observe that response.
Now I create a function which checks if the response was true or false and based on that I am using the logic to move to the next screen, but the issue is the returned value from the function. As LiveData works asynchronously in background it takes some time to return the value and in meantime the function returns the initial value which is false.
Function to verify response
private fun checkNumber(): Boolean {
var valid = false
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it.success == true) {
valid = true
}
})
Timber.d("Boolean: $valid")
return valid
}
Moving to next screen code:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
if (checkNumber()) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
}
}
So in case I received the true response from the server even then I would not move to the next screen because the initial value I received is false. I have spent few hours trying to fix it and any help would be appreciated. If you need any code let me know in comments. Thanks.
You have four distinct states:
The server returned a positive response
The server returned a negative response
The server failed (e.g., returned a 500, timed out)
You are waiting on the server
You are attempting to model that with two states: true and false. This will not work.
Instead, model it with four states. One approach is called "loading-content-error" and uses a sealed class to represent those states:
sealed class LoginState {
object Loading : LoginState()
data class Content(val isSuccess: Boolean) : LoginState()
object Error : LoginState()
}
Your LiveData (or your StateFlow, once you migrate to coroutines) would be a LiveData<LoginState>. Your observer can then use a when to handle Loading, Content, and Error as needed, such as:
For Loading, display a progress indicator
For Content, do whatever you are doing now with your boolean
For Error, display an error message
Actually, live data observation is an asynchronous operation. You have to code accordingly.
Just calling checkNumber() won't return since is asynchronous instead I give you some ideas to implement in a better way.
Just call the checkNumber when button click inside the check number do this instead of return valid
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
}
})
I'm investigating Kotlin Coroutines & Channels in my current Android application.
I have the following code that manages remote Api calls and controls UI Side effects
private val historical: CompletableDeferred<List<Any>> = CompletableDeferred()
private val mutex = Mutex()
#ExperimentalCoroutinesApi
fun perform(action: Action): ReceiveChannel<List<Any>> =
produce {
mutex.withLock {
if (historical.isCompleted) {
send(historical.getCompleted())
return#produce
}
send(action.sideEffects)
val networkResponse = repository.perform(action)
send(networkResponse.sideEffects)
send(listOf(networkResponse)).also {
historical.complete(listOf(response))
}
}
}
The above code gives me the desired result, however I would like to refactor it to something resembling
the Functional Programming "Railway Pattern" https://android.jlelse.eu/real-world-functional-programming-with-kotlin-arrow-b5a98e72f5e3
where my process flow is
stepOne(Historical.completed)
.stepTwo(action.sideEffects)
.stepThree(getReaction())
.stepFour(reaction.sideEffects)
.finalStep(reaction)
which will "short circuit" on either failures of any step or when Historical "isCompleted"
is it possible to achieve this style of call in Kotlin? and/or Kotlin & Arrow.kt?
You can use Arrow-kt's Either.
You can use Either's mapLeft(), map(), flatMap().
In case result is Exception use mapLeft(). Returned value from mapLeft() going to be new Left in result Either, for example return is String, result going to be Either<String, List<Any>>. In case result is Right, ie List<Any>, mapLeft() going to be skipped, but result type changes anyway, so you will have type of Either<String, List<Any>> with value of Right<List<Any>>. You can also return same Exception from mapLeft() if you choose so
I case you have no need in handling specific errors, you can just chain map() and flatMap(). map() is basically mapRight() and flatMap() is useful when you want to chain calls, ie somewhere in chain there is a receiver of List<Any> which can fail and you want to handle Exception the same way with Either for that call, you can just return new Either from flatMap()
Code gonna look something like this
fun perform(action: Either<Exception, Action>): ReceiveChannel<List<Any>> =
produce {
// if action is always right, you can start it as Right(action) but then first mapLeft does not make any sense
if (historical.completed) action
.mapLeft {
// handle actions exception here
// transform it to something else or just return it
send(action.sideEffects)
it
}.flatMap {
// handle right side of action either
// assume here that repository may fail and returns Either<Exception, NetworkResponse>
repository.perform(it)
}.mapLeft {
// handle repositorys exception here
// transform it to something else or just return it
send(it)
it
}.map {
// handle network response
send(listOf(networkResponse))
historical.complete(listOf(networkResponse))
}
}
I have a requirement wherein I have to send saved API requests on a button click. These API requests are added to a list and this list is saved to SharedPreferences if the device is offline. Once the device regains connectivity, the saved requests should be sent on a click of a button. If one of the requests get a HTTP status code of 401, the whole process should stop. However, in case of other Exceptions, the process should not interrupted and the next saved request on the list should be sent. If a request succeeds, it is removed from the list of saved requests. At the end of the process, any requests that remain unsent are saved to SharedPreferences.
Now I have a special case for an Exception that I call InvalidRequestException. I want to remove the request from the list when it encounters this particular error, and at the same time I want to carry on sending the remaining requests in the list.
I modeled my code from this post. Here is the code for the method that kicks off the whole process:
public LiveData<UploadStatus> startUploading() {
MutableLiveData<UploadStatus> uploadStatus = new MutableLiveData<>();
compositeDisposable.add(paramRepository.getSavedOfflineRequest() // returns Observable<List<Request>>
.doOnComplete(() -> uploadStatus.setValue(UploadStatus.NO_ITEMS))
.flatMapIterable( requests -> {
requestList = requests;
requestListSizeText.set(Integer.toString(requestList.size()));
return requestList;
}) // observable should now be Observable<Request>
.flatMapCompletable(this::uploadProcess)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() ->{
paramRepository.setOfflineRequestString(""); // clear saved offline requests from shared preferences
uploadStatus.setValue(UploadStatus.SUCCESS);
},
error -> {
if (error instanceof SessionExpiredException) {
uploadStatus.setValue(UploadStatus.LOGGED_OUT);
} else {
if(!requestList.isEmpty()) {
paramRepository.saveRequestsToPrefs(requestList);
} else {
paramRepository.deleteSavedRequests();
}
uploadStatus.setValue(UploadStatus.FAIL);
}
}
)
);
return uploadStatus;
}
The actual sending of saved requests happens in uploadProcess. This is where I attempt to catch the occurrence of InvalidRequestException and delete the request that encounters it:
private Completable uploadProcess(Request request) {
return apiService.transact(saleUrl, BuildConfig.ApiKey,request)
.doOnSubscribe(disposable -> {
uploadAttempts++;
})
.toMaybe()
.onErrorResumeNext(error -> {
if(error instanceof InvalidRequestException) {
requestList.remove(request);
if(requestList.isEmpty()) {
return Maybe.error(new OfflineTxnsNotUploadedException());
}
}
else if (error instanceof SessionExpiredException) // inform UI that session has expired
return Maybe.error(error);
else if (requestList.size() == uploadAttempts) { // nothing was uploaded
return Maybe.error(new OfflineTxnsNotUploadedException());
}
return Maybe.empty();
})
.flatMapCompletable(response -> {
requestList.remove(request);
successCount++;
successCountText.set(Integer.toString(successCount));
return createTransaction(request, response);
});
}
Now when I tested this, I found out that the whole stream stops whenever InvalidRequestException is encountered, which is not the behavior I want. I want to continue sending the other requests in the list. I actually removed the part where the request is removed from the list (requestList.remove(request);), and the stream continued and the next request was sent via apiService.transact().
Am I mistaken in assuming that returning Maybe.empty() would resume the emission of Observable<Request> from the flatMapIterable?
EDIT: It seems I am encountering a ConcurrentModificationException, that's why the stream terminates immediately and the other Requests are not sent. I will have to study this exception first.
As I noted in my edit, I wasn't able to catch the ConcurrentModificationException, thus the entire stream was interrupted. I was in fact, modifying the List<Request> that is being emitted into individual Observable<Request>, since requestList is only a shallow copy of the List<Request> emitted by getSavedOfflineRequest.
One solution I tried was to first serialize the list via Moshi, so that the unserialzied list will not hold any reference to the original list. I found out soon enough that requestList.remove(request) will return false, since I had yet to properly override the equals() and hashCode() method for the Request class. In the end, I just settled to create a "failed requests" list, adding Requests to this list whenever an error is encountered.
I'm implementing codes with jwt on Android.
At point of using refresh token, I'm not sure my code is correct way.
Here is sequene diagram of my flow.
Server issued access token and refresh token. These expire time is 1hour and 3 days. These token is saved to sharedpreferences.
Here is above diagram's description.
When access token is expired, http call will be failed with 401 error.
So I implemented getAccessToken() for re-newing access token.
(1) : One AsyncTask is used for this whole http call step.
- My AsyncTask is too big, I want to refactor it.
(2) : (1)'s AynsTask has a logic for re-getting access token.
- This logic was duplicated all my HTTP call functions.
(3) : After renewing access token, my app re-try to call /api/foo
- To retry it, AsyncTask's doBackground() function is call recursivly.
Here is my code snippet.
class ApplyCheck extends AsyncTask<String, Void, ResponseTypeEnum> {
private List<ApplyEntity> applyEntityList = null;
#Override
protected ResponseTypeEnum doInBackground(String... strings) {
try {
response = restManager.getApplyList(strings[0],"","",""); // call /api/foo
} catch (RestRuntimeException e) {
return ResponseTypeEnum.SERVER_ERROR;
}
switch (response.code()) {
case 200:
//set applyEntityList
....
return ResponseTypeEnum.SUCCESS;
case 401:
//<-- This routine is duplcated all my AsyncTasks
if(getAccessToken()) {
//<-- recursive call to re-call api
return doInBackground(strings);
} else {
return ResponseTypeEnum.TOKEN_EXPIRE;
}
}
}
//re-issue new access token
private boolean getAccessToken() {
Response response = restManager.getAccessToken(); // call /auth/issue-token
if(response.code() == 200) {
String tokens = response.body().string();
JSONObject jsonObject = new JSONObject(tokens);
sharedPreferences.edit().putString("accessToken", jsonObject.getString("accessToken"));
sharedPreferences.edit().putString("refreshToken", jsonObject.getString("refreshToken"));
return true;
} else {
return false;
}
My Questions
1. Is my approach correct? If not, please inform me good practice.
2. If yes, are any good practice for extracting common function for my duplicated AsyncTasks?
The process you have is fine IMHO. The only change is that I would not recursively call doInBackground. What you're doing is feasible, but it violates the intention of doInBackground. Rather modify your AsyncTask to cope with processing different responses in onPostExecute, (ie chaining your requests), and call the AsyncTask again with the relevant parameters for each use case. It will make it much easier to maintain as you can add specific methods to the AsyncTask to cope with each response type and can see how it's triggered in a linear way. If you need to update onProgressUpdate, you should also pass a progress value to the chained AsyncTask calls so it can maintain consistency on the progress. Otherwise it would keep restarting on each call.
I have an issue with my network client design. I have a use case, when the client tries to request an item from a REST API, but in case the API returns a 404 HTTP status code I need to send a request to create the item on the server and then request the item again.
I would like to use RxJava to avoid the callback hell. Is this a valid use case RxJava? Is it possible to create such a conditional sub-request?
Thank you for your time and answers.
Based on your question, I assume you have something that look like
public Observable<Item> getItem();
that will either return the item, or fire an error and
public Observable<?> createItem();
That will create one.
You can use those two together like so:
public Observable<Item> getOrCreateItem() {
return getItem().onErrorResumeNext(error -> {
// Depending on your framework, figure out which is the result code
if (error.getResultCode() == 404) {
return createItem().flatMap(ignored -> getItem());
} else {
return Observable.error(error);
}
});
}
With Retrofit, you'd have to simply make sure the exception is a RetrofitError, cast it, and get the response and the status code. (((RetrofitError) error).getResponse().getStatus())