溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

攜程機票Android Jetpack與Kotlin Coroutines的方法教程

發(fā)布時間:2021-10-23 09:51:13 來源:億速云 閱讀:129 作者:iii 欄目:web開發(fā)

這篇文章主要講解了“攜程機票Android Jetpack與Kotlin Coroutines的方法教程”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“攜程機票Android Jetpack與Kotlin Coroutines的方法教程”吧!

一、前言

1.1 技術(shù)背景與選型

自 2017年 Google IO 大會以來,經(jīng)過三年的發(fā)展,Kotlin 已成為 Android  平臺無爭議的首選開發(fā)語言。但是相比語言本身,Kotlin 1.2 版本后進入 stable 狀態(tài)的協(xié)程(coroutines)的行業(yè)采用率仍然較低。

協(xié)程的優(yōu)勢主要有:

  • 更簡單的異步并發(fā)實現(xiàn)方式(近似于同步寫法)

  • 更便捷的任務管理

  • 更便捷的生產(chǎn)者-消費者模式實現(xiàn)

  • 更高效的 cold stream 實現(xiàn)(即 Flow,根據(jù)官方數(shù)據(jù),F(xiàn)low 在部分 benchmarks 場景下效率是 RxJava  的兩倍,詳見參考鏈接 1)。

Google Android 團隊同時也在大力推廣 Jetpack 組件庫,其中 AAC 架構(gòu)組件帶來了全新的應用架構(gòu)實現(xiàn)方式,可以更便捷的實現(xiàn) MVVM  這一非常適用于復雜業(yè)務場景的設(shè)計模式。

1.2 業(yè)務背景

今年接到一個大需求,產(chǎn)品方向上希望嘗試一種交通類業(yè)務融合的平臺化搜索首頁新體驗。于是各業(yè)務研發(fā)團隊經(jīng)過幾輪技術(shù)評估,決定聯(lián)合啟動開發(fā)這個新項目。借此機會,機票  App 團隊決定基于 Android Jetpack AAC 組件庫和 Kotlin Coroutines 技術(shù)方案進行重構(gòu)實現(xiàn)。

機票首頁的業(yè)務邏輯可以歸納抽象為以下兩種場景:

  • 多個不同 View,依賴同一個數(shù)據(jù)源的變化。

  • 多個不同 View,當用戶操作時,都會觸發(fā)同一數(shù)據(jù)源的變更。

針對這兩個場景,基于 ViewModel、LiveData 實現(xiàn)的 MVVM 模式非常契合,可以做到業(yè)務邏輯清晰且代碼耦合度低。ViewModel  表示一個業(yè)務模塊相關(guān)數(shù)據(jù)狀態(tài)的總集,同時向 View 暴露諸多數(shù)據(jù)狀態(tài)需要響應 View 的操作時調(diào)用的接口。而從屬于 ViewModel 下的  LiveData 則表示各個數(shù)據(jù)狀態(tài)本身,并提供給 View 訂閱。

在代碼實現(xiàn)中,我們在多個 View 中可以使用相同的 ViewModelStoreOwner(一般是 Fragment 或 Activity)獲取到同一個  ViewModel 對象,只要多個 View 訂閱同一個 ViewModel 中相同的 LiveData,并在數(shù)據(jù)狀態(tài)需要響應 UI操作而更新的時候調(diào)用  ViewModel 中的同一個函數(shù),即可清晰簡潔的應對這兩種場景。

同時復盤當前機票首頁的代碼歷史債:

  • 代碼冗長,沒有合理的封裝、拆分以及架構(gòu)模式,單文件代碼行數(shù)高。

  • 復雜的異步操作導致回調(diào)代碼層層嵌套。

  • 不恰當?shù)木€程池配置。

  • 重復多余的 null 檢查與可能暗藏的 null 安全問題。

  • 過多的 UI 層級嵌套,代碼冗雜且性能不高。

  • 仍在使用一些 Google 官方淘汰的舊技術(shù),沒有及時跟進新技術(shù)。

通過合理的封裝、拆分以及使用 ViewModel 與 LiveData 可以方便的解決問題 1;

Kotlin 自身的空安全特性解決了問題 4;

