preface

In App, the general status of network requests can be divided into loading, request error, request success, request success but the data is null. For user experience, different states require different interfaces to be displayed to users, such as reminding of network exceptions and clicking to request again.

Before the project has been using Retrofit+RxJava+OkHttp as the network request framework, RxJava has a good encapsulation of different request states, onSubscribe, onNext, onError, and so on, just need to make the corresponding action in different callback ok.

RxJava is easy to use, but as new technologies emerge, RxJava becomes more and more fungible. Kotlin’s coroutine is such a thing.

This article is based on Jetpack architecture, coroutine +Retrofit+Okhttp as a network request framework, different request states (loading, error, empty, etc.) are encapsulated, so that developers do not have to care about where loading is needed, where error prompts need to be displayed.

Also, there are several pits in the use of Jetpack and coroutines during encapsulation, which are described in this article.

Basic use of coroutines

API:www.wanandroid.com/project/tre… Big Wanandroid from Hongyang

Add dependencies if you need to use coroutines

dependencies {
    implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.9'
}
Copy the code

Before Retrofit2.6.0, we used coroutine, and the data returned after API requests could be packaged with Call or Defeerd. After 2.6, we could return data directly with the modifier suspend, as shown below:

interface ProjectApi {

    @GET("project/tree/json")
    suspend fun loadProjectTree(a): BaseResp<List<ProjectTree>>
}
Copy the code

Because the Jetpack architecture is used, the entire network request is mainly divided into UI, ViewModel, Repository three layers, with LiveData as the medium for communication.

First, the Repository layer makes network requests,

 class ProjectRepo{
    private lateinit var mService: ProjectApi

    init {
        mService = RetrofitManager.initRetrofit().getService(ProjectApi::class.java)
    }

    suspend fun loadProjectTree(a): List<ProjectTree> {
        return mService.loadProjectTree()
    }
 }
Copy the code

You create an apiService using Retrofit and OkHttp, leaving the internal details unexpanded here, and then call loadProjectTree() directly for a network request to return the data. LoadProjectTree () is marked with the suspend keyword, which Kotlin uses to force functions to be called from within a coroutine.

And then the ViewModel layer,

class ProjectViewModel : ViewModel(a){
      //LiveData
      val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
      fun loadProjectTree() {
        viewModelScope.launch(Dispatchers.IO) {
            val data = mRepo.loadProjectTree()
            mProjectTreeLiveData.postValue(data)
        }
    }
}
Copy the code

Create class ProjectViewModel and inherit from ViewModel, create an internal LiveData for UI communication, and create a new coroutine using viewModelScope.launch(Dispatchers.IO). The network request is then executed on the I/O thread and the requested data is notified to the UI using LiveData.

The viewModelScope.launch(dispatchers.io) is mentioned here. The viewModelScope is the scope of a coroutine that is already encapsulated in the ViewModel KTX extension and can be used directly. Dispatchers.IO means that this coroutine is executed on the I/O thread, while launch creates a new coroutine.

And finally, the UI layer,

class ProjectFragment : Fragment {

    override fun initData(a) {
        // To request data, call loadProjectTreemViewModel? .loadProjectTree() mViewModel? .mProjectTreeLiveData? .observe(this, Observer {
            / / update the UI})}Copy the code

The UI layer starts calling the request method of the ViewModel to perform the network request, and LiveData registers an observer, watches the data change, and updates the UI.

At this point, the logic of the network request is basically smooth.

In the case of normal environment, the above request is ok, but the app still has network failure, exception, data is null, the above requirements are not met, and then we start to deal with the data exception.

Network request exception handling

For coroutines exception handling, Android developer’s website also gives the answer (developer. The Android. Google. Cn/kotlin coro…). If an exception occurs, it is ok to perform the corresponding action in the catch. Let’s take a look at the implementation.

class ProjectViewModel : ViewModel(a){
      //LiveData
      val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
      fun loadProjectTree(a) {
        viewModelScope.launch(Dispatchers.IO) {
          try {
                  val data = mRepo.loadProjectTree()
                  mProjectTreeLiveData.postValue(data)
               } catch (e: Exception) {
                    / / exception
                    error(e)
               } finally{}}}}Copy the code

Again in the ViewModel layer, requests to mrepo.loadProjecttree () are added with a try-catch block to alert the user based on the Exception type when an Exception occurs.

At this point, the source of the exception has been found, and you need to alert the user by displaying the exception on the UI layer. We all know that mProjectTreeLiveData uses PostValue to distribute data to the UI, and in the same way, LiveData can also distribute exceptions to the UI.

No sooner said than done.

Network request state encapsulation

