1, the preface

I wrote a colorful cloud weather App step by step when I learned the first Line of Code by Guo Lin. I was very impressed by the encapsulation of network request framework inside, and I like the use of Retrofit + Kotlin + coroutine. Later, I also referred to this part of the code in my own project. However, with the in-depth writing of the code and the complexity of the function, the original framework has been unable to meet my use. The original main pain points are as follows:

  • Lack of failed callbacks
  • Displaying loading animations is cumbersome

Later, I tried to package a simple and easy to use framework, but unfortunately my personal ability is limited, the framework packaged by myself is always unsatisfactory. Fortunately, there are plenty of good blogs and code to look at. On this basis, some modifications have been made to the network request framework in the CLOUD Weather App to make it as simple and easy to use as possible. Take the login interface of Requesting to play Android as an example (I applied for the user name and password myself, see the code), there is a button on the page, click the button to initiate a login request.

Let’s start with a callback that initiates the request:

viewModel.loginLiveData.observeState(this) {
    onStart {
        LoadingDialog.show(activity)
        Log.d(TAG, "Request initiated")
    }
    onSuccess {
        Log.d(TAG, "Request successful")
        showToast("Login successful")
        binding.tvResult.text = it.toString()
    }
    onEmpty {
        showToast("Data is empty")
    }
    onFailure {
        Log.d(TAG, "Request failed")
        showToast(it.errorMsg.orEmpty())
        binding.tvResult.text = it.toString()
    }
    onFinish {
        LoadingDialog.dismiss(activity)
        Log.d(TAG, "Request terminated")}}Copy the code

There are five types of callback, which are described in more detail below. Call observeResponse(). Since its last argument is a callback to the successful request, using the power of Lambda expressions, it can be written succinct as follows:

viewModel.loginLiveData.observeResponse(this){
    binding.tvResult.text = it.toString()
}
Copy the code

If you need additional callbacks, you can use the named parameter plus, as shown below:

viewModel.loginLiveData.observeResponse(this, onStart = {
    LoadingDialog.show(this)
}, onFinish = {
    LoadingDialog.dismiss(activity)
}) {
    binding.tvResult.text = it.toString()
}
Copy the code

2. Frame building

Before we begin, it should be noted that this framework is based on the Cloud Weather App from Line 1 (3rd edition). The architecture diagram is shown below, which is familiar to anyone who has read Line 1 or Google’s documentation.

2.1 Adding a dependent Library

// Simplify the code for declaring the ViewModel in the Activity
implementation "Androidx. Activity: activity - KTX: 1.3.1." "

// lifecycle
def lifecycle_version = 2.3.1 ""
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

// retrofit2
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'the IO. Reactivex. Rxjava2: rxandroid: 2.1.1'

// okhttp
def okhttp_version = "4.8.1"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"

// Log interceptor
implementation('com. Making. Ihsanbal: LoggingInterceptor: 3.1.0') {
    exclude group: 'org.json'.module: 'json'
}
Copy the code

2.2 RetrofitThe builder

The Retrofit builder is layered here, with the base class doing some basic configuration and subclasses inheriting to add new configurations and configure their favorite log interceptors.

private const val TIME_OUT_LENGTH = 8L

private const val BASE_URL = "https://www.wanandroid.com/"

abstract class BaseRetrofitBuilder {

    private val okHttpClient: OkHttpClient by lazy {
        val builder = OkHttpClient.Builder()
            .callTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
            .connectTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
            .readTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
            .writeTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true) initLoggingInterceptor()? .also { builder.addInterceptor(it) } handleOkHttpClientBuilder(builder) builder.build() }private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build()

    fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)

    inline fun <reified T> create(a): T = create(T::class.java)

    /** * subclass custom OKHttpClient configuration */
    abstract fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)

    /** * Configure log interceptor */
    abstract fun initLoggingInterceptor(a): Interceptor?
}
Copy the code

RetrofitBuilder:

private const val LOG_TAG_HTTP_REQUEST = "okhttp_request"
private const val LOG_TAG_HTTP_RESULT = "okhttp_result"

object RetrofitBuilder : BaseRetrofitBuilder() {

    override fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder) {}

    override fun initLoggingInterceptor(a)= LoggingInterceptor
        .Builder()
        .setLevel(Level.BASIC)
        .log(Platform.INFO)
        .request(LOG_TAG_HTTP_REQUEST)
        .response(LOG_TAG_HTTP_RESULT)
        .build()
}
Copy the code