問題 5 與 6 主要通過合理的重構(gòu)以及使用 ConstraintLayout 等新技術(shù)來解決,但不在本文的討論范圍。

那么問題 2 與 3 的解決,就需要 Kotlin 協(xié)程出場了。

二. 熱身準備

2.1 拋磚引玉

在具體講解實現(xiàn)之前,先通過一個小例子拋磚引玉,來說明一個小問題。

如果我們在一個 Fragment 中或 Activity 中要獲取一個 ViewModel,然后訂閱它內(nèi)部的 LiveData,如果直接使用官方的 API  通常是這樣的:

private lateinit var myViewModel: MyViewModel    ......      myViewModel = ViewModelProvider(this)[MyViewModel::class.java]   myViewModel.liveData1.observer(this, Observe {     doSomething1(it)   })   myViewModel.liveData2.observer(this, Observe {     doSomething2(it)   })    ......

由于 Kotlin 的 lambda 表達式與操作符重載,這段代碼已經(jīng)比對應的 Java 代碼簡潔多了,但是這段代碼仍然不夠 Kotlin  style,我們稍微封裝一下,定義兩個新函數(shù):

// 頂層函數(shù)版本 inline fun <reified T : ViewModel> getViewModel(owner: ViewModelStoreOwner, configLiveData: T.() -> Unit = {}): T =         ViewModelProvider(owner)[T::class.java].apply { configLiveData() }  // 擴展函數(shù)版本 inline fun <reified T : ViewModel> ViewModelStoreOwner.getSelfViewModel(configLiveData: T.() -> Unit = {}): T =         getViewModel(this, configLiveData)

為了不同的使用場景并且方便不同人的使用習慣,這里同時寫了頂層函數(shù)版本與擴展函數(shù)版本,但是功能一模一樣(擴展函數(shù)版本直接調(diào)用了頂層函數(shù)版本)?,F(xiàn)在如果我們要在  Fragment 中獲取 ViewModel,看看會變成什么樣(這里使用擴展函數(shù)版本):

private lateinit var myViewModel: MyViewModel  ......  myViewModel = getSelfViewModel {     liveData1.observe(this@MyFragment, Observer {         doSomething1(it)     })     liveData2.observe(this@MyFragment, Observer {         doSomething2(it)     })     ...... }

這樣封裝的好處絕不僅僅在于讓代碼看起來“DSL”化。首先,內(nèi)聯(lián)的泛型實化函數(shù)讓我們避免去編寫 xxx::class.java  這樣的樣板式代碼,而是只需要傳一個泛型參數(shù)(在這個例子中由于 lateinit  屬性已經(jīng)聲明了類型,所以根據(jù)類型推導,我們連泛型參數(shù)都不必顯式寫出),這樣看起來會優(yōu)雅的多。其次,我們配合使用了帶接受者的 lambda 表達式與作用域函數(shù)  apply 使我們在獲取 ViewModel 內(nèi)的 LiveData 對象的時候不再需要重復寫多次 myViewModel. 這樣的樣板代碼。

最后從代碼結(jié)構(gòu)來看,我們通常在獲取到 ViewModel 對象后會直接訂閱所有需要訂閱的 LiveData,我們把所有的訂閱邏輯都寫到了  getSelfViewModel 函數(shù)的 lambda 表達式參數(shù)的作用域內(nèi),這樣我們對訂閱的代碼可以更加一目了然。

這里只是個拋磚引玉,在我們決定要開始使用 Kotlin 來替換 Java 的時候,最好能先打牢 Kotlin  基礎(chǔ),這樣我們才能發(fā)揮這門語言的最大潛力。從而避免使用 Kotlin 寫出 Java 風格的代碼。

2.2 代碼角色劃分

如果把當前的代碼按職責進行劃分,大概有以下幾種:數(shù)據(jù)類(data class,類似于 Java  Bean)、工具函數(shù)(例如格式化一個日期,將其轉(zhuǎn)換為可展示的字符串)、數(shù)據(jù)源(例如從網(wǎng)絡拉取數(shù)據(jù)或從本地數(shù)據(jù)庫讀取數(shù)據(jù))、核心業(yè)務邏輯(在拿到原始數(shù)據(jù)后我們可能要對它根據(jù)業(yè)務需求進行處理)、UI代碼(無須多言)、狀態(tài)信息(通常是一些用于表示狀態(tài)的可變對象等等或者數(shù)據(jù)的當前狀態(tài))。

