One, foreword

A few days ago, I posted a Jetpack coroutine +Retrofit web request status encapsulating practice, and received some feedback from my colleagues in the comments section:

.

Specific questions can be directly moved to the comments section of the previous post to view.

Because there are several important points, so on the last article added some content, mainly as follows:

  • ✅ Added local status management. Like a page of multiple interfaces, can be managed separately state switching;
  • ✅ UI layer added Error, Empty, Success Callback, developers can freely choose whether to listen, processing business logic more intuitive, convenient;
  • ✅ Unified UI switchover with the third-party library loadSir.
  • ✅ request invocation is simpler

Okay, let’s get started.

Second, local request state management

In many cases, there are different interfaces on the same interface in app development. Two interfaces request at the same time, one succeeds and the other fails. At this time, the successful interface continues to display its own page, while the failed interface displays an Error prompt interface, as shown in the figure below

ErrorLiveData and loadingLiveData are globally encapsulated in BaseFragment, and they are also created in BaseViewModel. This makes it impossible to tell where the error comes from if one interface sends an error when multiple interfaces request it simultaneously.

If you want each interface to manage its own state separately, then you need to create multiple erroeLiveData in the ViewModel, which is a good solution, but makes the code very redundant. Since you need each interface to manage different states, you can create a new LiveData that contains both the return result of the request and the different state values, and call it StateLiveData

/** * MutableLiveData, which distributes the request status to UI */
class StateLiveData<T> : MutableLiveData<BaseResp<T> > (){}Copy the code

In addition to requesting the public JSON for the return value, BaseResp needs to add different status values. We divide the state values into (STATE_CREATE,STATE_LOADING,STATE_SUCCESS,STATE_COMPLETED,STATE_EMPTY,STATE_FAILED, Several STATE_ERROR, STATE_UNKNOWN)

enum class DataState {
    STATE_CREATE,/ / create
    STATE_LOADING,/ / load
    STATE_SUCCESS,/ / success
    STATE_COMPLETED,/ / finish
    STATE_EMPTY,// The data is null
    STATE_FAILED,// The interface request succeeds but the server returns error
    STATE_ERROR,// The request failed
    STATE_UNKNOWN/ / unknown
}
Copy the code

Add DataState to BaseResp,

/** * The basic type returned by json */
class BaseResp<T>{
    var errorCode = -1
    var errorMsg: String? = null
    var data: T? = null
        private set
    var dataState: DataState? = null
    var error: Throwable? = null
    val isSuccess: Boolean
        get(a) = errorCode == 0
}
Copy the code

So how do you use StateLiveData?

We all know that data requests can have different results, such as success, exception, or null, so we can use the different results to set the corresponding state in the BaseResp DataState. Go directly to the data request Repository layer and make improvements to the exception handling in the previous section.

open class BaseRepository {
    /** * Repo requests a public method for data, * sets the value of baseresp. dataState in different states, and then notifies the UI of the dataState status */
    suspend fun <T : Any> executeResp(
        block: suspend () -> BaseResp<T>,
        stateLiveData: StateLiveData<T>
    ) {
        var baseResp = BaseResp<T>()
        try {
            baseResp.dataState = DataState.STATE_LOADING
            // Start requesting data
            val invoke = block.invoke()
            // Copy the result to baseResp
            baseResp = invoke
            if (baseResp.errorCode == 0) {
                // If the request succeeds, check whether the data is empty.
                // Since there are many types of data, you need to set the type to determine
                if (baseResp.data == null || baseResp.data is List<*> && (baseResp.data as List<*>).size == 0) {
                    //TODO:If the data is null, you need to modify the null condition when the structure changes
                    baseResp.dataState = DataState.STATE_EMPTY
                } else {
                    // If the request succeeds and the data is empty, STATE_SUCCESS is specified
                    baseResp.dataState = DataState.STATE_SUCCESS
                }

            } else {
                // Server request error
                baseResp.dataState = DataState.STATE_FAILED
            }
        } catch (e: Exception) {
            // Non-background return error, caught exception
            baseResp.dataState = DataState.STATE_ERROR
            baseResp.error = e
        } finally {
            stateLiveData.postValue(baseResp)
        }
    }
}
Copy the code

ExecuteResp () is the public method for data requests, which is passed two parameters, the first one taking the data request function as an argument, and the second one being StateLiveData, which was created above.

The BaseResp() method starts by creating a new BaseResp() object, setting the datastate.state_loading state to the BaseResp DataState, and then starts to exception the data request (see the previous article). If code=0, the interface request succeeded. Otherwise, the interface request succeeds, but the server returns an error. When code=0, the returned data is judged to be empty. Since the data has multiple types, you need to set the type for judgment. If the type is empty, the state is set to DataState

DataState STATE_SUCCESS. If an exception is thrown, the state is set to datastat.state_ERROR, and the baseResp with the state is distributed to the UI using stateLiveData after the request ends.

At this point, the request status has been set, and the next step is to start the interface switching process according to the different status.

3. Switch to the LoadSir interface

LoadSir is a load feedback page management framework, the status page automatically switch, the specific use of the description here, you can step github view.

When LiveData receives a change in data, the UI registers an Observer that receives the event. When the requested data is received, the UI is updated. In section 2, different states are added to the data.

