Kotlin
已經成爲Android
開發的Google第一推薦語言,項目中也已經使用了很長時間的kotlin了,加上Kotlin
1.3的發佈,kotlin協程也已經穩定了,不免會有一些本身的思考。java
對於項目中的網絡請求功能,咱們也在不停的反思,如何將其寫的優雅、簡潔、快速、安全。相信這也是各位開發者在不停思考的問題。因爲咱們的項目都是使用的Retrofit
做爲網絡庫,因此,全部的思考都是基於Retrofit
展開的。android
本篇文章中將會從個人思考進化歷程開始講起。涉及到Kotlin的協程、擴展方法、DSL,沒有基礎的小夥伴,先去了解這三樣東西,本篇文章再也不進行講解。 DSL能夠看看我寫這篇簡介git
在網絡請求中,咱們須要關注的隱式問題就是:頁面生命週期的綁定,關閉頁面後須要關閉未完成的網絡請求。爲此,各位前輩,是八仙過海、各顯神通。我也是從學習、模仿前輩,到自我理解的轉變。github
在最初的學習使用中,Callback
異步方法是Retrofit
最基本的使用方式,以下:數據庫
接口:編程
interface DemoService {
@POST("oauth/login")
@FormUrlEncoded
fun login(@Field("name") name: String, @Field("pwd") pwd: String): Call<String>
}
複製代碼
使用:api
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的方式
此種使用方式,百度一下,處處都是教程講解,可見此種方式起碼是你們較爲承認的一種方案。
在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過於龐大,有些大材小用。
隨着項目的的推動和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。建議學習下官方的封裝思想,就算不用,也是對本身大有裨益)
上面說了那麼多,這裏步入了正題。你們仔細觀察下會發現,上面均是使用的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 方法關閉網絡。
協程的開銷是小於Thread
多線程的,響應速度很快,很是適合輕量化的工做流程。對於協程的使用,還有帶我更深刻的思考和學習。協程並非Thread
的替代品,仍是多異步任務多一個補充,咱們不能按照慣性思惟去理解協程,而是要多從其自己特性入手,開發出它更安逸的使用方式。 並且隨着Retrofit 2.6.0
的發佈,自帶了新的協程方案,以下:
@GET("users/{id}")
suspend fun user(@Path("id") long id): User
複製代碼
增長了suspend
掛起函數的支持,可見協程的應用會愈來愈受歡迎。
上面所說的全部網絡處理方法,不管是Rx
仍是LiveData
,都是很好的封裝方式,技術沒有好壞之分。個人協程封裝方式,也許也不是最好的,可是咱們不能缺少思考、探索、實踐三要素,去想去作。
最好的答案,永遠都是本身給出的。
第一次寫這種類型的文章記錄,流程化比較嚴重,記錄不嚴謹,各位見諒。謝謝你們的閱讀