我們要將以上這幾種代碼劃分為三個角色,或者劃歸到三個范圍內(nèi),即:View、ViewModel、Model,也就是 MVVM 模式中三大角色。UI  代碼劃歸到 View;數(shù)據(jù)類、數(shù)據(jù)源劃規(guī)到 Model;而數(shù)據(jù)狀態(tài)或其他狀態(tài)信息劃歸到 ViewModel。而工具函數(shù)視情況而定,可以作為獨立組件也可以放到  Model 中。

三、正式實現(xiàn)

3.1 協(xié)程 Channel 與 LiveData 組合實現(xiàn)的基本模式

在 MVVM 模式中,VM 即 ViewModel  表示數(shù)據(jù)狀態(tài)。為了讓業(yè)務邏輯和代碼結(jié)構(gòu)更加合理。我們通常將一些彼此依賴對方狀態(tài)的數(shù)據(jù)(通常其表示的業(yè)務也是強相關(guān)的)拆分到同一個 ViewModel 中。而  LiveData (通常位于 ViewModel 內(nèi)部)表示的是某些具體的數(shù)據(jù)狀態(tài)。例如在攜程機票首頁的業(yè)務中,出發(fā)城市的相關(guān)數(shù)據(jù)就可以用一個 LiveData  來表示,到達城市則用另一個 LiveData 來表示,而這兩個 LiveData 都位于同一個 ViewModel 中。

如果不使用 livedata-ktx 包,我們創(chuàng)建 LiveData 對象的方式主要是通過調(diào)用 MutableLiveData  類的構(gòu)造方法,我們通過直接使用 MutableLiveData 對象來進行訂閱、數(shù)據(jù)更新等操作。MutableLiveData  與普通對象一樣,我們可以在任意一種異步框架下使用它。

但為了與 Kotlin 協(xié)程有更完美的配合,livedata-ktx 包提供給我們了另一種方式來創(chuàng)建 LiveData,即 liveData {}  函數(shù),該函數(shù)的函數(shù)簽名是這樣的:

fun <T> liveData(     context: CoroutineContext = EmptyCoroutineContext,     timeoutInMs: Long = DEFAULT_TIMEOUT,     @BuilderInference block: suspend LiveDataScope<T>.() -> Unit ): LiveData<T>

先看第三個參數(shù) block,它是一個 suspend lambda 表達式,也就是說,它運行在協(xié)程中。第一個參數(shù) context  通常用于指定這個協(xié)程執(zhí)行的調(diào)度器,而 timeoutInMs 用于指定超時時間,當這個 LiveData  沒有活躍的觀察者的時候,時間如果超過超時時間,該協(xié)程就會被取消。由于第一和第二個參數(shù)都有默認值,所以大多數(shù)情況下,我們只需要傳第三個參數(shù)。

liveData {} 函數(shù)在官方文檔中并沒有給出用例,所以并沒有一個所謂標準的“官方”用法。我們觀察了一下發(fā)現(xiàn),block 塊是一個帶接收者的  lambda,而接收者類型是 LiveDataScope,且 LiveDataScope 有一個成員函數(shù) emit,這就和 RxJava 的 create  操作符非常相似,更和 Flow 中的 flow {} 函數(shù)如出一轍。所以,如果要讓我們的 LiveData 作為一個可持續(xù)發(fā)射數(shù)據(jù)的數(shù)據(jù)源,liveData  {} 函數(shù)啟動的這個協(xié)程需要不停的從外部取數(shù)據(jù),這種場景正是協(xié)程中 Channel (參考鏈接2)的用武之地,我們用上述的技術(shù)編寫一個簡單的  ViewModel:

