preface

  • This article does not discuss the principles and basic uses of coroutines, Retrofit, and MVVM. If you need it, you can find good articles from other bloggers.
  • DataBinding’s bidirectional binding was not chosen for this article because I felt that DataBinding contaminates THE XML and is difficult to locate errors.
  • Frameworks such as Flux, Redux, and ReKotlin are not used because they are not well known.
  • This article can be regarded as a summary of the implementation process, welcome to share, suggestions.

Process and Thinking

Basic dependence

  • Life cycle component correlation
implementation 'androidx. Lifecycle: lifecycle - extensions: 2.2.0 - beta01'
implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0 - beta01"
Copy the code
  • coroutines
implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.0"
Copy the code
  • network
implementation 'com. Squareup. Retrofit2: retrofit: server'
implementation 'com. Squareup. Retrofit2: converter - gson: server'
implementation 'com. Squareup. Okhttp3: logging - interceptor: 3.11.0'
Copy the code

Note :Retrofit has a friendlier implementation of coroutines since 2.6, so version selection is a requirement.

Before you begin

Because of the access to coroutines, callbacks like onResponse and onFailure do not fit the design of coroutines. The Kotlin coroutine throws Retrofit’s onFailure handling directly as a Trowable, so start by building a try.. The catch is designed.

Basic network access encapsulation

The basic operations still need to be done

abstract class BaseRetrofitClient {

    companion object CLIENT {
        private const val TIME_OUT = 5
    }

    protected val client: OkHttpClient
        get() {
            val builder = OkHttpClient.Builder()
            val logging = HttpLoggingInterceptor()
            if (BuildConfig.DEBUG) {
                logging.level = HttpLoggingInterceptor.Level.BODY
            } else {
                logging.level = HttpLoggingInterceptor.Level.BASIC
            }
            builder.addInterceptor(logging)
                .connectTimeout(TIME_OUT.toLong(), TimeUnit.SECONDS)
            handleBuilder(builder)
            return builder.build()
        }
        
    /** * so that the builder can be extended */
    abstract fun handleBuilder(builder: OkHttpClient.Builder)

    open fun <Service> getService(serviceClass: Class<Service>, baseUrl: String): Service {
        return Retrofit.Builder()
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(baseUrl)
            .build()
            .create(serviceClass)
    }
}
Copy the code

Define the basic Api return classes