2.3 Global Exception Handling

Unexpected exceptions such as network disconnection, Json parsing failure, and so on can be encountered during a request, and it would be too much trouble to handle these exceptions every time we request them. The correct approach is to group the exceptions together.

Create an enumeration class that defines various exceptions:

enum class HttpError(val code: Int.val message: String){
    UNKNOWN(-100."Unknown error"),
    NETWORK_ERROR(1000."Network connection timed out. Please check network."),
    JSON_PARSE_ERROR(1001."Json parsing failed")
    / /......
}
Copy the code

Create a file in which you define a global method to handle various exceptions:

fun handleException(throwable: Throwable) = when (throwable) {
    is UnknownHostException -> RequestException(HttpError.NETWORK_ERROR, throwable.message)
    is HttpException -> {
        valerrorModel = throwable.response()? .errorBody()? .string()? .run { Gson().fromJson(this, ErrorBodyModel::class.java) } ? : ErrorBodyModel() RequestException(errorMsg = errorModel.message, error = errorModel.error) }is JsonParseException -> RequestException(HttpError.JSON_PARSE_ERROR, throwable.message)
    is RequestException -> throwable
    else -> RequestException(HttpError.UNKNOWN, throwable.message)
}
Copy the code

Of course, there are more than a few exceptions encountered in the actual project. Here are only a few examples to write, which can be enriched and perfected in the actual opening.

2.4 Callback status monitoring

There are four callback states:

  • onStart(): Request started (loading animation can be shown here)
  • onSuccess(): Request successful
  • onEmpty(): The request succeeded, butdatafornullordataIs a collection type but empty
  • onFailure(): Request failed
  • onFinish(): End of request (load animation can be turned off here)

Here we should pay attention to the criteria of onSuccess: not only the status code of the Http request is equal to 200, but also the criteria of the SUCCESS of the Api request. Take android Api as an example, when the errorCode is 0, the initiated request is successfully executed. Otherwise, it should be a case of onFailure() (see the accompanying mind map).

After figuring out how many callback states there are, you can perform listening. So where to listen? The second function of LiveData’s Observe () method can pass in an Observer argument. The Observer is an interface that we inherit from to customize Oberver so that we can listen for changes in LiveData values.

interface IStateObserver<T> : Observer<BaseResponse<T>> {

    override fun onChanged(response: BaseResponse<T>? {
        when (response) {
            is StartResponse -> {
                OnFinish () cannot be called directly after the onStart() callback and must wait for the request to end
                onStart()
                return
            }
            is SuccessResponse -> onSuccess(response.data)
            is EmptyResponse -> onEmpty()
            is FailureResponse -> onFailure(response.exception)
        }
        onFinish()
    }

    /** * request to start */
    fun onStart(a)

    /** * The request succeeded and data is not null */
    fun onSuccess(data: T)

    /** * The request was successful, but data is null or data is a collection type but empty */
    fun onEmpty(a)

    /**
     * 请求失败
     */
    fun onFailure(e: RequestException)

    /** * The request ends */
    fun onFinish(a)
}

Copy the code

Next we prepare an HttpRequestCallback class that implements the callback form of the DSL:

typealias OnSuccessCallback<T> = (data: T) -> Unit
typealias OnFailureCallback = (e: RequestException) -> Unit
typealias OnUnitCallback = () -> Unit

class HttpRequestCallback<T> {

    var startCallback: OnUnitCallback? = null
    var successCallback: OnSuccessCallback<T>? = null
    var emptyCallback: OnUnitCallback? = null
    var failureCallback: OnFailureCallback? = null
    var finishCallback: OnUnitCallback? = null

    fun onStart(block: OnUnitCallback) {
        startCallback = block
    }

    fun onSuccess(block: OnSuccessCallback<T>) {
        successCallback = block
    }

    fun onEmpty(block: OnUnitCallback) {
        emptyCallback = block
    }

    fun onFailure(block: OnFailureCallback) {
        failureCallback = block
    }