class CityViewModel : ViewModel() {          private val departCityTextChannel = Channel<String>(1)     val departCityTextLiveData = liveData {         for (result in departCityTextChannel)             emit(result)     }        // 外部的 UI 通過調(diào)用該方法來更新數(shù)據(jù)     fun updateCityUI() = viewModelScope.launch(Dispatchers.IO) {         val result = fetchData() // 拉取數(shù)據(jù)         departCityTextChannel.send(result)     } }

首先我們聲明并初始化了一個 Channel &mdash;&mdash;departCityTextChannel。然后我們使用 liveData {}  函數(shù)創(chuàng)建了LiveData 對象,在 liveData {} 函數(shù)啟動的協(xié)程內(nèi),我們通過無限循環(huán)不停的從 departCityTextChannel  中取數(shù)據(jù),如果取不到,這個協(xié)程就會被掛起,直到有數(shù)據(jù)到來(這比用 Java 線程加 BlockQueue 實現(xiàn)的類似的生產(chǎn)者消費者模式要高效很多)。for  循環(huán)對 Channel 有一等的支持。

如果 UI 要更新數(shù)據(jù),會調(diào)用 updateCityUI() 函數(shù),該函數(shù)內(nèi)的所有操作(通常都是耗時的)在其啟動的協(xié)程內(nèi)異步進行。在這里我們通過  viewmodel-ktx 包提供的 viewModelScope 來啟動協(xié)程,這個協(xié)程作用域的實現(xiàn)與 ViewModel 的實現(xiàn)相結(jié)合,可以通過  ViewModel 感知到外部 UI 組件的生命周期,從而幫助我們自動取消任務。

最后注意一點,我們在初始化 departCityTextChannel 時給工廠函數(shù) Channel(1)傳入的緩沖區(qū) size 的大小是  1。這主要是為了我們可以避免生產(chǎn)者協(xié)程在等待消費者從  Channel中取走數(shù)據(jù)時發(fā)生事實上的掛起,從而在一定程度上影響效率。當然如果有生產(chǎn)者生產(chǎn)的速度過快,而消費者消費的速度過慢而明顯跟不上的時候,我們可以適當調(diào)大  size 的值。

我們的每個 LiveData 幾乎都需要與其配合使用的 Channel,而且 liveData {} 函數(shù)做的事情也幾乎都是一樣的,即使用 for 循環(huán)從  Channel 拿到數(shù)據(jù)然后再使用 emit 函數(shù)發(fā)射出去。于是可以進行如下的封裝:

inline val <T> Channel<T>.coroutineLiveData: LiveData<T>     get() = liveData {         for (entry in this@coroutineLiveData)             emit(entry)     }

ViewModel 內(nèi)創(chuàng)建 departCityTextChannel 與 departCityTextLiveData 對象的代碼就變成了這樣:

class CityViewModel : ViewModel() {          private val departCityTextChannel = Channel<String>(1)     val departCityTextLiveData = departCityTextChannel.coroutineLiveData          ...... 省略其他代碼

我們封裝了一個名為 coroutineLiveData 的內(nèi)聯(lián)擴展屬性,它的 getter 已經(jīng)將 LiveData  的創(chuàng)建邏輯封裝好了,不過請注意,每次調(diào)用這個屬性,實際上都返回了一個新的 LiveData 對象,所以正確的做法是在調(diào)用 coroutineLiveData  屬性后,把它的結(jié)果保存下來,以此達到重復使用的目的,千萬不要每次都使用 departCityTextChannel.coroutineLiveData  這樣的方式來期望獲取到同一個 LiveData 對象。當然,如果你覺得這樣也許會有誤導,也可以把 coroutineLiveData 屬性改成擴展函數(shù)。

3.2 UI 代碼訂閱 LiveData

雖然整個機票首頁的 UI 都位于一個 Fragment 內(nèi),但業(yè)務之間不相關(guān)的 UI 我們可以分別單獨封裝成不同的 View。假如說跟城市有關(guān)的  UI,我們可能就會像下面這樣做:

class CityView : LinearLayout {          constructor(context: Context) : super(context)     constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)     constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)          private val tvCity: TextView          // ...... 省略更多的 View 聲明     init {         LayoutInflater.from(context).inflate(R.layout.flight_inquire_main_view, this).apply {             tvCIty = findViewById(R.id.tv_city)              // ...... 省略更多的 View 初始化         }     } }

如果在 Fragment 或 Activity 中,獲取 ViewModel 并訂閱 LiveData 很容易,我們只需要把它們自身使用 this  傳入即可。但是在 View 中獲取不到 Fragment 對象,所以我們不得已必須要定義一個 initObserve 函數(shù),通過將其暴露給 Fragment  調(diào)用來將 Fragment 自身的引用傳入,于是 View 的代碼就變成了如下這樣:

class CityView : LinearLayout {          constructor(context: Context) : super(context)     constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)     constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)          private val tvCity: TextView          // ...... 省略更多的 View 聲明          private lateinit var cityViewModel: CityViewModel          init {         LayoutInflater.from(context).inflate(R.layout.city_view, this).apply {             tvCIty = findViewById(R.id.tv_city)              // ...... 省略更多的 View 初始化         }         tvCity.setOnClickListener {             updateCityView()         }     }          fun <T> initObserver(owner: T) where T : ViewModelStoreOwner, T : LifecycleOwner {         cityViewModel = getViewModel(owner) {             cityLiveData.observe(owner, Observer {                 tvCity.text = it             })         }         // ...... 省略其他 LiveData 訂閱     }          private fun updateCityView() = cityVIewModel.updateCityView() }

