当前位置:网站首页>Encapsulation attempt of network request framework of retro + kotlin + MVVM
Encapsulation attempt of network request framework of retro + kotlin + MVVM
2022-07-03 11:09:00 【Lindroy】
1、 Preface
I was studying Guolin before 《 First line of code 》 I wrote a colorful cloud weather step by step App, I'm very impressed with the encapsulation of the network request framework , I like this very much Retrofit
+ Kotlin
+ Collocation and use of synergy . Later, I also referred to this part of the code in my own project . But as the code is written in depth and the function is complex , The original framework can no longer meet my needs . The main pain points are as follows :
- Missing failed callback
- It is troublesome to display the animation in loading
Later, I try to encapsulate a simple and easy-to-use framework , Unfortunately, my personal ability is limited , The self encapsulated framework is always unsatisfactory . Fortunately, there are many excellent blogs and codes for reference . On this basis , Yes, Caiyun weather App Some modifications have been made to the network request framework in , Be 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 launch the login request .
Let's first look at the callback after the request is made :
viewModel.loginLiveData.observeState(this) {
onStart {
LoadingDialog.show(activity)
Log.d(TAG, " Request start ")
}
onSuccess {
Log.d(TAG, " The request is successful ")
showToast(" Login successful ")
binding.tvResult.text = it.toString()
}
onEmpty {
showToast(" Data is empty ")
}
onFailure {
Log.d(TAG, " request was aborted ")
showToast(it.errorMsg.orEmpty())
binding.tvResult.text = it.toString()
}
onFinish {
LoadingDialog.dismiss(activity)
Log.d(TAG, " End of request ")
}
}
There are five kinds of callbacks , It will be described in detail below . Here we use DSL
Writing , If you like traditional writing , You can call another extension method observeResponse()
, Because its last parameter is the callback of successful request , So with Lambda The properties of expressions , It can be succinctly written in the following form :
viewModel.loginLiveData.observeResponse(this){
binding.tvResult.text = it.toString()
}
If you need other callbacks , have access to Named parameters add , As shown below :
viewModel.loginLiveData.observeResponse(this, onStart = {
LoadingDialog.show(this)
}, onFinish = {
LoadingDialog.dismiss(activity)
}) {
binding.tvResult.text = it.toString()
}
2、 Frame building
It must be stated before starting , This framework is based on 《 First line of code 》( The third edition ) Colorful cloud weather in App Of , Its architecture is shown below , If you've read 《 First line of code 》 Or Google related documents , Then it must not be strange .
2.1 Add dependency Library
// Simplify in Activity In a statement ViewModel Code for
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"
// Log interceptor
implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') {
exclude group: 'org.json', module: 'json'
}
2.2 Retrofit
Builder
Retrofit
The builder is layered here , The base class does some basic configuration , New configurations can be added after subclass inheritance , 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)
/** * Subclass customization OKHttpClient Configuration of */
abstract fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)
/** * Configure log interceptors */
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, you may encounter problems such as network disconnection 、Json
Unexpected situations such as parsing failure , If we have to deal with these exceptions every time we request , That's too much trouble . The correct way is to put 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 timeout , Please check the network "),
JSON_PARSE_ERROR(1001, "Json Parse failure ")
//······
}
Create a file , Define a global method in it , Used 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, these are not the only exceptions encountered in the actual project , Here is only a small part written as an example , It can be enriched and improved in the actual opening .
2.4 Callback status monitoring
There are four callback States :
onStart()
: Request start ( You can show loading animation here )onSuccess()
: The request is successfulonEmpty()
: The request is successful , butdata
bynull
perhapsdata
Is a collection type but is emptyonFailure()
: request was abortedonFinish()
: End of request ( You can turn off loading animation here )
Pay attention here onSuccess
Standards for : It's not just Http The result code of the request (status code) be equal to 200, And to achieve Api Criteria for successful request , To play Android Api For example ,errorCode
by 0 when , The initiated request is the successful execution ; otherwise , Should be classified as onFailure()
The situation of ( You can refer to the mind map attached to the article ).
After sorting out several callback States , You can monitor . So where to monitor ?LiveData
Of observe()
The second function of the method can be passed into Observer
Parameters .Observer
It's an interface , We inherit it and customize one Oberver
, So we can monitor LiveData
A change in the value of .
interface IStateObserver<T> : Observer<BaseResponse<T>> {
override fun onChanged(response: BaseResponse<T>?) {
when (response) {
is StartResponse -> {
//onStart() You cannot call directly after callback onFinish(), You must wait for the request to end
onStart()
return
}
is SuccessResponse -> onSuccess(response.data)
is EmptyResponse -> onEmpty()
is FailureResponse -> onFailure(response.exception)
}
onFinish()
}
/** * Request start */
fun onStart()
/** * The request is successful , And data Not for null */
fun onSuccess(data: T)
/** * The request is successful , but data by null perhaps data Is a collection type but is empty */
fun onEmpty()
/** * request was aborted */
fun onFailure(e: RequestException)
/** * End of request */
fun onFinish()
}
Next we prepare a HttpRequestCallback
class , Used to implement DSL
Callback form :
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 listening method , Considering that you need to customize it sometimes LiveData
( For example, to solve the problem of data backflow ), Here we use the writing method of extension function , To facilitate extension .
/** * monitor LiveData A change in the value of , Callback as DSL In the form of */
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()
}
})
}
/** * monitor LiveData A change in the value of */
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
Encapsulation of layers
Repository
Layer as the source of data , There are two channels : Network requests and databases . 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 by 200, Continue to judge errorCode Is it 0
emit(
when (it.success) {
true -> checkEmptyResponse(it.data)
false -> FailureResponse(handleException(RequestException(it)))
}
)
}.onFailure {
throwable ->
emit(FailureResponse(handleException(throwable)))
}
}
/** * data by null, perhaps data It's a collection type , But if the set is empty, it will enter onEmpty Callback */
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
Encapsulation of layers
ViewModel
Basically followed 《 First line of code 》 Writing in Chinese , I created two LiveData
. When the user clicks the button ,loginAction
The value of will change , Trigger switchMap
The code in , So as to achieve the purpose of requesting data .
class MainViewModel : ViewModel() {
private val loginAction = MutableLiveData<Boolean>()
/** * loginAction Only Boolean values are passed here , Don't pass the password , In the actual project , Will use DataBinding binding xml Layout and ViewModel, * No need to Activity perhaps Fragment Pass the password into ViewModel */
val loginLiveData = loginAction.switchMap {
if (it) {
Repository.login("PuKxVxvMzBp2EJM")
} else {
Repository.login("123456")
}
}
/** * Click login */
fun login() {
loginAction.value = true
}
fun loginWithWrongPwd() {
loginAction.value = false
}
}
Be careful : This kind of writing is usually not from
View
towards ViewModel Layer passing data , It needs to be matchedDataBinding
Of . If you don't want to write like this , You can modifyBaseRepository
Return value in , Go straight back toBaseResponse
.
3、 Mind map and source code
Last , Summarize this article with a mind map :
Source code address :GitHub ( Note that branches should be selected dev1.0)
4、 Reference resources
边栏推荐
- 最高月薪18K 拥有好的“心态和选择”, 成功就差“认真和坚持”~
- 值得关注的15种软件测试趋势
- Matlab memory variable management command
- "Core values of testing" and "super complete learning guide for 0 basic software testing" summarized by test engineers for 8 years
- 线性表顺序表综合应用题P18
- Word line and bit line
- 独家分析 | 关于简历和面试的真 相
- Overview of testing theory
- Redis notes 01: Introduction
- 测试理论概述
猜你喜欢
What are the strengths of "testers"?
T5 attempt
(二)进制
How can UI automated testing get out of trouble? How to embody the value?
The normal one inch is 25.4 cm, and the image field is 16 cm
字节跳动大裁员,测试工程师差点遭团灭:大厂招人背后的套路,有多可怕?
Redis notes 01: Introduction
What kind of living condition is a tester with a monthly salary of more than 10000?
QT: QSS custom qtableview instance
Multiple IO transfer - preamble
随机推荐
Differences among norm, normalize and normalized in eigen
Multiple IO transfer - preamble
A simple method of adding dividing lines in recyclerview
Cache routing component
12. Nacos server service registration of source code analysis of Nacos service registration
Qt:qss custom qheaderview instance
Win10系统下提示“系统组策略禁止安装此设备”的解决方案(家庭版无组策略)
Qt:qss custom QSlider instance
Commonly used discrete random distribution
公司测试部门来了个00后卷王之王,老油条感叹真干不过,但是...
2021 postgraduate entrance examination mathematics 2 linear algebra
如何让让别人畏惧你
17K薪资要什么水平?来看看95后测试工程师的面试全过程…
8年测试总监的行业思考,看完后测试思维认知更深刻
glassfish org. h2.server. Shutdownhandler classnotfoundexception exception exception handling
封装一个koa分布式锁中间件来解决幂等或重复请求的问题
栈,单调栈,队列,单调队列
How to realize automatic testing in embedded software testing?
Crawl with requests
今晚要修稿子準備發佈。但是,仍卡在這裡,也許你需要的是一個段子。