Traditional network request writing

val builder = OkHttpClient.Builder()
        builder.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
                .cookieJar(...)
                .addInterceptor(...) 
                
valretrofit = retrofit.Builder() .baseUrl(baseUrl) .client(builder.build()) .addCallAdapterFactory(callAdapterFactory) .addConverterFactory(converterFactory) .build() ... .Copy the code
dialog.show()
HttpClientManager.getInstance()
    .getHttpClient()
    .createRetrofitApi(
         URL.ROOT_URL,
         HttpApi::class.java).login("phone"."phoneCode").enqueue(MyCallback() {
		override fun success(a)
		override fun onfailed(a)
	})

Copy the code

Elegant implementation


// Include dialog, request failure unified processing, send one, multiple requests at a time, look comfortable!

http<LoginBean> {
    request { model.login("phone"."phoneCode").waitT() }
    success { codeSuccess.postValue(this)}}Copy the code

Let’s get started ↓↓↓

0. Build HttpClient


class HttpClient() {

    private object Client{
        val builder = HttpClient()
    }

    companion object {
        fun getInstance(a) = Client.builder 
    }

    Caching retrofit for the same ApiService in the same domain will not create retroFit objects repeatedly */
    private val apiMap by lazy {
        ArrayMap<String, Any>()
    }

    private val API_KEY = "apiKey"

    private var interceptors = arrayListOf<Interceptor>()
    private var converterFactorys = arrayListOf<Converter.Factory>()