owner 實際上就是 Fragment,不過這里為了解耦,沒有直接使用 Fragment,而是通過泛型,外加兩個上界約束來確定 owner  的職責,一旦某天這個 View 要移植到 Activity 中,Activity 也可以將自身直接通過 initObserver 函數(shù)傳入。在 Fragment  中,當我們通過 findViewById 拿到 View 對象之后就應該立即調(diào)用 initObserver 初始化訂閱,代碼就不贅述了。

我們用一張圖來總結(jié) 3.1 小節(jié)與 3.2 小節(jié):

攜程機票Android Jetpack與Kotlin Coroutines的方法教程

我們剛才編寫的示例代碼之間的關(guān)系已經(jīng)一目了然,MVVM 模式中的 V 與 VM 都已經(jīng)有了,雖然 M 在圖中沒有體現(xiàn),但獲取數(shù)據(jù)的數(shù)據(jù)源,也就是  CityViewModel.updateCityUI() 函數(shù)中調(diào)用的 fetchData() 函數(shù)就屬于  Model,它通常封裝了數(shù)據(jù)庫操作或網(wǎng)絡服務拉取。

3.3 復雜場景

在開頭的 1.2 小節(jié)中提到,我們有一些復雜的業(yè)務場景,比如多個獨立的 View 依賴同一個數(shù)據(jù)源,或者多個 View  都可能觸發(fā)同一個數(shù)據(jù)源的更新。那具體的實際情況舉例就是,比如說現(xiàn)在有兩個展示城市的 View,用戶可以在其中任意一個更改城市,兩個 View  中展示的城市信息都需要更新,這在實際情況中是非常典型的案例,將 1.2 小節(jié)中的場景 1 與場景 2 結(jié)合了起來。

基于以上的代碼示例,也就是說除了上面的 CityView 我們還需要一個與它共享同一個數(shù)據(jù)源的 View,假如說存在一個 CityView2:

