A typical scenario in Android development is retry after a failed network request: the general logic is to pop up a Dialog to alert the user to “failed network request” and provide a button to retry.

If the current page has only one network request, the logic is simple: just call the method that initiated the network request again. When there are multiple network requests on a page, MY common approach is to add state to the failed callback and call different methods based on different states. But this approach is a bit cumbersome and insecure. First, you need to add extra states and pass them back and forth. In some cases, you may even need to reinitialize the network request parameters. Even worse: you have to manage this state, and if you don’t manage it well, you’ll end up calling methods that shouldn’t be called and introducing serious bugs.

Until one day I saw CoroutineExceptionHandler, languid, can use coroutines context (can be in my previous blog Kotlin coroutines, context and scope to know more information about coroutines and context) to save the future may need to retry network Request and the Request data, That would solve the problem above.

Since most of the projects I work on use the ViewModel to decouple the network request logic from the UI layer, the network request is basically implemented by Coroutine+Retrofit, which is basically implemented using the viewModelScope.

viewModelScope.launch() { 
	request()
}
Copy the code

ViewModelScope is essentially an extension function of the ViewModel. It can be used to easily create coroutines in the ViewModel without expanding the specific code. By default, its CoroutineContext consists of a Job and a CoroutineDispatcher. The context of a coroutine is essentially a linked list structure that implements key-value access. We can through inheritance AbstractCoroutineContextElement way to implement custom CoroutineContext context:

class RetryCallback(val callback: () -> Unit) : AbstractCoroutineContextElement(RetryCallback) {
    companion object Key : CoroutineContext.Key<RetryCallback>
}
Copy the code

Then, when the network request exception with CoroutineExceptionHandler get we need to perform the operation:

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> val callback = coroutineContext[RetryCallback] ? .callback }Copy the code

Then, to add coroutineExceptionHandler to launch network request coroutines context:

viewModelScope.launch(exceptionHandler
      + RetryCallback { request() }) { 
	request()
}
Copy the code

At this point, the logic of retry can be implemented as long as the callback is retrieved from the page that initiated the network request and invoked when the retry button is clicked.

Encapsulate it further and add automatic retry after failure logic, creating an interface for the ViewModel to handle subsequent logic for network request errors:

Interface ViewModelFailed {/** * @param callback: throwable {/** * @param callback: throwable: Throwable, callback: () -> Unit) }Copy the code

Create an extension function for it, is used to create CoroutineExceptionHandler and RetryCallback context instances:

/ * * * @ param autoReTry: whether automatic retry * @ param callback: need to retry the function * * / fun ViewModelFailed. InitRetry (autoReTry: Boolean = false, callback: () -> Unit) = CoroutineExceptionHandler { coroutineContext, throwable -> val retryCallBack = { coroutineContext[RetryCallback] ? .callback? .invoke()} if (autoReTry) {// Invoke () retryCallback.invoke ()} else {// Do not automatically retry, RequestFailed (throwable) {retryCallBack}}} + retryCallBack (callback)Copy the code

The ViewModel needs to implement the ViewModelFailed interface and add exception handling context by calling the initRetry method in the coroutine that initiates the network request:

class MainViewViewModel : ViewModel(), ViewModelFailed { val liveData: MutableLiveData<BaseData> = MutableLiveData() /** * @param num: used to demonstrate Request data * @param repeat: number of automatic retries after a failure ** / fun request(num: Int, repeat: Int = 0) { liveData.value = BaseData.loading() viewModelScope.launch(initRetry(repeat > 0) { request(num,repeat - 1) }) { liveData.value = BaseData.success(simulateHttp(num)) } } private suspend fun simulateHttp(num: Int) = withContext(dispatchers.io) {// Simulate network request... } override fun requestFailed(throwable: Throwable, callback: () -> Unit) {callback () {override fun onRetry() {}}Copy the code

Finally, attach the complete code and use Demo