Retrofit 是Square公司為Android和Java開發的類型安全的Http Client。Retrofit 專注于接口的封裝,OkHttp 專注于網絡請求的高效,二者分工協作!使用 Retrofit 請求網絡,實際上是先使用 Retrofit 接口層封裝請求參數、Header、Url 等信息,然后由 OkHttp 完成后續的請求操作,最后在服務端返回數據之后,OkHttp 將原始的結果交給 Retrofit,Retrofit 根據用戶的需求對結果進行解析
Retrofit 雖然使用起來還算簡單,但每個接口都需要寫回調函數比較繁瑣,就算使用協程的掛起函數簡化了寫法,但處理請求錯誤、請求動畫、協程的創建與切換等操作還是使得一個簡單的請求需要寫一大篇額外代碼,本篇主要是通過函數式接口簡化了這些代碼的編寫,廢話不多說直接上代碼
用到的依賴和權限
在AndroidManifest.xml
中添加
<uses-permission android:name="android.permission.INTERNET" />
在build.gradle文件的dependencies中添加
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
自定義協程異常處理
創建自定義異常RequestException
作為請求時服務器內部錯誤使用
class RequestException constructor(
response: String
) : RuntimeException(response)
創建單例CoroutineHandler
作為協程異常處理類
object CoroutineHandler: AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) {
// 打印
exception.printStackTrace()
// 處理
when(exception.javaClass.name) {
ConnectException::class.java.name -> ToastUtil.show("請求異常,請檢查網絡")
RequestException::class.java.name -> {
// 處理服務器錯誤
}
...
}
context.cancel()
}
}
自定義數據轉換工廠
簡單封裝一下Gson
object JsonUtil {
val gson: Gson = Gson()
fun <T> object2Json(obj: T): String = gson.toJson(obj)
fun <T> json2Object(json: String, obj: Type): T = gson.fromJson(json, obj)
fun <T> json2Object(json: String, obj: Class<T>): T = gson.fromJson(json, obj)
fun <T> json2List(json: String): List<T> {
return gson.fromJson(json, object : TypeToken<LinkedList<T>>() {}.type)
}
fun <T> list2Json(list: List<T>): String {
return object2Json(list)
}
}
服務器返回的數據結構
data class Response<T> (
val code: Int,
val message: String,
val result: T
)
創建GsonResponseBodyConverter
,用作Response數據轉換器
class GsonResponseBodyConverter<T>(
private val type: Type
) : Converter<ResponseBody, T> {
override fun convert(value: ResponseBody): T {
val response = value.string()
LogUtil.dj(response)
val httpResult = JsonUtil.json2Object(response, Response::class.java)
// 這里是定義成code 200為正常,不正常則拋出之前定義好的異常,在自定義的協程異常處理類中處理
return if (httpResult.code == 200) {
JsonUtil.json2Object(response, type)
} else {
throw RequestException(response)
}
}
}
創建GsonRequestBodyConverter,用作Request的數據轉換
class GsonRequestBodyConverter<T>(
type: Type
) : Converter<T, RequestBody> {
private val gson: Gson = JsonUtil.gson
private val adapter: TypeAdapter<T> = gson.getAdapter(TypeToken.get(type)) as TypeAdapter<T>
override fun convert(value: T): RequestBody? {
val buffer = Buffer()
val writer: Writer =
OutputStreamWriter(buffer.outputStream(), Charset.forName("UTF-8"))
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return RequestBody.create(
MediaType.get("application/json; charset=UTF-8"),
buffer.readByteString()
)
}
}
創建自定義數據轉換工廠GsonResponseConverterFactory
,這里使用了剛才創建的轉換器
object GsonResponseConverterFactory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation?>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return GsonResponseBodyConverter<Type>(type)
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<out Annotation>,
methodAnnotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> {
return GsonRequestBodyConverter<Type>(type)
}
}
配置Retrofit和OkHttp
創建RetrofitClient
進行Retrofit和OkHttp配置,并提供API調用
object RetrofitClient {
private const val Authorization = "Authorization"
private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain: Interceptor.Chain ->
// 這里配置了統一攔截器用以添加token 如果不需要可以去掉
val request = chain.request().newBuilder().apply {
PreferenceManager.getToken()?.let {
addHeader(Authorization, it)
}
}.build()
LogUtil.d("request: ${request.method()} ${request.url()} ${request.body()}\n" +
"headers: ${request.headers()}")
chain.proceed(request)
}
.build()
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("http://poetry.apiopen.top/")
// 放入之前寫好的數據轉換工廠
.addConverterFactory(GsonResponseConverterFactory)
.client(okHttpClient)
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
創建ApiService用來編寫服務器的接口提供給retrofit.create()
使用,具體語法可以參考Retrofit官網
interface ApiService {
// 直接使用的網上接口,用作測試
@GET("sentences")
suspend fun test(): Response<User>
}
測試用的實體類
data class User(
private var name: String,
private var from: String
)
寫到這里,已經通過自定義的數據轉換和協程異常處理簡化了驗證數據的一部分代碼了,創建協程包裹住然后通過RetrofitClient.apiService.test()
已經可以直接發起請求了,如下所示:
// 這里使用了剛才寫的CoroutineHandler對異常進行處理
CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
val test = RetrofitClient.apiService.test()
LogUtil.d("[當前線程為:${Thread.currentThread().name}], 獲得數據:${test}")
}
打印的數據如下,可以看到獲取到的數據已經是可以直接使用的數據,但代碼還是不夠簡潔,每次都需要手動創建一個協程,以及寫上RetrofitClient.apiService
,如果想讓用戶體驗更好還要再加上請求動畫的代碼。總體來說還是有一些繁瑣,是否能把這些也省掉呢?我想應該是可以的。
通過函數式接口簡化重復代碼
函數式接口的作用是什么?簡單以下面代碼來說,可以在使用HttpPredicate
做參數的方法里傳入一段代碼,這段代碼里可以使用execute
方法的參數,這段代碼會傳入調用HttpPredicate.execute
的地方,是不是剛好符合要簡化的需求?把具體要寫的代碼提出來,把重復的業務邏輯放進去,就大功告成了
用 fun 修飾符可以在 Kotlin 中聲明一個函數式接口,這里創建函數式接口HttpPredicate
,用suspend
聲明這是掛起函數
fun interface HttpPredicate {
suspend fun execute(api: ApiService)
}
創建單例類HttpRequest
把重復的邏輯抽取出來,這里創建兩個方法,execute
是有請求動畫的,executeAsync
是沒有的,DialogManager.showRound()
請求動畫的具體實現可以查看《Android 用 AlertDialog 實現頁面等待提示》
object HttpRequest {
fun execute(http: HttpPredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
val showRound = DialogManager.showRound()
try {
http.execute(RetrofitClient.apiService)
} catch (e: Exception) {
throw e
} finally {
showRound?.let {
if (it.isShowing) {
it.cancel()
}
}
}
}
fun executeAsync(http: HttpPredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
http.execute(RetrofitClient.apiService)
}
}
現在可以把之前的請求接口代碼用函數式接口這種方式來寫,省略了協程和請求動畫的代碼
HttpRequest.execute(object : HttpPredicate {
override suspend fun execute(api: ApiService) {
val test = api.test()
LogUtil.d("[當前線程為:${Thread.currentThread().name}], 獲得數據:${test}")
}
})
對于函數式接口,我們可以通過 lambda 表達式實現 SAM 轉換,從而使代碼更簡潔、更有可讀性,最終簡化后的請求接口代碼如下:
HttpRequest.execute {
val test = it.test()
LogUtil.d("[當前線程為:${Thread.currentThread().name}], 獲得數據:${test}")
}
不需要請求動畫使用executeAsync
,不需要更新UI則去掉代碼里的線程切換即可。另外為了非請求接口時使用協程的方便也可以把協程的調用單獨創建一個工具類,替換HttpRequest
類中的協程調用,這樣協程的異常全部由CoroutineHandler
類來處理了,無需再單獨處理。
object CoroutineUtil {
fun interface CoroutinePredicate {
suspend fun execute()
}
fun execMain(code: CoroutinePredicate) = CoroutineScope(Dispatchers.Main).launch(CoroutineHandler) {
code.execute()
}
fun execIO(code: CoroutinePredicate) = CoroutineScope(Dispatchers.IO).launch(CoroutineHandler) {
code.execute()
}
}
函數式接口還可以免去創建用Lambda表達式來省略,這里就不描述了。