There are two ways to encapsulate the Retrofit + collaboration to achieve elegant and fast network requests

Posted by moagrius on Sat, 25 Dec 2021 04:31:32 +0100

objective

  • Simple call and less repetitive code

  • Independent of third-party libraries (only including Retrofit+Okhttp + collaboration)

  • You can start immediately even if you don't understand the collaborative process at all (template code)

  • What does it mean to write Kotlin code in Kotlin's way? Compare the following two codes:

mViewModel.wxArticleLiveData.observe(this, object : IStateObserver<List<WxArticleBean>>() {

    override fun onSuccess(data: List<WxArticleBean>?) {
    }

    override fun onError() {
    }
})
mViewModel.wxArticleLiveData.observeState(this) {

    onSuccess { data: List<WxArticleBean>? ->
    }

    onError {
    }
}

Since you use Kotlin, don't write the interface back in Java. Isn't DSL expression fragrant?

Two methods are provided:

  • Mode 1 has less code. The network request comes with Loading, and there is no need to call Loading manually
  • Mode 2: more thorough decoupling

The design ideas of the two methods are different in decoupling. Depending on the specific requirements, no one is better or worse. According to their own projects, which is more convenient to use.

Encapsulation based on official architecture:

1, Package one

Code example in Activity

Click request network

mViewModel.getArticleData()

Set listening, only listen for successful results, and use default exception handling

mViewModel.wxArticleLiveData.observeState(this) {
    onSuccess { data ->
        Log.i("wutao","The result of the network request is: $data")
    }
}

If you need to handle each callback separately

These callbacks are optional and do not need to be implemented

mViewModel.wxArticleLiveData.observeState(this) {
    onSuccess { data ->
        Log.i("wutao","The result of the network request is: $data")
    }

    onEmpty{
        Log.i("wutao", "The returned data is empty, showing the empty layout")
    }

    onFailed {
        Log.i("wutao", "Background returned errorCode: $it")
    }

    onException { e ->
        Log.i("wutao","This is an exception callback that is not returned in the background")
    }

    onShowLoading {
         Log.i("wutao","Customize the of a single request Loading")
    }

    onComplete {
        Log.i("wutao","End of network request")
    }
}

Request self Loading

Many network requests require Loading. I don't want to write the onShowLoading {} method every time, so easy.

mViewModel.wxArticleLoadingLiveData.observeState(this, this) {
    onSuccess { data ->
		Log.i("wutao","The result of the network request is: $data")
    }
}

The second method of observeState() can pass in the reference of the ui, so that the Loading will be automatically loaded before a single network request, and the Loading will be automatically cancelled if it succeeds or fails.

The above code is in Activity. Let's take a look at ViewModel.

Code example in ViewModel

class MainViewModel{

    private val repository by lazy { WxArticleRepository() }

    val wxArticleLiveData = StateLiveData<List<WxArticleBean>>()

    fun requestNet() {
        viewModelScope.launch {
            repository.fetchWxArticle(wxArticleLiveData)
        }
    }
}

It is very simple to introduce the corresponding data warehouse Repo, and then use the co process to execute the network request method. Take a look at the code in Repo.

Code example in Repository

class WxArticleRepository : BaseRepository() {

    private val mService by lazy { RetrofitClient.service }

    suspend fun fetchWxArticle(stateLiveData: StateLiveData<List<WxArticleBean>>) {
        executeResp(stateLiveData, mService::getWxArticle)
    }  
}
interface ApiService {

    @GET("wxarticle/chapters/json")
    suspend fun getWxArticle(): BaseResponse<List<WxArticleBean>>
}

Get a Retrofit instance and then call the ApiService interface method.

Advantages of encapsulation

  • The code is very concise. There is no need to write thread switching code, and there are not many interface callbacks.

  • With its own Loading status, it is not necessary to manually enable and close Loading.

  • Data driven ui, with LiveData as the carrier, returns the page status and network results to the ui through LiveData.

The project address is shown in:

github.com/ldlywt/Fast... (the branch name is withLoading)

Shortcomings of packaging one