/* Server returns a number of plays */
data class ApiResponse<out T>(val code: Int./*val errorMsg: String? , * / val data: T?)
/* Login receipt */
data class LoginRes(val token: String)
Request / * * /
data class LoginReq(val phoneNumber: String, val password: String)

Copy the code

Define an Api to facilitate testing

interface UserApi {

    companion object {
        const val BASE_URL = "https://xxx.com"      // You can find some public apis to test
    }

    @POST("/auth/user/login/phone")
    suspend fun login(@Body body: RequestBody): ApiResponse<LoginRes>

}

Copy the code

Encapsulation BaseViewModel

Network requests must be made in child threads, as is common sense in Android development. Using coroutines for network requests makes asynchronous code appear to be executed synchronously, which greatly improves code readability, but it does take time to understand hangs. The final thing in the BaseViewModel is to set up a try.. for the Retrofit network request code block about coroutines. The catch.

  • Important to have a try.. catch
/ * * *@paramThe pending code block that tryBlock attempts to execute@paramCatchBlock catch exception code block "the implementation of the coroutine to Retrofit does not have onFailure callback onFailure, but is Throwable directly" *@paramFinallyBlock finally code block */
private suspend fun tryCatch(
    tryBlock: suspend CoroutineScope. () -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit,
    finallyBlock: suspend CoroutineScope.() -> Unit
) {
    coroutineScope {
        try {
            tryBlock()
        } catch (e: Throwable) {
            catchBlock(e)
        } finally {
            finallyBlock()
        }
    }
}
Copy the code

Lower down the exceptions caught to ensure that everything is under control during execution.

  • The main thread
/** * it is not necessary to enable catchBlock and finallyBlock in the main thread. Different services may handle errors differently
fun launchOnMain(
    tryBlock: suspend CoroutineScope. () -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit = {},             // Default empty implementation, can be changed according to the situation
    finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
    viewModelScope.launch {
        tryCatch(tryBlock, catchBlock, finallyBlock)
    }
}
Copy the code
  • IO thread
/** */ dispatchers. IO */
fun launchOnIO(
    tryBlock: suspend CoroutineScope. () -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit = {},
    finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
    viewModelScope.launch(Dispatchers.IO) {
        tryCatch(tryBlock, catchBlock, finallyBlock)
    }
}
Copy the code
  • Don’t forget onCleared
override fun onCleared(a) {
    super.onCleared()
    viewModelScope.cancel()
}
Copy the code

Error handling

Error handling is divided into 1. Request exceptions (and exceptions in trycatch),2. The exceptions defined in the response body returned by the server are common on APP with NetWork access properties. Therefore, I define a NetWorkError. Kt file for NetWork exception handling, and the functions in it are top-level functions. This makes it easy to access directly from other parts of the project without requiring class names or instantiations.

Try catch exception handling

General triggered link timeout and parsing exceptions can be handled. If there is no try catch, the APP may crash or have no receipt for a long time, resulting in poor experience.

/** * handle request-layer errors, and handle possible known errors */
fun handlingExceptions(e: Throwable) {
    when (e) {
        is CancellationException -> {}
        is SocketTimeoutException -> {}
        is JsonParseException -> {}
        else- > {}}}Copy the code

Server defined response exception

Generally, the server has a response code for the request, and the client processes the response according to the response code. Different error codes will have different log feedback or prompt, but this is based on the success of the request. There are generally no more than successes and failures.

  • Http request response encapsulation
// When is used to make possible scenarios known, making code more maintainable.
sealed class HttpResponse

data class Success<out T>(val data: T) : HttpResponse()
data class Failure(val error: HttpError) : HttpResponse()
Copy the code
  • Error enumeration
enum class HttpError(val code: Int.val errorMsg: String?) {
    USER_EXIST(20001."user does not exist"),
    PARAMS_ERROR(20002."params is error")
    / /... more
}
Copy the code
  • Error handling
/** * handle the response layer error */
fun handlingApiExceptions(e: HttpError) {
    when (e) {
        HttpError.USER_EXIST -> {}
        HttpError.PARAMS_ERROR -> {}
        // .. more}}Copy the code
  • Processing of HttpResponse
/** * Handle HttpResponse *@param res
 * @paramSuccessBlock success *@paramFailureBlock * / failure
fun <T> handlingHttpResponse(
    res: HttpResponse,
    successBlock: (data: T) -> Unit,
    failureBlock: ((error: HttpError) -> Unit)? = null
) {
    when (res) {
        is Success<*> -> {
            successBlock.invoke(res.data as T)
        }
        isFailure -> { with(res) { failureBlock? .invoke(error) ? : defaultErrorBlock.invoke(error) } } } }// The default processing scheme
val defaultErrorBlock: (error: HttpError) -> Unit= { error -> UiUtils.showToast(error.errorMsg ? :"${error.code}")            // Can be split depending on whether it is debug
}
Copy the code

Here we are handling HttpRespoonse directly, and we need a transformation of the current response content

  • Transform server response
fun <T : Any> ApiResponse<T>.convertHttpRes(a): HttpResponse {
    return if (this.code == HTTP_SUCCESS) {
        data? .let { Success(it) } ? : Success(Any()) }else {
        Failure(HttpError.USER_EXIST)
    }
}
Copy the code

Temporarily defined as an extension function to be used in conjunction with this. Once the basic encapsulation is complete, start a test class to test.

test

  • client
object UserRetrofitClient : BaseRetrofitClient() {

    val service by lazy { getService(UserApi::class.java.UserApi.BASE_URL) }

    override fun handleBuilder(builder: OkHttpClient.Builder){}}Copy the code
  • model
class LoginRepository {

    suspend fun doLogin(phone: String, pwd: String) = UserRetrofitClient.service.login(
        LoginReq(phone, pwd).toJsonBody()
    )

}
Copy the code
  • viewModel
class LoginViewModel : BaseViewModel() {

    private val repository by lazy { LoginRepository() }

    companion object {
        const val LOGIN_STATE_SUCCESS = 0
        const val LOGIN_STATE_FAILURE = 1
    }

    // Login status
    val loginState: MutableLiveData<Int> = MutableLiveData()

    fun doLogin(phone: String, pwd: String) {
        launchOnIO(
            tryBlock = {
                repository.doLogin(phone, pwd).run {
                    // Process the response
                    handlingHttpResponse<LoginRes>(
                        convertHttpRes(),
                        successBlock = {
                            loginState.postValue(LOGIN_STATE_SUCCESS)
                        },
                        failureBlock = { ex ->
                            loginState.postValue(LOGIN_STATE_FAILURE)
                            handlingApiExceptions(ex)
                        }
                    )
                }
            },
            // Request exception handling
            catchBlock = { e ->
                handlingExceptions(e)
            }
        )
    }
}
Copy the code
  • Finally, listen on loginState in LoginAct
vm.loginState.observe(this, Observer { state ->
            when(state){
                LoginViewModel.LOGIN_STATE_SUCCESS ->{
                    UiUtils.showToast("success")
                }
                LoginViewModel.LOGIN_STATE_FAILURE ->{
                    UiUtils.showToast("failure")}}})Copy the code

conclusion

These are some of the ways I can think of so far, and I think Kotlin has really made a big difference, especially in readability and maintainability. While there is no standard approach to architecture and overall design, these issues are all relative.