Android Development - RxJava with Network Requests: Implementing Token Refresh and Retrying Network Requests
Hello everyone!
It’s been a while since my last post,
Today I want to share a method to solve the issue of refreshing tokens and retrying the same network request when using RxJava with network requests (e.g., OkHttp + Retrofit).
This problem is very common in apps that require a lot of connections.
When we need to make requests to the server,
to ensure the legality of the user,
we usually use a token mechanism to verify login or access API permissions.
And tokens usually have an expiration date,
to provide a good user experience,
we need to implement a more complete process when we are unaware that the token in a network request has expired.
In this article, I will list the related knowledge used,
but the main focus is on sharing the process of implementing token refresh and network reconnection,
so I won’t go into detail about each one. If you are interested, you can look it up or message me for discussion:
- Genetic
- Kotlin extension
- Kotlin function type
- RxJava
- Retrofit
- Okhttp
Usually, when integrating API requests with a token mechanism in an application,
if no corresponding processing is done, the actual execution process might be as follows:
App network request -> Token expired -> Server returns access expired -> App handles the error accordingly
In this situation,
even though error handling is done,
every time the token expires,
it will trigger error handling (e.g., notifying the user that the token has timed out),
one or two times might be considered occasional,
but after multiple times,
users might think your application has a problem,
unable to execute smoothly,
thus reducing the user experience,
leading to more subsequent issues.
Therefore,
I hope to implement a similar process as follows,
so that after the token is refreshed,
the original network connection can be retried:
App network request -> Token expired -> Server returns access expired -> Execute token refresh process -> App retries the same network request
In this article, the main method I use for network requests is the RxJava operator,
wrapping Retrofit and applying OkHttp to request network APIs. Here I share a way I request network:
/** | |
* @param paymentRequest 請求資料的model | |
* @param result 一個自行定義的Observer抽象類,主要定義了 onNext 中的資料判斷處理 | |
* | |
* 這是一種用來網路請求的最終方式 | |
* 使用時輸入請求body 與 實作抽象類 RetrofitNoKeyResultObserver | |
* 這邊的repo 是接 Retrofit create後的結果 | |
* 然後 getPaymentData 會是 Retrofit內的interface | |
* 兩者互接後會返回一個Obserable <T> 的結果*/ | |
override fun getPaymentDataApi(paymentRequest: PaymentRequest, result: RetrofitNoKeyResultObserver<PaymentResult>) { | |
repo.getPaymentData(paymentRequest) | |
.subscribeOn(Schedulers.io()) | |
.observeOn(AndroidSchedulers.mainThread()) | |
.subscribe(result) | |
} |
If someone uses RxJava to handle network requests,
they usually use an Rx operator to control it.
Here,
I use Observable. And in the above code:
repo.getPaymentData(paymentRequest)
The returned result is an Observable.
If you follow the method I mentioned earlier,
the first situation might occur,
that is, after the network request, the token becomes invalid,
only error handling is done and then the token is refreshed,
but the API is not retried.
To solve this problem,
I started researching how to use RxJava to refresh the token during execution,
and use the new token to reconnect to the original API.
Therefore,
I wrote a Kotlin extension function
to achieve this functionality:
/** 用來 重新取得refresh token | |
* @param retryCount : 重試次數 | |
* @param delayInSeconds : 間隔秒數 | |
* @param resetRequest : 因為參數重取 故需要重設request與Observable | |
* | |
* 用法 :在rx操作符Observable後方呼叫該extension, | |
* 並輸入預期重設的rx操作符Observable | |
* */ | |
fun <T> Observable<T>.retryNoKeyWhenError(retryCount: Int = 1, delayInSeconds: Long = 5, resetRequest: () -> Observable<T>): Observable<T> { | |
return flatMap { response -> | |
/**這邊是根據專案內result寫的情況去解析你server respone的資料*/ | |
val noKeyResult = Gson().fromJson(Gson().toJson(response), NoKeyResult::class.java) | |
/**這邊判斷你的返回state是什麼,進而執行對應的處理*/ | |
when (noKeyResult.result?.state) { | |
/**正確直接繼續*/ | |
State.SUCCESS.value -> Observable.just(response) | |
/**當是簽章錯誤時 重新獲取*/ | |
State.FAIL_SIGNATURE_ERROR.value, | |
State.FAIL_SIGNATURE_EXPIRED.value, | |
State.FAIL_KEY_TOKEN_EXPIRED.value -> { | |
getTokenByLogin().flatMap { data -> | |
saveRetryRefreshToken(data) | |
Observable.concat(Observable.just(response), Observable.error(BaseObserver.RetrofitResultException(noKeyResult.result?.state!!, noKeyResult.result?.msg!!))) | |
} | |
} | |
/**其他錯誤 直接拋錯*/ | |
else -> Observable.concat(Observable.just(response), Observable.error(RuntimeException())) | |
} | |
}.retryWhen { errors -> | |
/** zipWith 用來連接兩個事件*/ | |
errors.zipWith( | |
Observable.range(1, retryCount), BiFunction { throwable: Throwable, count: Int -> Pair(throwable, count) }) | |
.flatMap { count: Pair<Throwable, Int> -> | |
/** flatMap用來抓出事件的內容 判斷是錯誤事件後重試*/ | |
if (count.first is BaseObserver.RetrofitResultException && count.second <= retryCount) { | |
/** 關鍵點是這行 , 前有有輸入一個function type ,用來在要擴充的Obserble處來重設預期重設的request data*/ | |
resetRequest.invoke().delay(delayInSeconds, TimeUnit.SECONDS) | |
} else { | |
/** 拋出錯誤訊息*/ | |
Observable.error(count.first) | |
} | |
} | |
} | |
} |
1. When actually applying this extension, it will look like this:
/** | |
* | |
* @param paymentRequest 請求資料的model | |
* @param result 一個封裝Observer的類,主要定義了 onNext 中的資料判斷處理 | |
* 這是應用了上面的 extension後的長相 | |
* 用了function type 的形式去重寫request model 內的值*/ | |
override fun getPaymentDataApi(paymentRequest: PaymentRequest, result: RetrofitNoKeyResultObserver<PaymentResult>) { | |
repo.getPaymentData(paymentRequest) | |
.retryNoKeyWhenError(resetRequest = { | |
repo.getPaymentData(resetRequestToken(paymentRequest)) | |
}) | |
.subscribeOn(Schedulers.io()) | |
.observeOn(AndroidSchedulers.mainThread()) | |
.subscribe(result) | |
} |
It’s very convenient to use,
just add the extension function where you need to re-acquire the Token,
and continue using the original method where you don’t need to re-acquire the Token.
This extension function is very flexible,
just add the following code:
.retryNoKeyWhenError(resetRequest = {
repo.getPaymentData(resetRequestToken(paymentRequest))})
2. Let’s take a look at the extended function separately
I used flapmap to parse the data returned within Observable
and since it's a connection request, the server usually returns the result in a fixed format,
so we parse the request result status here.
If it's a success, then the entire original response is returned to the observer,
if it's an error, you write the handling mode you need based on actual requirements, such as:
Encountering these situations
(this is a self-defined enum class, mainly for signature or token expiration situations, you can define it yourself)
State.FAIL_SIGNATURE_ERROR.value
State.FAIL_SIGNATURE_EXPIRED.value
State.FAIL_KEY_TOKEN_EXPIRED.value
will execute the API to re-acquire the token, save the desired data, and then return the corresponding data, then return the original error situation to the observer.
3. At this point, using retryWhen
Because an error situation was returned earlier,
it will trigger retryWhen,
here I also defined an Observable for the number of retries and the interval in seconds for retries,
it will determine how many seconds to retry once based on the input Int in case of failure.
The most important thing is, the function type used earlier,
plays an important role here, because this is where you write the method you expect to execute after the request fails:
resetRequest.invoke().delay(delayInSeconds, TimeUnit.SECONDS)
Since the return type of the extension is defined as Observable
it can be reconnected to the original subscription, which I think is quite convenient,
for your reference.
Finally,
you can define specific error constants and handling processes based on other situations,
and add corresponding handling logic in this extension function.
No matter what situation you encounter,
as long as you define the corresponding error constants and handling processes in advance,
this extension function can help you execute the corresponding handling.
You can extend this function according to your needs,
making it more suitable for your application scenarios.