    /** * interceptor */
    fun setInterceptors(list: MutableList<Interceptor>? : HttpClient {
        interceptors.clear()
        if(! list.isNullOrEmpty()) { interceptors.addAll(list) }return this
    }

    /** * parser */
    fun setConverterFactorys(list: MutableList<Converter.Factory>? : HttpClient {
        converterFactorys.clear()
        // Make sure there is a default parser
        converterFactorys.add(GsonConverterFactory.create())
        if(! list.isNullOrEmpty()) { converterFactorys.addAll(list) }return this
    }

    /** create a different Api based on apiClass and baseUrl@paramBaseUrl String Root directory *@paramClazz Class<T> specific API *@paramNeedAddHeader Boolean Specifies whether to add a public header *@paramShowLog Boolean Whether to display log */
    fun <T> createRetrofitApi(
        baseUrl: String = URL.ROOT_URL,
        clazz: Class<T> = HttpApi::class.java,
        needAddHeader: Boolean = true,
        showLog: Boolean = true
    ): T {
        val key = getApiKey(baseUrl, clazz)
        val api = apiMap[key] as T
        if (api == null) {
            L.e(API_KEY, "RetrofitApi --->>> \"$key\" Does not exist, need to create new")
            val builder = OkHttpClient.Builder()
            builder
                .connectTimeout(Content.HTTP_TIME, TimeUnit.SECONDS)
                .readTimeout(Content.HTTP_TIME, TimeUnit.SECONDS)

            if (needAddHeader) {
                builder.addInterceptor(MyHttpInterceptor()) // Head interceptor
            }
            if (interceptors.isNotEmpty()) {
                interceptors.forEach {
                    builder.addInterceptor(it)
                }
            }
            if (showLog) {
                builder.addInterceptor(LogInterceptor {
                    L.i(Content.HTTP_TAG, it)
                }.apply {
                    level = LogInterceptor.Level.BODY
                })
            }
            val rBuilder = Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(builder.build())
            if (converterFactorys.isEmpty()) { // Make sure there is a default parser
                converterFactorys.add(GsonConverterFactory.create())
            }
            converterFactorys.forEach {
                rBuilder.addConverterFactory(it)
            }
            val newAapi = rBuilder.build().create(clazz)
            apiMap[key] = newAapi
            return newAapi
        }
        return api
    }

    override fun <K> getApiKey(baseUrl: String, apiClass: Class<K>) =
        "apiKey_${baseUrl}_${apiClass.name}"

    /** * Clean up all interceptors */
    fun clearInterceptor(a) : HttpClient {
        interceptors.clear()
        return this
    }

    /**
     * 清空所有解析器
     */
    fun clearConverterFactory(a) : HttpClient {
        converterFactorys.clear()
        return this
    }

    /** * Clear all API caches */
    fun clearAllApi(a) : HttpClient {
        L.e(Content.HTTP_TAG, "Clear all API caches.")
        apiMap.clear()
        return this}}/** * HttpApi */
interface HttpApi {

    // This is not recommended, because the business logic for each project is not exactly the same. Customization is highly controllable (see below).
    suspend fun sth(a): CommonBean<String>
	
    // This method can be used without a coroutine. This method is recommended
    fun login(@Body body: RequestBody): Call<CommonBean<LoginBean>>

}

// Write the default method of getting HttpApi in HttpApi
val defaultApi: HttpApi
    get() {
    	return HttpClient.getInstance().createRetrofitApi()
    }

Copy the code

1. WaitT extension function


// Exception formatting
class ApiException : Exception {

    var msg: String? = "The Internet is not helping, try again!"
    var errorCode = Content.REQUEST_SUCCESS_CODE

    private constructor(code: Int, throwable: Throwable) : super(throwable) {
        msg = throwable.message
        errorCode = code
    }

    constructor(message: String? , code:Int = _500_SERVICE) : super(message) {
        msg = message
        errorCode = code
    }

    companion object {

        const val _500_SERVICE = 500
        const val UNKNOW_ERROR = -1

        fun formatException(e: Throwable): ApiException {
            val apiException: ApiException
            when (e) {
                is HttpException -> {
                    apiException = ApiException(e.code(), e)
                    apiException.msg = "The Internet is not helping, try again!"
                }
                is SocketTimeoutException -> {
                    apiException = ApiException(_500_SERVICE, "The Internet is not helping, try again!")}... Other Exception handlingis ApiException -> {
                    apiException = e
                }
                else -> {
                    apiException = ApiException(UNKNOW_ERROR, e)
                    apiException.msg = "Unknown exception, please contact administrator."}}return apiException
        }

}

/** * request base class */
@Parcelize
class CommonBean<T>(
    var success: Boolean = false.var result: @RawValue T? = null.var error: ErrorBean? = null.var message: String? = "",
) : Parcelable, Serializable

/** * Error returns entity class *@property errorCode Int
 * @property errorKey String?
 * @property errorMessage String
 * @constructor* /
@Parcelize
data class ErrorBean(
    var errorCode: Int = -1.var errorKey: String? = "".var errorMessage: String = ""
) : Parcelable, Serializable

// This method is customized according to the needs of the project, and is 100% autonomous and controllable
suspend fun <T> Call<CommonBean<T>>.waitT(a): CommonBean<T> {
    return suspendCoroutine {

        enqueue(object : Callback<CommonBean<T>> {

            override fun onResponse(
                call: Call<CommonBean<T>>,
                response: Response<CommonBean<T>>
            ) {
                val body = response.body()
                if (body is ResponseBody) { / / ResponseBody situation
                    if (response.isSuccessful) {
                        it.resume(body)
                    } else {
                        it.resumeWithException(ApiException("The Internet doesn't work. Let's try again."))}}else { // Other entity classes
                    if (response.isSuccessful) { // The request succeeded
                        valisSuccess = body? .success ? :false // Business logic OK
                        if(body ! =null && isSuccess) { // Business logic OK
                            it.resume(body)
                        } else { // The request succeeds but the business logic is incorrect
                            valerrorBean = body? .error it.resumeWithException(ApiException(errorBean? .errorMessage)) } }else { // The exception thrown by the server is determined according to the business logic. For example, in my case, I must separately handle the exception of 401
                        if (response.code() == 401) { 
                            // The server throws the 401 exception
                            it.resumeWithException(ApiException("Current account is logged in from another device".401))}else {
                            it.resumeWithException(ApiException("The Internet doesn't work. Let's try again.")}}}}override fun onFailure(call: Call<T_CommonBean<T>>, t: Throwable) {
                t.printStackTrace()
                L.e(Content.HTTP_TAG, "OnFailure interface exception -->${call.request().url()}")
                it.resumeWithException(ApiException.formatException(t))
            }
        })
    }
}
Copy the code

2. BaseViewModel


// Register in BaseActivity or BaseFragment
class BaseViewModel : ViewModel() {

	/** * Dialog Network request */
	data class DialogRespBean(
	    var title: String? = "Loading".var isCancelable: Boolean = true
	)
	/** * Network request failed to respond to Bean * on UI@propertyState Int distinguishes between refreshing (loading) and loading more *@propertyMessage String Error description *@constructor* /
	data class NetErrorRespBean(
	    var state: Int = Content.REFRESH,
	    var message: String? = "The Internet doesn't work. Let's try again."
	)

    /** * Displays dialog */
    val showDialog by lazy {
        MutableLiveData<DialogRespBean>()
    }

    /** * Destroy dialog */
    val dismissDialog by lazy {
        MutableLiveData<Void>()
    }
     
    /** * Network request error */
    val networkError by lazy {
        MutableLiveData<NetErrorRespBean>()
    }

    /** * Stop all operations */
    val stopAll by lazy {
        MutableLiveData<Void>()
    }

    /** * The current account is logged in to another device */
    val loginOut bylazy { MutableLiveData<String? > ()}/** * Token is invalid or login time has expired */
    val tokenError bylazy { MutableLiveData<String? > ()}// Look down
    open fun lunchByIO(
        context: CoroutineContext = IO,
        block: suspend CoroutineScope. () - >Unit
    ) = viewModelScope.launch(context) { block() }
    
}
Copy the code

3. HttpExtend with DSL

The preparatory work

val IO: CoroutineContext
    get() {
        return Dispatchers.IO
    }

val Main: CoroutineContext
    get() {
        return Dispatchers.Main
    }

/** * Switch to the IO thread */
suspend fun IO(block: suspend CoroutineScope. () - >Unit) {
    withContext(IO) {
        block()
    }
}

/** * Switch to the UI thread */
suspend fun UI(block: suspend CoroutineScope. () - >Unit) {
    withContext(Main) {
        block()
    }
}

@Parcelize
data class HttpLoader(
    var state: Int = Content.REFRESH, // To distinguish between refresh and load more
    var showDialog: Boolean = true.// Ask whether to display dialog
    var autoDismissDialog: Boolean = true.// Whether to automatically destroy the dialog after the request succeeds
    var dialogTitle: String = "Loading".// dialog title
    var dialogCancel: Boolean = true // dialog whether to cancel
) : Parcelable

// This is the processing of single event MutableLiveData, which is not the focus of this article, so it is simply skipped and can be directly postValue
fun <T> MutableLiveData<T>.clear(a) {
    value = null
}

Copy the code

HttpExtend


internal fun <T> BaseViewModel.http(block: HttpExtend<T>. () - >Unit) {
    val httpExtend = HttpExtend<T>(this)
    block.invoke(httpExtend)
}

internal fun BaseViewModel.http2(block: HttpExtend<Nothing>. () - >Unit) {
    http(block)
}

/** Send one */ at a time
internal fun <T> HttpExtend<T>.request(startBlock: suspend CoroutineScope. () - >CommonBean<T>? {
    start(startBlock)
}

/** Send multiple */ at a time
>>> UI {} */
internal fun HttpExtend<Nothing>.request2(startBlock: suspend CoroutineScope. () - >Unit) {
    start2(startBlock)
}

internal fun <T> HttpExtend<T>.loading(loaderBlock: () -> HttpLoader) {
    dialog(loaderBlock)
}

internal fun <T> HttpExtend<T>.success(resultBlock: T? . () - >Unit) {
    callback(resultBlock)
}

internal fun <T> HttpExtend<T>.failed(errorBlock: Exception? . () - >Unit) {
    error(errorBlock)
}

internal fun <T> HttpExtend<T>.finally(finaly: () -> Unit) {
    end(finaly)
}

class HttpExtend<T>(var viewModel: BaseViewModel) {

    private var httpLoader = HttpLoader()
	// Request a successful callback
    private varhttpCallBack: (T? . () - >Unit)? = null
    private varhttpError: (Exception? . () - >Unit)? = null
    private var httpFinally: (() -> Unit)? = null

    infix fun dialog(httpLoader: () -> HttpLoader) {
        this.httpLoader = httpLoader()
    }

    private fun showDialog(a) {
        if (httpLoader.showDialog) viewModel.showDialog.postValue(
            DialogRespBean(
                httpLoader.dialogTitle,
                httpLoader.dialogCancel
            )
        )
    }
	
    // Request one at a time
    infix fun start(startBlock: suspend CoroutineScope. () - >T_CommonBean<T>? {
        showDialog()
        viewModel.lunchByIO {
            try {
                valrequest = startBlock() UI { httpCallBack? .invoke(request? .result) } }catch (e: Exception) {
                callError(e)
            } finally {
                callFinally()
            }
        }
    }

    // Request more than one at a time
    infix fun start2(startBlock: suspend CoroutineScope. () - >Unit) {
        showDialog()
        viewModel.lunchByIO {
            try {
                startBlock()
            } catch (e: Exception) {
                callError(e)
            } finally {
                callFinally()
            }
        }
    }

    // Request a callback
    infix fun callback(resultBlock: T? . () - >Unit) {
        httpCallBack = resultBlock
    }

    // Request failure processing
    infix fun error(errorBlock: Exception? . () - >Unit) {
        httpError = errorBlock
    }

    // will be called regardless of whether the request succeeds or fails
    infix fun end(end: () -> Unit) {
        httpFinally = end
    }

    Of course, you can also use a sealed class
    private suspend fun callError(e: Exception) {
        e.printStackTrace()
        UI {
            // The exception association that the waitT extension function throws
            val apiException = ApiException.formatException(e)
            // It depends on the business logic
            when (apiException.errorCode) {
                401 -> {
                    L.e(Content.HTTP_TAG, "CallError --> Token invalid or login time expired")
                    viewModel.tokenError.postValue(apiException.msg)
                }
                888- > {// The current account is used to log in to another device
                    L.e(Content.HTTP_TAG, "CallError --> Current account is logged in to another device")
                    viewModel.loginOut.postValue(apiException.msg)
                }
                else- > {// Normal server request failure processing
                    L.e(Content.HTTP_TAG, "CallError --> Request failed")
                    viewModel.networkError.postValue(
                        NetErrorRespBean(
                            httpLoader.state,
                            apiException.msg
                        )
                    )
                    httpError?.invoke(apiException)
                }
            }
            viewModel.dismissDialog.clear() // There was a crash, destroy the dialog anyway}}// Finally execute
    private suspend fun callFinally(a) {
        UI {
            if(httpLoader.autoDismissDialog && httpLoader.showDialog) { viewModel.dismissDialog.clear() } httpFinally? .invoke() } } }Copy the code

4. Final call


class LoginModel() {
    fun login(phone:String,phoneCode:String) = defaultApi.login(phone,phoneCode)

    fun registrId(id:String) = defaultApi.registrId(id)

    fun getUserInfo(id:String) = defaultApi.getUserInfo(id)
}

class LoginViewModel : BaseViewModel() {

    val loginSuccess by lazy {
        MutableLiveData<UserInfoBean>()
    }
    
    private val model by lazy {
        LoginModel()
    }

    // Send one at a time
    fun login(phone:String,phoneCode:String) {
        http<LoginBean> {
            request { model.login("phone"."phoneCode").waitT() }
            success { loginSuccess.postValue(this)}}}// Send multiple at a time
    fun login2(phone:String,phoneCode:String) {
        http2 {
            loading { HttpLoader(showDialog = false, autoDismissDialog = false) }
            request2 {
                val loginBean = model.login("phone"."phoneCode").waitT()
                val registrBean = model.registrId(loginBean.id).waitT()
                val userBean = model.getUserInfo(registrBean.id).waitT()
                // The request succeeded
                UI {
                    loginSuccess.postValue(userBean)
                    dismissDialog.clear()
                }
            }
            failed {
                dismissDialog.clear()
            }
            finally { 
                // do sth}}}}Copy the code

The most important thing is the waitT and HttpExtend exception handling. —>>> demo