Kotlin
已經成為Android
開發的Google第一推薦語言,專案中也已經使用了很長時間的kotlin了,加上Kotlin
1.3的釋出,kotlin協程也已經穩定了,難免會有一些自己的思考。
對於專案中的網路請求功能,我們也在不停的反思,如何將其寫的優雅、簡潔、快速、安全。相信這也是各位開發者在不停思考的問題。由於我們的專案都是使用的Retrofit
作為網路庫,所以,所有的思考都是基於Retrofit
展開的。
本篇文章中將會從我的思考進化歷程開始講起。涉及到Kotlin的協程、擴充套件方法、DSL,沒有基礎的小夥伴,先去了解這三樣東西,本篇文章不再進行講解。 DSL可以看看我寫這篇簡介
在網路請求中,我們需要關注的隱式問題就是:頁面生命週期的繫結,關閉頁面後需要關閉未完成的網路請求。為此,各位前輩,是八仙過海、各顯神通。我也是從學習、模仿前輩,到自我理解的轉變。
1. Callback
在最初的學習使用中,Callback
非同步方法是Retrofit
最基本的使用方式,如下:
介面:
interface DemoService {
@POST("oauth/login")
@FormUrlEncoded
fun login(@Field("name") name: String, @Field("pwd") pwd: String): Call<String>
}
複製程式碼
使用:
val retrofit = Retrofit.Builder()
.baseUrl("https://baidu.com")
.client(okHttpClient.build())
.build()
val api = retrofit.create(DemoService::class.java)
val loginService = api.login("1", "1")
loginService.enqueue(object : Callback<String> {
override fun onFailure(call: Call<String>, t: Throwable) {
}
override fun onResponse(call: Call<String>, response: Response<String>) {
}
})
複製程式碼
這裡不再細說。
在關閉網路請求的時候,需要在onDestroy
中呼叫cancel
方法:
override fun onDestroy() {
super.onDestroy()
loginService.cancel()
}
複製程式碼
這種方式,容易導致忘記呼叫cancel
方法,而且網路操作和關閉請求的操作是分開的,不利於管理。
這當然不是優雅的方法。隨著Rx的火爆,我們專案的網路請求方式,也逐漸轉為了Rx的方式
2. RxJava
此種使用方式,百度一下,到處都是教程講解,可見此種方式起碼是大家較為認可的一種方案。
在Rx的使用中,我們也嘗試了各種各樣的封裝方式,例如自定義Subscriber
,將onNext
、onCompleted、
onError進行拆分組合,滿足不同的需求。
首先在Retrofit
裡新增Rx轉換器RxJava2CallAdapterFactory.create()
:
addCallAdapterFactory(RxJava2CallAdapterFactory.create())
複製程式碼
RxJava的使用方式大體如下,先將介面的Call
改為Observable
:
interface DemoService {
@POST("oauth/login")
@FormUrlEncoded
fun login(@Field("name") name: String, @Field("pwd") pwd: String): Observable<String>
}
複製程式碼
使用:(配合RxAndroid繫結宣告週期)
api.login("1","1")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) //RxAndroid
.subscribe(object :Observer<String> {
override fun onSubscribe(d: Disposable) {
}
override fun onComplete() {
}
override fun onNext(t: String) {
}
override fun onError(e: Throwable) {
}
})
複製程式碼
這種使用方式確實方便了不少,響應式程式設計的思想也很優秀,一切皆為事件流。通過RxAndroid
來切換UI執行緒和繫結頁面生命週期,在頁面關閉的時候,自動切斷向下傳遞的事件流。
RxJava
最大的風險即在於記憶體洩露,而RxAndroid
確實規避了一定的洩露風險。
並且通過檢視RxJava2CallAdapterFactory
的原始碼,發現也確實呼叫了cancel
方法,嗯……貌似不錯呢。
但總是覺得RxJava過於龐大,有些大材小用。
3. LiveData
隨著專案的的推進和Google全家桶的釋出。一個輕量化版本的RxJava
進入到了我們視線,那就是LiveData
,LiveData
借鑑了很多RxJava
的的設計思想,也是屬於響應式程式設計的範疇。LiveData
的最大優勢即在於響應Acitivty
的生命週期,不用像RxJava
再去繫結宣告週期。
同樣的,我們首先需要新增LiveDataCallAdapterFactory (連結裡是google官方提供的寫法,可直接拷貝到專案中),用於把retrofit的Callback
轉換為LiveData
:
addCallAdapterFactory(LiveDataCallAdapterFactory.create())
複製程式碼
介面改為:
interface DemoService {
@POST("oauth/login")
@FormUrlEncoded
fun login(@Field("name") name: String, @Field("pwd") pwd: String): LiveData<String>
}
複製程式碼
呼叫:
api.login("1", "1").observe(this, Observer {string ->
})
複製程式碼
以上就是最基礎的使用方式,在專案中使用時候,通常會自定義Observer
,用來將各種資料進行區分。
在上面呼叫的observe
方法中,我們傳遞了一個this
,這個this
指的是宣告週期,一般我們在AppCompatActivity
中使用時,直接傳遞其本身就可以了。
下面簡單跳轉原始碼進行說明下。通過檢視原始碼可以發現:
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer)
複製程式碼
其this
本身是傳遞的LifecycleOwner
。
那麼我們在一層層跳轉AppCompatActivity
,會發現AppCompatActivity
是繼承於SupportActivity
的父類:
public class SupportActivity extends Activity implements LifecycleOwner, Component
複製程式碼
其本身對LifecycleOwner
介面進行了實現。也就是說,除非特殊要求,一般我們只需要傳遞其本身就可以了。LiveData
會自動處理資料流的監聽和解除繫結。
通常來說:在onCreate
中對資料進行一次性的繫結,後面就不需要再次繫結了。
當生命週期走到onStart
和onResume
的時候,LiveData
會自動接收事件流;
當頁面處於不活動的時候,將會暫停接收事件流,頁面恢復時恢復資料接收。(例如A跳轉到B,那麼A將會暫停接收。當從B回到A以後,將恢復資料流接收)
當頁面onDestroy
時候,會自動刪除觀察者,從而中斷事件流。
可以看出LiveData
作為官方套件,使用簡單,生命週期的響應也是很智慧的,一般都不需要額外處理了。
(更高階的用法,可以參考官方Demo,可以對資料庫快取等待都進行一整套的響應式封裝,非常nice。建議學習下官方的封裝思想,就算不用,也是對自己大有裨益)
4. Kotlin協程
上面說了那麼多,這裡步入了正題。大家仔細觀察下會發現,上面均是使用的Retrofit
的enqueue
非同步方法,再使用Callback
進行的網路回撥,就算是RxJava和Livedata的轉換器,內部其實也是使用的Callback
。在此之前,Retrofit
的作者也寫了一個協程的轉換器,地址在這,但內部依然使用的是Callback
,本質均為一樣。(目前該庫才被廢棄,其實我也覺得這樣使用協程就沒意義了,Retrofit
在最新的2.6.0版本,直接支援了kotlin協程的suspend
掛起函式),
之前瞭解Retrofit
的小夥伴應該知道,Retrofit
是有同步和非同步兩種呼叫方式的。
void enqueue(Callback<T> callback);
複製程式碼
上面這就是非同步呼叫方式,傳入一個Callback
,這也是我們最最最常用到的方式。
Response<T> execute() throws IOException;
複製程式碼
上面這種是同步呼叫方法,會阻塞執行緒,返回的直接就是網路資料Response
,很少使用。
後來我就在思考,能不能結合kotlin的協程,拋棄Callback
,直接使用Retrofit
的同步方法,把非同步當同步寫,程式碼順序書寫,邏輯清晰,效率高,同步的寫法就更加方便物件的管理。
說幹就幹。
首先寫一個協程的擴充套件方法:
val api = ……
fun <ResultType> CoroutineScope.retrofit() {
this.launch(Dispatchers.Main) {
val work = async(Dispatchers.IO) {
try {
api.execute() // 呼叫同步方法
} catch (e: ConnectException) {
e.logE()
println("網路連線出錯")
null
} catch (e: IOException) {
println("未知網路錯誤")
null
}
}
work.invokeOnCompletion { _ ->
// 協程關閉時,取消任務
if (work.isCancelled) {
api.cancel() // 呼叫 Retrofit 的 cancel 方法關閉網路
}
}
val response = work.await() // 等待io任務執行完畢返回資料後,再繼續後面的程式碼
response?.let {
if (response.isSuccessful) {
println(response.body()) //網路請求成功,獲取到的資料
} else {
// 處理 HTTP code
when (response.code()) {
401 -> {
}
500 -> {
println("內部伺服器錯誤")
}
}
println(response.errorBody()) //網路請求失敗,獲取到的資料
}
}
}
}
複製程式碼
上面就是核心程式碼,主要的意思都寫了註釋。整個工作流程是出於ui協程中,所以可以隨意操作UI控制元件,接著在io執行緒中去同步呼叫網路請求,並且等待io執行緒的執行完畢,接著再拿到結果進行處理,整個流程都是基於同步程式碼的書寫方式,一步一個流程,沒有回掉而導致的程式碼割裂感。那麼繼續,我們想辦法把獲取的資料返回出去。
這裡我們採用DSL方法,首先自定義一個類:
class RetrofitCoroutineDsl<ResultType> {
var api: (Call<ResultType>)? = null
internal var onSuccess: ((ResultType?) -> Unit)? = null
private set
internal var onComplete: (() -> Unit)? = null
private set
internal var onFailed: ((error: String?, code, Int) -> Unit)? = null
private set
var showFailedMsg = false
internal fun clean() {
onSuccess = null
onComplete = null
onFailed = null
}
fun onSuccess(block: (ResultType?) -> Unit) {
this.onSuccess = block
}
fun onComplete(block: () -> Unit) {
this.onComplete = block
}
fun onFailed(block: (error: String?, code, Int) -> Unit) {
this.onFailed = block
}
}
複製程式碼
此類對外暴露了三個方法:onSuccess
,onComplete
,onFailed
,用於分類返回資料。
接著,我們對我們的核心程式碼進行改造,將方法進行傳遞:
fun <ResultType> CoroutineScope.retrofit(
dsl: RetrofitCoroutineDsl<ResultType>.() -> Unit //傳遞方法,需要哪個,傳遞哪個
) {
this.launch(Dispatchers.Main) {
val retrofitCoroutine = RetrofitCoroutineDsl<ResultType>()
retrofitCoroutine.dsl()
retrofitCoroutine.api?.let { it ->
val work = async(Dispatchers.IO) { // io執行緒執行
try {
it.execute()
} catch (e: ConnectException) {
e.logE()
retrofitCoroutine.onFailed?.invoke("網路連線出錯", -100)
null
} catch (e: IOException) {
retrofitCoroutine.onFailed?.invoke("未知網路錯誤", -1)
null
}
}
work.invokeOnCompletion { _ ->
// 協程關閉時,取消任務
if (work.isCancelled) {
it.cancel()
retrofitCoroutine.clean()
}
}
val response = work.await()
retrofitCoroutine.onComplete?.invoke()
response?.let {
if (response.isSuccessful) {
retrofitCoroutine.onSuccess?.invoke(response.body())
} else {
// 處理 HTTP code
when (response.code()) {
401 -> {
}
500 -> {
}
}
retrofitCoroutine.onFailed?.invoke(response.errorBody(), response.code())
}
}
}
}
}
複製程式碼
這裡使用DSL傳遞方法,可以更具需要傳遞的,例如只需要onSuccess
,那就只傳遞這一個方法,不必三個都傳遞,按需使用。
使用方式:
首先需要按照kotlin的官方文件來改造下activity:
abstract class BaseActivity : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job // 定義job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job // Activity的協程
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
job.cancel() // 關閉頁面後,結束所有協程任務
}
}
複製程式碼
Activity
實現CoroutineScope
介面,就能直接根據當前的context
獲取協程使用。
接下來就是真正的使用,在任意位置即可呼叫此擴充套件方法:
retrofit<String> {
api = api.login("1","1")
onComplete {
}
onSuccess { str ->
}
onFailed { error, code ->
}
}
複製程式碼
在有的時候,我們只需要處理onSuccess
的情況,並不關心其他兩個。那麼直接寫:
retrofit<String> {
api = api.login("1","1")
onSuccess { str ->
}
}
複製程式碼
需要哪個寫哪個,程式碼非常整潔。
可以看出,我們不需要單獨再對網路請求進行生命週期的繫結,在頁面被銷燬的時候,job
也就被關閉了,當協程被關閉後,會執行呼叫 Retrofit 的 cancel 方法關閉網路。
5. 小節
協程的開銷是小於Thread
多執行緒的,響應速度很快,非常適合輕量化的工作流程。對於協程的使用,還有帶我更深入的思考和學習。協程並不是Thread
的替代品,還是多非同步任務多一個補充,我們不能按照慣性思維去理解協程,而是要多從其本身特性入手,開發出它更安逸的使用方式。
而且隨著Retrofit 2.6.0
的釋出,自帶了新的協程方案,如下:
@GET("users/{id}")
suspend fun user(@Path("id") long id): User
複製程式碼
增加了suspend
掛起函式的支援,可見協程的應用會越來越受歡迎。
上面所說的所有網路處理方法,不論是Rx
還是LiveData
,都是很好的封裝方式,技術沒有好壞之分。我的協程封裝方式,也許也不是最好的,但是我們不能缺乏思考、探索、實踐三要素,去想去做。
最好的答案,永遠都是自己給出的。
第一次寫這種型別的文章記錄,流程化比較嚴重,記錄不嚴謹,各位見諒。謝謝大家的閱讀