/** * A LiveData Observer class, * LoadSir, * LoadSir, * LoadSir, * LoadSir, * LoadSir, * LoadSir, * LoadSir; OnDataEmpty, onError, * Developers can create an IStateObserver at the UI layer for each interface request and override the corresponding callback. * /
abstract class IStateObserver<T> (view: View?). :Observer<BaseResp<T> >,Callback.OnReloadListener {
    private var mLoadService: LoadService<Any>? = null

    init {
        if(view ! =null) {
            mLoadService = LoadSir.getDefault().register(view, this,
                Convertor<BaseResp<T>> { t ->
                    var resultCode: Class<out Callback> = SuccessCallback::class.java

                    when (t? .dataState) {

                        // Data is just starting to request, loading
                        DataState.STATE_CREATE, DataState.STATE_LOADING -> resultCode =
                            LoadingCallback::class.java
                        // The request succeeded
                        DataState.STATE_SUCCESS -> resultCode = SuccessCallback::class.java
                        // The data is empty
                        DataState.STATE_EMPTY -> resultCode =
                            EmptyCallback::class.java
                        DataState.STATE_FAILED ,DataState.STATE_ERROR -> {
                            val error: Throwable? = t.error
                            onError(error)
                            // You can set the UI for the error interface depending on the type of error
                            if (error is HttpException) {
                                // Network error
                            } else if (error is ConnectException) {
                                // No network connection
                            } else if (error is InterruptedIOException) {
                                // The connection timed out
                            } else if (error is JsonParseException
                                || error is JSONException
                                || error is ParseException
                            ) {
                                // Error parsing
                            } else {
                                // Unknown error
                            }
                            resultCode = ErrorCallback::class.java
                        }
                        DataState.STATE_COMPLETED, DataState.STATE_UNKNOWN -> {
                        }
                        else -> {
                        }
                    }
                    Log.d(TAG, "resultCode :$resultCode ")
                    resultCode
                })
        }

    }


    override fun onChanged(t: BaseResp<T>) {
        Log.d(TAG, "onChanged: ${t.dataState}")

        when (t.dataState) {
            DataState.STATE_SUCCESS -> {
                // The request succeeded, but the data was not null
                onDataChange(t.data)
            }

            DataState.STATE_EMPTY -> {
                // The data is empty
                onDataEmpty()
            }

            DataState.STATE_FAILED,DataState.STATE_ERROR->{
                // Request errort.error? .let { onError(it) } }else- > {}}// Load the different state interface
        Log.d(TAG, "onChanged: mLoadService $mLoadService") mLoadService? .showWithConvertor(t) }/** * Requests data that is not empty */
    open fun onDataChange(data: T?) {}/** * The request succeeded, but the data is empty */
    open fun onDataEmpty(a) {}/** * Request error */
    open fun onError(e: Throwable?) {}}Copy the code

IStateObserver is the implementation class of the Observer interface. The argument is passed to a View, and this View is the interface you want to replace. This is the key to the fact that the same interface can be displayed differently in different modules. Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir: Loadsir? Empty is set to EmptyCallback, and when that is done, showWithConvertor is called uniformly in the onChanged callback, which is the interface toggle operation.

In the onChange callback, the onDataChange, onDataEmpty, and onError notifications are also distributed according to the status value.

At this point, the switchover between different status interfaces and the distribution of status notifications are completed.

4. How to use it

Above basically will complete the entire process package, the use is relatively simple.

The Repository layer:

class ProjectRepo() : BaseRepository(a){
      suspend fun loadProjectTree(stateLiveData: StateLiveData<List<ProjectTree>>) {
        executeResp({mService.loadProjectTree()},stateLiveData)
    }
}
Copy the code

Just one line of code, the incoming API request in the executeResp method, and StateLiveData.

The ViewModel layer:

class ProjectViewModel : BaseViewModel(a){  
    val mProjectTreeLiveData = StateLiveData<List<ProjectTree>>()
        
     fun loadProjectTree(a) {
        viewModelScope.launch(Dispatchers.IO) {
            mRepo.loadProjectTree(mProjectTreeLiveData)
        }
    }
Copy the code

The call is still a line of code that creates a new StateLiveData, and then calls the Repository layer network request directly in the viewModelScope scope, passing in StateLiveData as an argument.

The UI layer:

class ProjectFragment : BaseFragment<FragmentProjectBinding.ProjectViewModel> (){
      override fun initData(a) { mViewModel? .loadProjectTree() mViewModel? .mProjectTreeLiveData? .observe(this, object : IStateObserver<List<ProjectTree>>(mBinding? .rvProjectAll) {override fun onDataChange(data: List<ProjectTree>?) {
                    super.onDataChange(data)
                    Log.d(TAG, "onDataChange: ") data? .let { mAdapter.setData(it) } }override fun onReload(v: View?) {
                    Log.d(TAG, "onReload: ") mViewModel? .loadProjectTree() }override fun onDataEmpty(a) {
                    super.onDataEmpty()
                    Log.d(TAG, "onDataEmpty: ")}override fun onError(e: Throwable?) {
                    super.onError(e) showToast(e? .message!!) Log.d(TAG,"onError: ${e? .printStackTrace()}")}})}}Copy the code

The UI layer registers observers using The ViewModel’s StateLiveData, and what’s different is mViewModel? .mProjectTreeLiveData? The second argument to.observe () is replaced with an IStateObserver, and a View is passed. This View represents the UI you want to replace if the request is abnormal.

  • OnDataChange: Request succeeded, data is not empty;
  • OnReload: Click to rerequest;
  • OnDataEmpty: when the data is empty;
  • OnError: The request failed

Developers can choose to listen according to their own business needs.

Let’s see what happens.

Five, the last

This integration makes up for some details, more in line with the App development logic, of course, each App business is different, this is the developer to customize some request details, but coroutine +Retrofit network request is the general idea. More detailed code can be found on Github

Source code: componentization +Jetpack+ Kotlin + MVVM

Please combine [Jetpack] coroutine +Retrofit network request state encapsulation combat