1、 [The Error status]

Still in the ViewModel layer, we add a new LiveData for exceptions: errorLiveData

class ProjectViewModel : ViewModel(a){
      / / exception LiveData
      val errorLiveData = MutableLiveData<Throwable>()
      //LiveData
      val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
      fun loadProjectTree(a) {
        viewModelScope.launch(Dispatchers.IO) {
          try {
                  val data = mRepo.loadProjectTree()
                  mProjectTreeLiveData.postValue(data)
               } catch (e: Exception) {
                    / / exception
                    error(e)
                    errorLiveData.postValue(e)
               } finally{}}}}Copy the code

At the UI layer, an observer is registered with errorLiveData, and if there is an exception notification, the UI of the exception is displayed (UI layer code is omitted). This can actually achieve the function we started to want: request success to display the successful interface, failure to display the exception interface. But the problem is that it’s not elegant, because if you have multiple ViewModels, multiple UIs, you have to write errorLiveData on every page, so it’s redundant.

So we can take the public method out, create a BaseViewModel class,

open class BaseViewModel : ViewModel(a){
     val errorLiveData = MutableLiveData<Throwable>()

     fun launch( block: suspend () -> Unit,
          error: suspend (Throwable) -> Unit,
          complete: suspend (a) -> Unit
     ) {
          viewModelScope.launch(Dispatchers.IO) {
               try {
                    block()
               } catch (e: Exception) {
                    error(e)
               } finally {
                    complete()
               }
          }
     }


}
Copy the code

In addition to defining errorLiveData and putting the operation of the new build coroutine into it, the developer simply needs to extend BaseViewModel for each ViewModel and rewrite launch (), so the ViewModel in the above case will look like this:

class ProjectViewModel : BaseViewModel(a){
    
      //LiveData
      val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
      fun loadProjectTree(a) {
        launch(
            {
                val state = mRepo.loadProjectTree()
                mProjectTreeLiveData.postValue(state.data)
            },
            {
                errorLiveData.postValue(it)
            },
            {
                loadingLiveData.postValue(false)})}Copy the code

Similarly, the UI layer can create a BaseFragment abstract class, register observers with errorLiveData in onViewCreated, and take action when an exception is notified.

abstract class BaseFragment<T : ViewDataBinding.VM : BaseViewModel> : Fragment(a){

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState) mViewModel = getViewModel() mViewModel? .errorLiveData? .observe(viewLifecycleOwner, Observer { Log.d(TAG,"onViewCreated: error ")
            showError()
            throwableHandler(it)
        })
    }
}
Copy the code

Each subfragment only needs to inherit BaseFragment, and the specific exception listening is not managed by the developer.

2、 [Loading status]

In addition to the abnormal state, the request is indispensable Loading, here Loading is divided into two kinds, one is the whole page to replace Loading, such as Recyclerview list, you can directly load the whole page first, and then display data; Another is that the data interface is not replaced, but a Loading Dialog is displayed on the upper layer. For example, when clicking login, a Loading is required.

The thinking of Loading and exception handling is consistent, can add a LoadingLiveData in BaseViewModel, data type to a Boolean, at the start of each request LoadingLiveData. PostValue (true), when the end of the request or abnormal, Just LoadingLiveData. PostValue (false). UI layer BaseFragment, you can listen to LoadingLiveData emitted true or false, in order to control Loading display and hide.

The ViewModel layer:

open class BaseViewModel : ViewModel(a){
     / / load
     val loadingLiveData = SingleLiveData<Boolean>()
     / / exception
     val errorLiveData = SingleLiveData<Throwable>()

     fun launch( block: suspend () -> Unit,
          error: suspend (Throwable) -> Unit,
          complete: suspend (a) -> Unit
     ) {
          loadingLiveData.postValue(true)
          viewModelScope.launch(Dispatchers.IO) {
               try {
                    block()
               } catch (e: Exception) {
                    Log.d(TAG, "launch: error ")
                    error(e)
               } finally {
                    complete()
               }
          }
     }
}
Copy the code

The BaseViewModel notifying Loading is displayed at the start of the launch, and the notification of the end of the request is distributed in the finally of the try-catch-finally block.

The UI layer:

abstract class BaseFragment<T : ViewDataBinding.VM : BaseViewModel> : Fragment(a){

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mViewModel = getViewModel()
        //Loading Displays the hidden listenermViewModel? .loadingLiveData? .observe(viewLifecycleOwner, Observer {if (it) {
                //show loading
                showLoading()
            } else {
               
                dismissLoading()
            }
        })
        
        // Request an exception to listenmViewModel? .errorLiveData? .observe(viewLifecycleOwner, Observer { Log.d(TAG,"onViewCreated: error ")
            showError()
            throwableHandler(it)
        })
    }
}
Copy the code