    fun onFinish(block: OnUnitCallback) {
        finishCallback = block
    }
}
Copy the code

Then declare the new listening method, considering that some times need to customize LiveData(such as to solve the problem of data flooding), here we use the extension function writing method, easy to expand.

/** * listen for changes in LiveData values, callback to DSL */
inline fun <T> LiveData<BaseResponse<T>>.observeState(
    owner: LifecycleOwner.crossinline callback: HttpRequestCallback<T>. () - >Unit
) {
    val requestCallback = HttpRequestCallback<T>().apply(callback)
    observe(owner, object : IStateObserver<T> {
        override fun onStart(a){ requestCallback.startCallback? .invoke() }override fun onSuccess(data: T){ requestCallback.successCallback? .invoke(data)}override fun onEmpty(a){ requestCallback.emptyCallback? .invoke() }override fun onFailure(e: RequestException){ requestCallback.failureCallback? .invoke(e) }override fun onFinish(a){ requestCallback.finishCallback? .invoke() } }) }/** * listen for changes in LiveData values */
inline fun <T> LiveData<BaseResponse<T>>.observeResponse(
    owner: LifecycleOwner.crossinline onStart: OnUnitCallback = {},
    crossinline onEmpty: OnUnitCallback = {},
    crossinline onFailure: OnFailureCallback = { e: RequestException -> },
    crossinline onFinish: OnUnitCallback = {},
    crossinline onSuccess: OnSuccessCallback<T>) {
    observe(owner, object : IStateObserver<T> {
        override fun onStart(a) {
            onStart()
        }

        override fun onSuccess(data: T) {
            onSuccess(data)}override fun onEmpty(a) {
            onEmpty()
        }

        override fun onFailure(e: RequestException) {
            onFailure(e)
        }

        override fun onFinish(a) {
            onFinish()
        }
    })
}
Copy the code

2.5 RepositoryThe encapsulation

As a source of data, the Repository layer has two channels: network requests and databases. For now, only network requests are being processed here.

The base class Repository:

abstract class BaseRepository {

    protected fun <T> fire(
        context: CoroutineContext = Dispatchers.IO,
        block: suspend() - >BaseResponse<T>): LiveData<BaseResponse<T>> = liveData(context) {
        this.runCatching {
            emit(StartResponse())
            block()
        }.onSuccess {
            // Status code is 200, continue to check whether errorCode is 0
            emit(
                when (it.success) {
                    true -> checkEmptyResponse(it.data)
                    false -> FailureResponse(handleException(RequestException(it)))
                }
            )
        }.onFailure { throwable ->
            emit(FailureResponse(handleException(throwable)))
        }
    }

    /** * data is null, or data is a collection type, but the collection is empty, which goes into the onEmpty callback */
    private fun <T> checkEmptyResponse(data: T?).: ApiResponse<T> =
        if (data= =null| | -data is List<*> && (data as List<*>).isEmpty())) {
            EmptyResponse()
        } else {
            SuccessResponse(data)}}Copy the code

Subclasses Repository:

object Repository : BaseRepository() {

    fun login(pwd: String) = fire {
        NetworkDataSource.login(pwd)
    }

}
Copy the code

The network request data source, where the network interface is invoked:

object NetworkDataSource {
    private val apiService = RetrofitBuilder.create<ApiService>()

    suspend fun login(pwd: String) = apiService.login(password = pwd)
}
Copy the code

2.6 ViewModelThe encapsulation

The ViewModel basically follows the first line of code, creating two LiveData. When the user clicks the button, the value of the loginAction changes, triggering the code in the switchMap to request the data.

class MainViewModel : ViewModel() {

    private val loginAction = MutableLiveData<Boolean> ()/** * loginAction passes only a Boolean value, not a password. In a real project, it uses the DataBinding binding XML layout and ViewModel, * There is no need to pass passwords from activities or fragments into ViewModel */
    val loginLiveData = loginAction.switchMap {
        if (it) {
            Repository.login("PuKxVxvMzBp2EJM")}else {
            Repository.login("123456")}}/** * click login */
    fun login(a) {
        loginAction.value = true
    }

    fun loginWithWrongPwd(a) {
        loginAction.value = false}}Copy the code

Note: This writing does not normally pass data from the View to the ViewModel layer, and is accompanied by DataBinding. If you don’t want to do that, you can modify the return value in BaseRepository and return BaseResponse directly.

3. Mind mapping and source code

Finally, summarize this article with a mind map:

GitHub (note that the branch should be dev1.0)

reference

  • JetpackMvvm
  • FastJetpack