class CityView2 : LinearLayout {          // ...... 省略其他代碼          private val tvCity: TextView          private lateinit var cityViewModel: CityViewModel          init {         LayoutInflater.from(context).inflate(R.layout.city_view2, this).apply {             tvCIty = findViewById(R.id.tv_city2)         }         tvCity.setOnClickListener {             updateCityView()         }     }          fun <T> initObserver(owner: T) where T : ViewModelStoreOwner, T : LifecycleOwner {         cityViewModel = getViewModel(owner) {             cityLiveData.observe(owner, Observer {                 tvCity.text = it             })         }     }          private fun updateCityView() = cityVIewModel.updateCityView() }

其他代碼大同小異,無非是初始化 View、initObserver 函數(shù)、以及更新 UI 的函數(shù)。為了確保 CityView2 與 CityView 內(nèi)的  cityViewModel 是同一個,只需確保 initObserver 函數(shù)傳進來的 owner 是同一個對象就可以了。

這里我也畫了一張圖來描述這種關(guān)系:

攜程機票Android Jetpack與Kotlin Coroutines的方法教程

四、新技術(shù)在生產(chǎn)環(huán)境遇到的挑戰(zhàn)

任何一種被業(yè)界所公認且信賴的開源技術(shù)通常都經(jīng)過了數(shù)百萬乃至數(shù)千萬級用戶量的生產(chǎn)環(huán)境的檢驗。攜程機票舊首頁的 PV 量級在千萬級別,考慮到 iOS 與  Android 雙平臺以及 AB 實驗,新的 Android 機票平臺化首頁的 PV  量級也有百萬級別。能否在百萬級別的用戶量下有優(yōu)異的穩(wěn)定性表現(xiàn),是對本文提到的這幾項技術(shù)的考驗。

Kotlin 語言及其標準庫本身已經(jīng)迭代到 1.3.x 版本(截止文章發(fā)稿前,最新版本為 1.4.10,而攜程使用的則是  1.3.71),再加上好幾年的國內(nèi)外生產(chǎn)環(huán)境的檢驗,已經(jīng)相對穩(wěn)定。而本次使用的 ViewModel、LiveData 等 Jetpack  架構(gòu)組件的版本為2.2.0,經(jīng)過線上數(shù)月的觀測也非常穩(wěn)定。但 Kotlin 協(xié)程框架 kotlinx.coroutines  最終還是出現(xiàn)了兩個頗為棘手的問題。

4.1 集成協(xié)程的 APK 在部分國產(chǎn) Android 5.x 手機上報錯:INSTALL_FAILED_DEXOPT

問題描述:Android app 工程在配置了大部分版本號為 1.3.x 的 kotlinx.coroutines 庫后,在部分國產(chǎn)的 Android  5.x 手機上安裝會報錯:INSTALL_FAILED_DEXOPT,導致無法安裝。

在攜程的編譯工具鏈條件下,只有 1.3.0 版本的 kotlinx.coroutines 庫可用,而其余 1.3.x 高版本在集成依賴后,會在 vivo  X5Pro D(Android 5.0)這款機型上穩(wěn)定復現(xiàn)這個問題。當然,能穩(wěn)定復現(xiàn)這一問題的手機品牌和型號不止這一個。

Kotlin 中文社區(qū)的論壇中也對此有所討論(參考鏈接 3)。這個帖子的博主也在 kotlinx.coroutines 庫的官方 Github 倉庫的  issues 中向官方提問,但 JetBrains 官方回復說,這是 Google 工具鏈的問題(參考鏈接 4)。之后這個問題又提交給了 Google 方面,但  Google 方面表示,已經(jīng)了解此問題,但由于涉及到的系統(tǒng)版本 Android 5.x 過于老舊,因此不予修復(參考鏈接 5)。

兩家官方的態(tài)度都已至此,我們只能抱希望由自己解決該問題。我們能嘗試的方案包括:升級 Android SDK Build-Tools 版本、升級  Gradle 版本、升級至 Kotlin 1.4,并將 kotlinx.coroutines 升級至 1.3.9、使用 JDK 8 編譯  kotlinx.coroutines 的 Jar 包(官方使用的是 JDK 6)。以上嘗試全部無效。最終的方案是,只能暫時使用 1.3.0 版本的  kotlinx.coroutines 庫,由于 1.3.1~1.3.8 版本中包含了大量對 Flow 的完善以及 Bug  修復,因此為了穩(wěn)定性考慮,業(yè)務代碼中只能暫時不使用Flow。

4.2 主線程調(diào)度器 Dispatchers.Main 獲取失敗導致 Crash

問題描述:協(xié)程主線程調(diào)度器 Dispatchers.Main 在調(diào)用時會有小概率情況發(fā)生 crash,與機型、系統(tǒng)版本無關(guān)。

這個問題經(jīng)由線上 crash 上報被我們發(fā)現(xiàn),共造成了 2000 余次的用戶 crash。

該問題是 Dispatcher.Main 的實現(xiàn)上有缺陷導致的。在 kotlinx.coroutines 的官方 Github issues  頁中已經(jīng)有人提到了這個問題(參考鏈接 6)。官方在 1.3.3 版本中使用 Class.forName 的方式替換了原先的 ServiceLoader  實現(xiàn),從而修復了該問題(參考鏈接 7),因此如果要避免該問題的出現(xiàn)最正確的解決方式是升級 kotlinx.coroutines 庫的版本。

但是狗血的問題發(fā)生了,由于 4.1 小節(jié)描述的問題,除 1.3.0 版本以外,其他版本的 kotlinx.coroutines 庫均會發(fā)生 5.x  手機無法集成的問題。這兩個問題的同時出現(xiàn)近乎導致了我們的解決方案的“死鎖”,進退兩難。

在發(fā)現(xiàn)線上問題的最初,我們自定義了主線程調(diào)度器,從而代替官方的 Dispatchers.Main,并將業(yè)務代碼中的所有 Dispatcher.Main  替換為自定義的調(diào)度器,但這并沒有完全解決問題。由于 ktx 版本的 Jetpack 架構(gòu)組件也依賴了 1.3.0 版本的 kotlinx.coroutines  庫,所以即使我們不使用 Dispatchers.Main,ViewModel 和 LiveData  的內(nèi)部也會使用。無奈之下我們只得試圖復制使用到Dispatchers.Main 的 ViewModel 與 LiveData 的代碼,并將其中的  Dispatchers.Main 替換為自定義的主線程調(diào)度器。

但以上的方案均是臨時的,在不能升級 kotlinx.coroutines 庫的情況下,最終我們決定 fork kotlinx.coroutines  的代碼。并將官方在 1.3.3 修復該問題的 commit 通過類似 cherry-pick 的方式 merge 到 1.3.0  版本的代碼上,然后更改版本號并重新編譯 Jar 包,并將其放到公司內(nèi)部源上以供使用。

從長遠來看,隨著 5.x 手機的數(shù)量越來越少,最終攜程 app 的系統(tǒng)支持最低版本會提升到 Android 6.0,只有等到那時升級  kotlinx.coroutines 版本才算最終相對完美的解決該問題。

五、結(jié)語

Kotlin 語言本身的優(yōu)勢以及所解決的問題很多都是 Java 開發(fā)者所面臨的痛點。經(jīng)過了數(shù)年的技術(shù)積累沉淀,1.3.x 版本(1.3.x  的最后一個版本是 1.3.72)的 Kotlin 已經(jīng)相對穩(wěn)定和成熟。

Kotlin 協(xié)程很強大,是一個雄心勃勃的項目,它為許多 Java 開發(fā)者帶來了新的概念以及老問題的新解決方案。雖然它已經(jīng)進入 release  階段達一年半之久,但從我們的實踐結(jié)果來看,其穩(wěn)定性仍然還有提升的空間。隨著 Kotlin 1.4 以及 kotlinx.coroutines 1.3.9  的推出,無論是 Kotlin 語言本身還是協(xié)程都已經(jīng)進入了下一個階段,相信在未來不久的時間里,它們的性能、穩(wěn)定性、以及功能都會真正再上一個臺階。

Google 官方近些年與 Android 開發(fā)社區(qū)的關(guān)系日益密切,他們采納了許多 Android 開發(fā)者提出的有效建議,并將其落地,Jetpack  就是成果之一。作為真正的官方出品,它的穩(wěn)定性從實際表現(xiàn)來看的確經(jīng)受住了考驗。

Jetpack 不僅包含架構(gòu)組件,還包含了一系列實用的庫,比如聲明式 UI 框架(Compose)、SQLite  數(shù)據(jù)庫操作框架(Room)、依賴注入(Hilt)、后臺任務管理(WorkManager)等等,在未來的開發(fā)計劃中逐漸嘗試向更多的 Jetpack  相關(guān)技術(shù)遷移也會是一個重要的 Android 端技術(shù)改進方向。

感謝各位的閱讀,以上就是“攜程機票Android Jetpack與Kotlin Coroutines的方法教程”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對攜程機票Android Jetpack與Kotlin Coroutines的方法教程這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI