1 Introduction
When studying Guo Lin's "First Line of Code", I wrote a Caiyun Weather App step by step. I was very impressed with the packaging of the network request framework inside. I like the Retrofit
+ Kotlin
+ coroutine. Later I also referenced this part of the code in my own project. But with the in-depth writing of the code and the complexity of the functions, the original framework can no longer satisfy my use. The main pain points are as follows:
- Missing failed callback
- It is troublesome to display the loading animation
Later, I tried my best to encapsulate a simple and easy-to-use framework, but unfortunately my personal ability is limited, and the framework I encapsulated is always not satisfactory. Fortunately, there are still many excellent blogs and codes for reference. On this basis, some modifications have been made to the network request framework in the Caiyun Weather App to make it as simple and easy to use as possible. Take the login interface requesting to play Android as an example (the user name and password are applied by myself, see the code), there is a button on the page, and the login request is initiated after clicking the button.
Let's take a look at how to write the callback after the request is initiated:
viewModel.loginLiveData.observeState(this) {
onStart {
LoadingDialog.show(activity)
Log.d(TAG, "请求开始")
}
onSuccess {
Log.d(TAG, "请求成功")
showToast("登录成功")
binding.tvResult.text = it.toString()
}
onEmpty {
showToast("数据为空")
}
onFailure {
Log.d(TAG, "请求失败")
showToast(it.errorMsg.orEmpty())
binding.tvResult.text = it.toString()
}
onFinish {
LoadingDialog.dismiss(activity)
Log.d(TAG, "请求结束")
}
}
There are five types of callbacks, which will be described in detail below. DSL
is used here. If you like the traditional writing method, you can call another extension method observeResponse()
. Since its last parameter is the callback for successful request, it can be written concisely in the following form with the help of the characteristics of Lambda expressions:
viewModel.loginLiveData.observeResponse(this){
binding.tvResult.text = it.toString()
}
If you need other callbacks, you can use the named parameter add, as shown below:
viewModel.loginLiveData.observeResponse(this, onStart = {
LoadingDialog.show(this)
}, onFinish = {
LoadingDialog.dismiss(activity)
}) {
binding.tvResult.text = it.toString()
}
2. Frame construction
Before you start, you must explain that this framework is based on the Caiyun Weather App in the "First Line of Code" (third edition). Its architecture is shown below. If you have read "The First Line of Code" or Google's related documents , Then I must be familiar with this.
2.1 Add dependent libraries
//简化在 Activity 中声明 ViewModel 的代码
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 'io.reactivex.rxjava2:rxandroid:2.1.1'
// okhttp
def okhttp_version = "4.8.1"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
//日志拦截器
implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') {
exclude group: 'org.json', module: 'json'
}
2.2 Retrofit
builder
Retrofit
builder is layered here. The base class has done some basic configuration. After the subclass inherits, you can add new configurations and configure your favorite log interceptor.
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(): T = create(T::class.java)
/**
* 子类自定义 OKHttpClient 的配置
*/
abstract fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)
/**
* 配置日志拦截器
*/
abstract fun initLoggingInterceptor(): Interceptor?
}
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()= LoggingInterceptor
.Builder()
.setLevel(Level.BASIC)
.log(Platform.INFO)
.request(LOG_TAG_HTTP_REQUEST)
.response(LOG_TAG_HTTP_RESULT)
.build()
}
2.3 Global exception handling
When requesting, we may encounter Json
parsing failure, etc. If we have to deal with these exceptions every time we request, it would be too troublesome. The correct approach is to concentrate the exceptions together.
Create an enumeration class that defines various exceptions:
enum class HttpError(val code: Int, val message: String){
UNKNOWN(-100,"未知错误"),
NETWORK_ERROR(1000, "网络连接超时,请检查网络"),
JSON_PARSE_ERROR(1001, "Json 解析失败")
//······
}
Create a file and define a global method in it to handle various exceptions:
fun handleException(throwable: Throwable) = when (throwable) {
is UnknownHostException -> RequestException(HttpError.NETWORK_ERROR, throwable.message)
is HttpException -> {
val errorModel = 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)
}
Of course, the exceptions encountered in the actual project are not limited to these few. Here is just a small part of it as an example, and it can be enriched and perfected in the actual opening.
2.4 Callback status monitoring
There are four callback states:
onStart()
: Request start (loading animation can be displayed here)onSuccess()
: The request was successfulonEmpty()
: The request is successful, butdata
isnull
ordata
is a collection type but is emptyonFailure()
: The request failedonFinish()
: End of request (loading animation can be turned off here)
Here we must note onSuccess
criteria: Http request is not just the result of code (status code) is equal to 200, but also to reach standard Api request is successful , to play Andrews Api, for example, errorCode
0, initiated request The execution is successful; otherwise, it should be classified as onFailure()
(you can refer to the mind map attached to the article).
After clarifying that there are several callback states, you can implement monitoring. So where do you listen? LiveData
the observe()
second function method can pass Observer
parameters. Observer
is an interface, we inherit it to customize a Oberver
, by which we can monitor the LiveData
of the value of 06187565c067f5.
interface IStateObserver<T> : Observer<BaseResponse<T>> {
override fun onChanged(response: BaseResponse<T>?) {
when (response) {
is StartResponse -> {
//onStart()回调后不能直接就调用onFinish(),必须等待请求结束
onStart()
return
}
is SuccessResponse -> onSuccess(response.data)
is EmptyResponse -> onEmpty()
is FailureResponse -> onFailure(response.exception)
}
onFinish()
}
/**
* 请求开始
*/
fun onStart()
/**
* 请求成功,且 data 不为 null
*/
fun onSuccess(data: T)
/**
* 请求成功,但 data 为 null 或者 data 是集合类型但为空
*/
fun onEmpty()
/**
* 请求失败
*/
fun onFailure(e: RequestException)
/**
* 请求结束
*/
fun onFinish()
}
Next, we prepare a HttpRequestCallback
class to implement the callback form of 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
}
}
Then declare a new monitoring method, considering that you need to customize LiveData
at some point (for example, to solve the problem of data backflow), here the extension function is written to facilitate expansion.
/**
* 监听 LiveData 的值的变化,回调为 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() {
requestCallback.startCallback?.invoke()
}
override fun onSuccess(data: T) {
requestCallback.successCallback?.invoke(data)
}
override fun onEmpty() {
requestCallback.emptyCallback?.invoke()
}
override fun onFailure(e: RequestException) {
requestCallback.failureCallback?.invoke(e)
}
override fun onFinish() {
requestCallback.finishCallback?.invoke()
}
})
}
/**
* 监听 LiveData 的值的变化
*/
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() {
onStart()
}
override fun onSuccess(data: T) {
onSuccess(data)
}
override fun onEmpty() {
onEmpty()
}
override fun onFailure(e: RequestException) {
onFailure(e)
}
override fun onFinish() {
onFinish()
}
})
}
2.5 Repository
layer encapsulation
Repository
layer has two channels: network request and database. Only network requests are processed here for the time being.
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 为200,继续判断 errorCode 是否为 0
emit(
when (it.success) {
true -> checkEmptyResponse(it.data)
false -> FailureResponse(handleException(RequestException(it)))
}
)
}.onFailure { throwable ->
emit(FailureResponse(handleException(throwable)))
}
}
/**
* data 为 null,或者 data 是集合类型,但是集合为空都会进入 onEmpty 回调
*/
private fun <T> checkEmptyResponse(data: T?): ApiResponse<T> =
if (data == null || (data is List<*> && (data as List<*>).isEmpty())) {
EmptyResponse()
} else {
SuccessResponse(data)
}
}
Subclass Repository
:
object Repository : BaseRepository() {
fun login(pwd: String) = fire {
NetworkDataSource.login(pwd)
}
}
Network request data source, call the network interface here:
object NetworkDataSource {
private val apiService = RetrofitBuilder.create<ApiService>()
suspend fun login(pwd: String) = apiService.login(password = pwd)
}
2.6 ViewModel
layer
ViewModel
basically followed the wording in the "first line of code" and created two LiveData
. When the user clicks the button, loginAction
will change, triggering switchMap
, so as to achieve the purpose of requesting data.
class MainViewModel : ViewModel() {
private val loginAction = MutableLiveData<Boolean>()
/**
* loginAction 在这里只传递布尔值,不传递密码,在实际项目中,会使用 DataBinding 绑定 xml 布局和 ViewModel,
* 不需要从 Activity 或者 Fragment 中把密码传入 ViewModel
*/
val loginLiveData = loginAction.switchMap {
if (it) {
Repository.login("PuKxVxvMzBp2EJM")
} else {
Repository.login("123456")
}
}
/**
* 点击登录
*/
fun login() {
loginAction.value = true
}
fun loginWithWrongPwd() {
loginAction.value = false
}
}
Note: This writing method usually does notView
to the ViewModel layer, it needs to be matched withDataBinding
. If you do not want to write, you can modifyBaseRepository
return values directly backBaseResponse
.
3. Mind map and source code
Finally, summarize this article with a mind map:
Source address: GitHub (note that the branch should choose dev1.0)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。