作者:limuyang2,連結:https://juejin.im/post/5cfb38f96fb9a07eeb139a00
Kotlin已經成為Android開發的Google第一推薦語言,項目中也已經使用了很長時間的kotlin了,加上Kotlin1.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的作者也寫了一個協程的轉換器,地址在這:https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter,但內部依然使用的是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,都是很好的封裝方式,技術沒有好壞之分。我的協程封裝方式,也許也不是最好的,但是我們不能缺乏思考、探索、實踐三要素,去想去做。
最好的答案,永遠都是自己給出的。