Registers a loading observer. When notification is true, loading is displayed, false is hidden.

3、 [The Empty state]

The status of null data occurs after the request succeeds. In this case, the data can be judged as null directly in the UI layer in the listener on the successful request.

At this point, the basic encapsulation of the network request is complete, but there are several issues that need to be resolved while running the test, such as the fact that a try-catch does not throw an exception when the network is unavailable. The next step is the second encapsulation.

Expose the problem of secondary encapsulation

Problem 1: Network request exception, try-catch does not throw the exception

Because the business scenario is quite complex, relying only on try-catch to obtain exceptions will obviously miss something. In this case, we can directly use the code returned by the server as the basis for the request status. If errorCode=0, the request succeeds. If errorCode=0, the request fails.

Let’s create a new sealed class, ResState, to store the Success and Error states,

sealed class ResState<out T : Any> {
    data class Success<out T : Any> (val data: T) : ResState<T> ()data class Error(val exception: Exception) : ResState<Nothing>()
}
Copy the code

Create a BaseRepository class to code the data returned by a Repository request.

open class BaseRepository(a) {

    suspend fun <T : Any> executeResp(
        resp: BaseResp<T>, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
        errorBlock: (suspend CoroutineScope.() -> Unit)? = null
    ): ResState<T> {
        return coroutineScope {
            if (resp.errorCode == 0) { successBlock? .let { it() } ResState.Success(resp.data) }else {
                Log.d(TAG, "executeResp: error") errorBlock? .let { it() } ResState.Error(IOException(resp.errorMsg)) } } } }Copy the code

When errorCode == 0, set ResState to Success and return the data, errorCode! =0, sets the status to Error and returns Exception. A Repository simply inherits its BaseRepository,

class ProjectRepo : BaseRepository(a){

    suspend fun loadProjectTree(a): ResState<List<ProjectTree>> {
        return executeResp(mService.loadProjectTree())
    }
Copy the code

The return value is wrapped in ResState<>, and the result of the request is passed directly to the executeResp () method, with the corresponding changes made in the ViewModel.

class ProjectViewModel : BaseViewModel(a){
    val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()

    fun loadProjectTree(a) {
        launch(
            {
                val state = mRepo.loadProjectTree()
                // Add ResState
                if (state is ResState.Success) {
                    mProjectTreeLiveData.postValue(state.data)
                } else if (state is ResState.Error) {
                    Log.d(TAG, "loadProjectTree: ResState.Error")
                    errorLiveData.postValue(state.exception)
                }
            },
            {
                errorLiveData.postValue(it)
            },
            {
                loadingLiveData.postValue(false)})}Copy the code

The ViewModel layer adds a ResState judgment, which notifies the UI of the data if it is resstate. Success, and the UI of the exception if it is resstate. Error.

The code value returned by the server is undoubtedly the most accurate judgment.

Problem 2: After errorLiveData registers an observer once, it is notified regardless of whether the request failed or succeeded.

This is a feature of MutableLiveData that is notified whenever a registered observer is in the foreground. So what does this feature affect? I’m listening in on errorLiveData and Toast the different exceptions. If I go to a page and the request succeeds but the errorLiveData still receives the notification, a Toast notification will pop up. The phenomenon is as follows:

Let’s change the MutableLiveData to a single event-responsive liveData, where only one receiver can receive the information and avoid event consumption notifications in a scenario where unnecessary business can be avoided.

class SingleLiveData<T> : MutableLiveData<T> (){

    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {

        if (hasActiveObservers()) {
            Log.w(TAG, "When multiple observers exist, only one will be notified of data updates.")}super.observe(owner, Observer { t ->
            if (mPending.compareAndSet(true.false)) {
                observer.onChanged(t)
            }
        })

    }

    override fun setValue(value: T?) {
        mPending.set(true)
        super.setValue(value)
    }

    @MainThread
    fun call(a) {
        value = null
    }

    companion object {
        private const val TAG = "SingleLiveData"}}Copy the code

Replace MutableLiveData in BaseViewModel with SingleLiveData.

The last

This is the end of the coroutine +Retrofit network request state encapsulation. For Error, Empty and other view switches, click to re-request and other operations, you can step to Github to view. Finally, let’s look at the request effect.

Source code: componentization +Jetpack+ Kotlin + MVVM

2021.5.20 update:

For network encapsulation has been upgraded, more convenient and fast, the specific content please see [Jetpack] coroutine +Retrofit network request state encapsulation actual battle (2)