The core idea of encapsulation is that a LiveData runs through the whole network request chain. This is its advantage and disadvantage.

  • Incomplete decoupling violates the idea of "setting clearly defined responsibility boundaries between each module of the application"

  • During LiveData listening, if Loading is required, BaseActivity needs to implement the interface with Loading method.

  • The UI reference was passed in the second parameter of the obserState() method.

  • You can't "see the method as you like". If you just touch it, you will have many questions: why do you need a livedata as the parameter of the method. Where is the return value of the network request?

  • Encapsulation 1 also has one of the biggest defects: for multiple data sources, encapsulation 1 shows a very unfriendly side.

Repository is a data warehouse. The methods of obtaining data in the project are all agreed here. Network data acquisition is only one of them.

If you want to add one to obtain data from the database or cache, it is difficult to change the encapsulation. If you force it to change, it will destroy the encapsulation and is very invasive.

Aiming at the deficiency of package one, package two is optimized.

2, Package II

thinking

  • To solve the above shortcomings, you can't use LiveData as the carrier to run through the whole network request.

  • Remove the ui reference from the Observe() method. Don't underestimate a ui reference. This reference represents that the specific Activity is coupled with Observe, and the Activity also implements the IUiView interface.

  • The network request is separated from the Loading status, and Loading needs to be controlled manually.

  • All methods in the Repository have return values and will return results. There is no need to use livedata as a method parameter.

  • LiveData only exists in the ViewModel, and LiveData does not run through the entire request chain. There is no need to refer to LiveData in the Repository. The code of the Repository is simply to obtain data.

  • It is also very easy to handle for multiple data sources.

  • It has nothing to do with the ui and can be used completely as an independent Lib.

Code in Activity

// Request network
mViewModel.login("username", "password")

// Register listening
mViewModel.userLiveData.observeState(this) {
    onSuccess {data ->
        mBinding.tvContent.text = data.toString()
    }

    onComplete {
        dismissLoading()
    }
}

A ui reference is no longer required in observeState().

In ViewModel

class MainViewModel {

    val userLiveData = StateLiveData<User?>()

    fun login(username: String, password: String) {
        viewModelScope.launch {
            userLiveData.value = repository.login(username, password)
        }
    }
}

Send the data through the setValue or postValue method of livedata.

In Repository

suspend fun login(username: String, password: String): ApiResponse<User?> {
    return executeHttp {
        mService.login(username, password)
    }
}

All the methods in the Repository return the request results, and the method parameters do not need livedata. The Repository can be completely independent.

For multiple data sources

// WxArticleRepository
class WxArticleRepository : BaseRepository() {

    private val mService by lazy {
        RetrofitClient.service
    }

    suspend fun fetchWxArticleFromNet(): ApiResponse<List<WxArticleBean>> {
        return executeHttp {
            mService.getWxArticle()
        }
    }

    suspend fun fetchWxArticleFromDb(): ApiResponse<List<WxArticleBean>> {
        return getWxArticleFromDatabase()
    }
}
// MainViewModel.kt  
private val dbLiveData = StateLiveData<List<WxArticleBean>>()
private val apiLiveData = StateLiveData<List<WxArticleBean>>()
val mediatorLiveDataLiveData = MediatorLiveData<ApiResponse<List<WxArticleBean>>>().apply {
    this.addSource(apiLiveData) {
        this.value = it
    }
    this.addSource(dbLiveData) {
        this.value = it
    }
}

It can be seen that encapsulation II is more in line with the principle of single responsibility. The Repository simply obtains data, and ViewModel processes and sends the data.

Project address:

github.com/ldlywt/Fast... (Master branch)

3, Implementation principle

The data comes from Hongyang God Play Android open API

Back to data structure definition:
{
    "data": ...,
    "errorCode": 0,
    "errorMsg": ""
}

The code gap between package 1 and package 2 is very small. It mainly depends on package 2.

Define data return class

open class ApiResponse<T>(
        open val data: T? = null,
        open val errorCode: Int? = null,
        open val errorMsg: String? = null,
        open val error: Throwable? = null,
) : Serializable {
    val isSuccess: Boolean
        get() = errorCode == 0
}

data class ApiSuccessResponse<T>(val response: T) : ApiResponse<T>(data = response)

class ApiEmptyResponse<T> : ApiResponse<T>()

data class ApiFailedResponse<T>(override val errorCode: Int?, override val errorMsg: String?) : ApiResponse<T>(errorCode = errorCode, errorMsg = errorMsg)

