1、前言
之前在學(xué)習(xí)郭霖《第一行代碼》時(shí)按部就班地寫(xiě)過(guò)一個(gè)彩云天氣 App私杜,對(duì)里面的網(wǎng)絡(luò)請(qǐng)求框架的封裝印象非常深刻,很喜歡這種 Retrofit
+ Kotlin
+ 協(xié)程的搭配使用救欧。隨后也在自己的項(xiàng)目里參考了這部分的代碼衰粹。但隨著代碼的深入編寫(xiě)和功能的復(fù)雜,原來(lái)的框架已經(jīng)無(wú)法滿足我的使用了笆怠。原主要有如下的痛點(diǎn):
- 缺少失敗的回調(diào)
- 顯示加載中動(dòng)畫(huà)比較麻煩
后面我自己試著努力去封裝一個(gè)簡(jiǎn)單易用的框架铝耻,可惜個(gè)人能力有限,自己封裝的框架總是不如人意蹬刷。好在還有很多優(yōu)秀的博客和代碼可供參考瓢捉。在此基礎(chǔ)上,對(duì)彩云天氣 App中的網(wǎng)絡(luò)請(qǐng)求框架做了一些修改办成,盡可能地做到簡(jiǎn)單易用泊柬。以請(qǐng)求玩安卓的登錄接口為例(用戶名和密碼是我自己申請(qǐng)的,見(jiàn)代碼)诈火,頁(yè)面上有一個(gè)按鈕,點(diǎn)擊按鈕后就發(fā)起登錄請(qǐng)求。
先來(lái)看看發(fā)起請(qǐng)求后的回調(diào)怎么寫(xiě):
viewModel.loginLiveData.observeState(this) {
onStart {
LoadingDialog.show(activity)
Log.d(TAG, "請(qǐng)求開(kāi)始")
}
onSuccess {
Log.d(TAG, "請(qǐng)求成功")
showToast("登錄成功")
binding.tvResult.text = it.toString()
}
onEmpty {
showToast("數(shù)據(jù)為空")
}
onFailure {
Log.d(TAG, "請(qǐng)求失敗")
showToast(it.errorMsg.orEmpty())
binding.tvResult.text = it.toString()
}
onFinish {
LoadingDialog.dismiss(activity)
Log.d(TAG, "請(qǐng)求結(jié)束")
}
}
回調(diào)一共有五種冷守,會(huì)在下文詳細(xì)介紹刀崖。這里采用了DSL
的寫(xiě)法,如果你喜歡傳統(tǒng)的寫(xiě)法拍摇,可以調(diào)用另外一個(gè)擴(kuò)展方法observeResponse()
亮钦,由于它最后一個(gè)參數(shù)就是請(qǐng)求成功的回調(diào),所以借助 Lambda 表達(dá)式的特性充活,可以簡(jiǎn)潔地寫(xiě)成如下的形式:
viewModel.loginLiveData.observeResponse(this){
binding.tvResult.text = it.toString()
}
如果還需要其他回調(diào)蜂莉,可以使用具名參數(shù)加上,如下所示:
viewModel.loginLiveData.observeResponse(this, onStart = {
LoadingDialog.show(this)
}, onFinish = {
LoadingDialog.dismiss(activity)
}) {
binding.tvResult.text = it.toString()
}
2混卵、框架搭建
開(kāi)始之前必須說(shuō)明映穗,這個(gè)框架是基于《第一行代碼》(第三版)中的彩云天氣 App的,它的架構(gòu)圖如下所示幕随,如果你閱讀過(guò)《第一行代碼》或者谷歌的相關(guān)文檔蚁滋,那么想必對(duì)此不會(huì)陌生。
2.1 添加依賴庫(kù)
//簡(jiǎn)化在 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
構(gòu)建器
Retrofit
構(gòu)建器這里做了分層赘淮,基類做了一些基本的配置辕录,子類繼承后可以添加新的配置,并配置自己喜歡的日志攔截器梢卸。
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 全局異常處理
請(qǐng)求時(shí)可能會(huì)遇到諸如網(wǎng)絡(luò)斷開(kāi)走诞、Json
解析失敗等意外情況,如果我們每次請(qǐng)求都要處理一遍這些異常蛤高,那也未免太麻煩了蚣旱。正確的做法是把異常集中到一起處理。
創(chuàng)建一個(gè)定義各種異常的枚舉類:
enum class HttpError(val code: Int, val message: String){
UNKNOWN(-100,"未知錯(cuò)誤"),
NETWORK_ERROR(1000, "網(wǎng)絡(luò)連接超時(shí)襟齿,請(qǐng)檢查網(wǎng)絡(luò)"),
JSON_PARSE_ERROR(1001, "Json 解析失敗")
//······
}
創(chuàng)建一個(gè)文件姻锁,在里面定義一個(gè)全局方法,用于處理各種異常:
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)
}
實(shí)際項(xiàng)目中遇到的異常當(dāng)然不止這幾個(gè)猜欺,這里只是作為舉例寫(xiě)了少部分位隶,實(shí)際開(kāi)放中把它豐富完善即可。
2.4 回調(diào)狀態(tài)監(jiān)聽(tīng)
回調(diào)狀態(tài)一共有四種:
-
onStart()
:請(qǐng)求開(kāi)始(可在此展示加載動(dòng)畫(huà)) -
onSuccess()
:請(qǐng)求成功 -
onEmpty()
:請(qǐng)求成功开皿,但data
為null
或者data
是集合類型但為空 -
onFailure()
:請(qǐng)求失敗 -
onFinish()
:請(qǐng)求結(jié)束(可在此關(guān)閉加載動(dòng)畫(huà))
這里要注意onSuccess
的標(biāo)準(zhǔn):并不僅僅是 Http 請(qǐng)求的結(jié)果碼(status code)等于 200涧黄,而且要達(dá)到Api請(qǐng)求成功的標(biāo)準(zhǔn),以玩安卓的Api 為例赋荆,errorCode
為 0時(shí)笋妥,發(fā)起的請(qǐng)求才是執(zhí)行成功;否則窄潭,都應(yīng)該歸為onFailure()
的情況(可以參考文章附帶的思維導(dǎo)圖)春宣。
理清楚有幾種回調(diào)狀態(tài)后,就可以實(shí)施監(jiān)聽(tīng)了。那么在哪里監(jiān)聽(tīng)呢月帝?LiveData
的observe()
方法的第二個(gè)函數(shù)可以傳入Observer
參數(shù)躏惋。Observer
是一個(gè)接口,我們繼承它自定義一個(gè)Oberver
嚷辅,借此我們就可以監(jiān)聽(tīng)LiveData
的值的變化簿姨。
interface IStateObserver<T> : Observer<BaseResponse<T>> {
override fun onChanged(response: BaseResponse<T>?) {
when (response) {
is StartResponse -> {
//onStart()回調(diào)后不能直接就調(diào)用onFinish(),必須等待請(qǐng)求結(jié)束
onStart()
return
}
is SuccessResponse -> onSuccess(response.data)
is EmptyResponse -> onEmpty()
is FailureResponse -> onFailure(response.exception)
}
onFinish()
}
/**
* 請(qǐng)求開(kāi)始
*/
fun onStart()
/**
* 請(qǐng)求成功簸搞,且 data 不為 null
*/
fun onSuccess(data: T)
/**
* 請(qǐng)求成功扁位,但 data 為 null 或者 data 是集合類型但為空
*/
fun onEmpty()
/**
* 請(qǐng)求失敗
*/
fun onFailure(e: RequestException)
/**
* 請(qǐng)求結(jié)束
*/
fun onFinish()
}
接下來(lái)我們準(zhǔn)備一個(gè)HttpRequestCallback
類,用于實(shí)現(xiàn)DSL
的回調(diào)形式:
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
}
}
然后聲明新的監(jiān)聽(tīng)方法趁俊,考慮到某些時(shí)候需要自定義的LiveData
(比如為了解決數(shù)據(jù)倒灌的問(wèn)題)域仇,這里采用擴(kuò)展函數(shù)的寫(xiě)法,便于擴(kuò)展则酝。
/**
* 監(jiān)聽(tīng) LiveData 的值的變化殉簸,回調(diào)為 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()
}
})
}
/**
* 監(jiān)聽(tīng) 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
層的封裝
Repository
層作為數(shù)據(jù)的來(lái)源沽讹,有個(gè)兩個(gè)渠道:網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫(kù)般卑。這里暫時(shí)只處理了網(wǎng)絡(luò)請(qǐng)求爽雄。
基類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叹谁,繼續(xù)判斷 errorCode 是否為 0
emit(
when (it.success) {
true -> checkEmptyResponse(it.data)
false -> FailureResponse(handleException(RequestException(it)))
}
)
}.onFailure { throwable ->
emit(FailureResponse(handleException(throwable)))
}
}
/**
* data 為 null,或者 data 是集合類型析苫,但是集合為空都會(huì)進(jìn)入 onEmpty 回調(diào)
*/
private fun <T> checkEmptyResponse(data: T?): ApiResponse<T> =
if (data == null || (data is List<*> && (data as List<*>).isEmpty())) {
EmptyResponse()
} else {
SuccessResponse(data)
}
}
子類Repository
:
object Repository : BaseRepository() {
fun login(pwd: String) = fire {
NetworkDataSource.login(pwd)
}
}
網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)源穿扳,在這里調(diào)用網(wǎng)絡(luò)接口:
object NetworkDataSource {
private val apiService = RetrofitBuilder.create<ApiService>()
suspend fun login(pwd: String) = apiService.login(password = pwd)
}
2.6 ViewModel
層的封裝
ViewModel
基本遵循了《第一行代碼》中的寫(xiě)法衩侥,創(chuàng)建了兩個(gè)LiveData
。用戶點(diǎn)擊按鈕時(shí)矛物,loginAction
的值就會(huì)發(fā)生改變茫死,觸發(fā)switchMap
中的代碼,從而達(dá)到請(qǐng)求數(shù)據(jù)的目的履羞。
class MainViewModel : ViewModel() {
private val loginAction = MutableLiveData<Boolean>()
/**
* loginAction 在這里只傳遞布爾值峦萎,不傳遞密碼屡久,在實(shí)際項(xiàng)目中,會(huì)使用 DataBinding 綁定 xml 布局和 ViewModel骨杂,
* 不需要從 Activity 或者 Fragment 中把密碼傳入 ViewModel
*/
val loginLiveData = loginAction.switchMap {
if (it) {
Repository.login("PuKxVxvMzBp2EJM")
} else {
Repository.login("123456")
}
}
/**
* 點(diǎn)擊登錄
*/
fun login() {
loginAction.value = true
}
fun loginWithWrongPwd() {
loginAction.value = false
}
}
注意:這種寫(xiě)法通常不從
View
向ViewModel層傳遞數(shù)據(jù)涂身,是需要搭配DataBinding
的。如果你不想這樣寫(xiě)搓蚪,可以修改BaseRepository
中的返回值,直接返回BaseResponse
丁鹉。
3妒潭、思維導(dǎo)圖及源碼
最后,用一張思維導(dǎo)圖總結(jié)本文:
源碼地址:GitHub (注意分支要選擇 dev1.0)
推薦
【Android面試題】2022最新Android中高級(jí)大廠高頻面試題匯總助力金三銀四高薪必備_嗶哩嗶哩_bilibili
Android開(kāi)發(fā)進(jìn)階學(xué)習(xí)—設(shè)計(jì)思想解讀開(kāi)源框架 · 已更新至104集(持續(xù)更新中~)_嗶哩嗶哩_bilibili
Android音視頻開(kāi)發(fā):音視頻基礎(chǔ)知識(shí)到直播推流實(shí)戰(zhàn)系列教程_嗶哩嗶哩_bilibili
Android項(xiàng)目實(shí)戰(zhàn)-從0開(kāi)始手把手實(shí)現(xiàn)組件化路由SDK項(xiàng)目實(shí)戰(zhàn)_嗶哩嗶哩_bilibili
【Android開(kāi)發(fā)教程】一節(jié)課解剖Retrofit源碼內(nèi)核_嗶哩嗶哩_bilibili