data class ApiErrorResponse<T>(val throwable: Throwable) : ApiResponse<T>(error = throwable)

Based on the base class returned in the background, different status data classes are defined according to different results.

Unified processing of network requests: BaseRepository

open class BaseRepository {

    suspend fun <T> executeHttp(block: suspend () -> ApiResponse<T>): ApiResponse<T> {
        runCatching {
            block.invoke()
        }.onSuccess { data: ApiResponse<T> ->
            return handleHttpOk(data)
        }.onFailure { e ->
            return handleHttpError(e)
        }
        return ApiEmptyResponse()
    }

    /**
     * Non background return error, exception caught
     */
    private fun <T> handleHttpError(e: Throwable): ApiErrorResponse<T> {
        if (BuildConfig.DEBUG) e.printStackTrace()
        handlingExceptions(e)
        return ApiErrorResponse(e)
    }

    /**
     * Return 200, but also judge isSuccess
     */
    private fun <T> handleHttpOk(data: ApiResponse<T>): ApiResponse<T> {
        return if (data.isSuccess) {
            getHttpSuccessResponse(data)
        } else {
            handlingApiExceptions(data.errorCode, data.errorMsg)
            ApiFailedResponse(data.errorCode, data.errorMsg)
        }
    }

    /**
     * Successful and empty data processing
     */
    private fun <T> getHttpSuccessResponse(response: ApiResponse<T>): ApiResponse<T> {
        return if (response.data == null || response.data is List<*> && (response.data as List<*>).isEmpty()) {
            ApiEmptyResponse()
        } else {
            ApiSuccessResponse(response.data!!)
        }
    }

}

The error code processing of the Retrofit coroutine is thrown through exceptions, so try... Catch to catch non-200 error codes. Wrapped into different data class objects.

Extend LiveData and Observer

Judge which data class it is in LiveData Observer() and carry out corresponding callback processing:

abstract class IStateObserver<T> : Observer<ApiResponse<T>> {

    override fun onChanged(apiResponse: ApiResponse<T>) {
        when (apiResponse) {
            is ApiSuccessResponse -> onSuccess(apiResponse.response)
            is ApiEmptyResponse -> onDataEmpty()
            is ApiFailedResponse -> onFailed(apiResponse.errorCode, apiResponse.errorMsg)
            is ApiErrorResponse -> onError(apiResponse.throwable)
        }
        onComplete()
    }

Then extend LiveData and replace the callback callback of java with the DSL expression of kotlin, which is short for code.

class StateLiveData<T> : MutableLiveData<ApiResponse<T>>() {

    fun observeState(owner: LifecycleOwner, listenerBuilder: ListenerBuilder.() -> Unit) {
        val listener = ListenerBuilder().also(listenerBuilder)
        val value = object : IStateObserver<T>() {

            override fun onSuccess(data: T) {
                listener.mSuccessListenerAction?.invoke(data)
            }

            override fun onError(e: Throwable) {
                listener.mErrorListenerAction?.invoke(e) ?: toast("Http Error")
            }

            override fun onDataEmpty() {
                listener.mEmptyListenerAction?.invoke()
            }

            override fun onComplete() {
                listener.mCompleteListenerAction?.invoke()
            }

            override fun onFailed(errorCode: Int?, errorMsg: String?) {
                listener.mFailedListenerAction?.invoke(errorCode, errorMsg)
            }

        }
        super.observe(owner, value)
    }
}

4, Summary

Encapsulation 1: the amount of code is less. You can encapsulate some specific ui related information according to the needs of the project, so that it can be developed faster and used better.

Package 2: decoupling is more thorough and can run independently of the ui module.

In my opinion, framework design mainly serves your own project needs (except open source projects). It is better to comply with the design patterns and design principles, but it doesn't matter if you don't meet them. It's good to be suitable for your own project needs and save your own time.

We use it in our own projects, how light, how fast and how cool it is.

Xiaobian collected and sorted out some learning documents, interview questions, Android core notes and other documents related to Android development from the Internet during his learning and promotion, hoping to help you learn and improve. If you need reference, you can go to me directly CodeChina address: https://codechina.csdn.net/u012165769/Android-T3 Access.

Topics: Java Android network Design Pattern