精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

攜程機票Android Jetpack與Kotlin Coroutines實踐

開發 前端
Kotlin 協程很強大,是一個雄心勃勃的項目,它為許多 Java 開發者帶來了新的概念以及老問題的新解決方案。雖然它已經進入 release 階段達一年半之久,但從我們的實踐結果來看,其穩定性仍然還有提升的空間。

一、前言

1.1 技術背景與選型

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

協程的優勢主要有:

  • 更簡單的異步并發實現方式(近似于同步寫法)
  • 更便捷的任務管理
  • 更便捷的生產者-消費者模式實現
  • 更高效的 cold stream 實現(即 Flow,根據官方數據,Flow 在部分 benchmarks 場景下效率是 RxJava 的兩倍,詳見參考鏈接 1)。

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

1.2 業務背景

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

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

  • 多個不同 View,依賴同一個數據源的變化。
  • 多個不同 View,當用戶操作時,都會觸發同一數據源的變更。

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

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

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

  • 代碼冗長,沒有合理的封裝、拆分以及架構模式,單文件代碼行數高。
  • 復雜的異步操作導致回調代碼層層嵌套。
  • 不恰當的線程池配置。
  • 重復多余的 null 檢查與可能暗藏的 null 安全問題。
  • 過多的 UI 層級嵌套,代碼冗雜且性能不高。
  • 仍在使用一些 Google 官方淘汰的舊技術,沒有及時跟進新技術。

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

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

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

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

二. 熱身準備

2.1 拋磚引玉

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

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

  1. private lateinit var myViewModel: MyViewModel 
  2.  
  3.   ...... 
  4.    
  5.   myViewModel = ViewModelProvider(this)[MyViewModel::class.java] 
  6.   myViewModel.liveData1.observer(this, Observe { 
  7.     doSomething1(it) 
  8.   }) 
  9.   myViewModel.liveData2.observer(this, Observe { 
  10.     doSomething2(it) 
  11.   }) 
  12.  
  13.   ...... 

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

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

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

  1. private lateinit var myViewModel: MyViewModel 
  2.  
  3. ...... 
  4.  
  5. myViewModel = getSelfViewModel { 
  6.     liveData1.observe(this@MyFragment, Observer { 
  7.         doSomething1(it) 
  8.     }) 
  9.     liveData2.observe(this@MyFragment, Observer { 
  10.         doSomething2(it) 
  11.     }) 
  12.     ...... 

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

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

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

2.2 代碼角色劃分

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

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

三、正式實現

3.1 協程 Channel 與 LiveData 組合實現的基本模式

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

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

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

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

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

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

  1. class CityViewModel : ViewModel() { 
  2.      
  3.     private val departCityTextChannel = Channel<String>(1) 
  4.     val departCityTextLiveData = liveData { 
  5.         for (result in departCityTextChannel) 
  6.             emit(result) 
  7.     } 
  8.    
  9.     // 外部的 UI 通過調用該方法來更新數據 
  10.     fun updateCityUI() = viewModelScope.launch(Dispatchers.IO) { 
  11.         val result = fetchData() // 拉取數據 
  12.         departCityTextChannel.send(result) 
  13.     } 

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

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

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

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

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

ViewModel 內創建 departCityTextChannel 與 departCityTextLiveData 對象的代碼就變成了這樣: 

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

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

3.2 UI 代碼訂閱 LiveData

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

  1. class CityView : LinearLayout { 
  2.      
  3.     constructor(context: Context) : super(context) 
  4.     constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) 
  5.     constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) 
  6.      
  7.     private val tvCity: TextView 
  8.      
  9.     // ...... 省略更多的 View 聲明 
  10.     init { 
  11.         LayoutInflater.from(context).inflate(R.layout.flight_inquire_main_view, this).apply { 
  12.             tvCIty = findViewById(R.id.tv_city) 
  13.              // ...... 省略更多的 View 初始化 
  14.         } 
  15.     } 

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

  1. class CityView : LinearLayout { 
  2.      
  3.     constructor(context: Context) : super(context) 
  4.     constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) 
  5.     constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) 
  6.      
  7.     private val tvCity: TextView 
  8.      
  9.     // ...... 省略更多的 View 聲明 
  10.      
  11.     private lateinit var cityViewModel: CityViewModel 
  12.      
  13.     init { 
  14.         LayoutInflater.from(context).inflate(R.layout.city_view, this).apply { 
  15.             tvCIty = findViewById(R.id.tv_city) 
  16.              // ...... 省略更多的 View 初始化 
  17.         } 
  18.         tvCity.setOnClickListener { 
  19.             updateCityView() 
  20.         } 
  21.     } 
  22.      
  23.     fun <T> initObserver(owner: T) where T : ViewModelStoreOwner, T : LifecycleOwner { 
  24.         cityViewModel = getViewModel(owner) { 
  25.             cityLiveData.observe(owner, Observer { 
  26.                 tvCity.text = it 
  27.             }) 
  28.         } 
  29.         // ...... 省略其他 LiveData 訂閱 
  30.     } 
  31.      
  32.     private fun updateCityView() = cityVIewModel.updateCityView() 

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

我們用一張圖來總結 3.1 小節與 3.2 小節:

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

3.3 復雜場景

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

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

  1. class CityView2 : LinearLayout { 
  2.      
  3.     // ...... 省略其他代碼 
  4.      
  5.     private val tvCity: TextView 
  6.      
  7.     private lateinit var cityViewModel: CityViewModel 
  8.      
  9.     init { 
  10.         LayoutInflater.from(context).inflate(R.layout.city_view2, this).apply { 
  11.             tvCIty = findViewById(R.id.tv_city2) 
  12.         } 
  13.         tvCity.setOnClickListener { 
  14.             updateCityView() 
  15.         } 
  16.     } 
  17.      
  18.     fun <T> initObserver(owner: T) where T : ViewModelStoreOwner, T : LifecycleOwner { 
  19.         cityViewModel = getViewModel(owner) { 
  20.             cityLiveData.observe(owner, Observer { 
  21.                 tvCity.text = it 
  22.             }) 
  23.         } 
  24.     } 
  25.      
  26.     private fun updateCityView() = cityVIewModel.updateCityView() 

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

這里我也畫了一張圖來描述這種關系:

四、新技術在生產環境遇到的挑戰

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

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

4.1 集成協程的 APK 在部分國產 Android 5.x 手機上報錯:INSTALL_FAILED_DEXOPT

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

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

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

兩家官方的態度都已至此,我們只能抱希望由自己解決該問題。我們能嘗試的方案包括:升級 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 修復,因此為了穩定性考慮,業務代碼中只能暫時不使用Flow。

4.2 主線程調度器 Dispatchers.Main 獲取失敗導致 Crash

問題描述:協程主線程調度器 Dispatchers.Main 在調用時會有小概率情況發生 crash,與機型、系統版本無關。

這個問題經由線上 crash 上報被我們發現,共造成了 2000 余次的用戶 crash。

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

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

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

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

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

五、結語

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

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

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

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

 

責任編輯:未麗燕 來源: 知乎
相關推薦

2022-05-13 09:27:55

Widget機票業務App

2022-06-03 09:21:47

Svelte前端攜程

2023-05-12 10:14:38

APP開發

2023-01-04 12:17:07

開源攜程

2017-04-11 15:11:52

ABtestABT變量法

2022-06-17 09:42:20

開源MMKV攜程機票

2022-06-10 08:35:06

項目數據庫攜程機票

2023-11-13 11:27:58

攜程可視化

2025-06-24 09:51:47

2023-08-25 09:51:21

前端開發

2022-08-06 08:27:41

Trace系統機票前臺微服務架構

2025-06-24 09:44:41

2023-08-18 10:49:14

開發攜程

2017-04-11 15:34:41

機票前臺埋點

2024-07-05 15:05:00

2022-07-15 12:58:02

鴻蒙攜程華為

2014-12-25 17:51:07

2017-03-15 17:38:19

互聯網

2023-11-06 09:56:10

研究代碼

2022-08-12 08:34:32

攜程數據庫上云
點贊
收藏

51CTO技術棧公眾號

国产亚洲电影| sqte在线播放| 精品一区二区免费| 欧美日韩成人网| 国产精品久久久久久亚洲av| 国产粉嫩在线观看| 国产女人水真多18毛片18精品视频| 国产精品成久久久久三级| 天天做夜夜爱爱爱| 日韩欧美在线精品| 欧美精品aⅴ在线视频| 亚洲乱码日产精品bd在线观看| 日韩一区av| 国产电影精品久久禁18| 国产激情综合五月久久| 欧美人妻一区二区| 国产精品手机在线播放 | 亚洲视频中文字幕| 国内精品二区| 国产视频一二三四区| 国产精品乱看| 色综合色综合久久综合频道88| 亚洲天堂视频一区| 国产成人aa在线观看网站站| 欧美无砖砖区免费| 久久久999视频| a级在线观看| 国产精品高清亚洲| 免费在线观看91| 蜜臀av中文字幕| 激情六月婷婷综合| 国产精品久久久久久久久久ktv| 国产大片中文字幕| 欧美激情aⅴ一区二区三区| 在线看日韩欧美| 国产精品300页| 巨人精品**| 精品动漫一区二区三区在线观看| 99re精彩视频| 亚洲综合在线电影| 欧美日韩免费看| 男人天堂a在线| 182tv在线播放| 亚洲欧美电影一区二区| 亚洲精品一区二区三区蜜桃久| 青青青手机在线视频观看| 99热精品国产| 国产亚洲二区| 色网站免费观看| 成人深夜在线观看| 成人毛片网站| 亚洲av色香蕉一区二区三区| 国产在线一区二区| 91精品视频免费| 国产麻豆免费观看| 国产乱一区二区| 97在线资源站| 亚洲精品无amm毛片| 成人免费电影视频| 国产亚洲欧美一区二区| 婷婷国产在线| 91麻豆国产自产在线观看| 精选一区二区三区四区五区| 天堂在线免费av| 久久亚洲综合色一区二区三区 | 成人羞羞视频在线看网址| 亚洲国产又黄又爽女人高潮的| 国产精品手机在线观看| 欧洲亚洲成人| 一本一本久久a久久精品牛牛影视| 91l九色lporny| 欧美日一区二区| 神马久久久久久| 在线观看亚洲网站| 狠狠爱成人网| 欧美一区二区三区免费视| 91video| 轻轻草成人在线| 成人福利免费观看| 老熟妇高潮一区二区高清视频 | 亚洲女人毛茸茸高潮| 欧美大人香蕉在线| 欧美国产一区二区三区| 国产 欧美 日韩 在线| 六月婷婷一区| 成人h猎奇视频网站| 人妻精品一区一区三区蜜桃91| 久久色.com| 三年中文高清在线观看第6集| 国产后进白嫩翘臀在线观看视频| 第一福利永久视频精品| 久草福利视频在线| 亚洲精品国产九九九| 精品亚洲男同gayvideo网站| 午夜成人亚洲理伦片在线观看| 国内激情久久| 国产一区二区在线免费视频| 亚洲精品国产片| 亚洲国产成人一区二区三区| 国产一级片91| 俄罗斯av网站| 亚洲视频在线观看免费视频| 国产一区91精品张津瑜| 精品国产福利| 黄网站在线播放| 欧美丝袜一区二区| 亚洲第一成肉网| 免费看av成人| 久久久久久九九九| 亚洲视频中文字幕在线观看| 成人听书哪个软件好| 亚洲精品日韩在线观看| av福利导福航大全在线| 欧美日韩成人综合天天影院| 人妻 丝袜美腿 中文字幕| 欧美少妇xxxx| 欧美性受xxx| 国产三级小视频| 国产女主播在线一区二区| a级黄色一级片| 国产午夜亚洲精品一级在线| 伊人久久久久久久久久| 日韩黄色在线视频| 国产乱码精品一区二区三区av| 日韩电影天堂视频一区二区| 国产v日韩v欧美v| 日韩欧美中文字幕制服| 中文字幕美女视频| 日本不卡高清视频| 欧美日韩一区二区三区免费| 98色花堂精品视频在线观看| 日韩视频永久免费| 日韩av手机在线免费观看| 新狼窝色av性久久久久久| 国产高清一区视频| 青青草原国产在线| 日韩一区二区不卡| 小泽玛利亚一区二区免费| 日本欧美韩国一区三区| 日本一区二区三区视频在线播放| a日韩av网址| 亚洲裸体xxxx| av一级在线观看| 久久久久免费观看| 国产精品亚洲a| 亚洲都市激情| 国产91在线播放| 你懂的免费在线观看视频网站| 疯狂做受xxxx欧美肥白少妇| 午夜视频在线观看国产| 亚洲欧洲一级| 久久爱av电影| 国产精品高清乱码在线观看| 亚洲亚裔videos黑人hd| 中文在线观看av| 亚洲欧洲精品一区二区精品久久久| 免费av不卡在线| 女同性一区二区三区人了人一 | 一级特黄特色的免费大片视频| 国产精品美女www爽爽爽| 182午夜在线观看| 亚洲综合专区| 国产精品久久久一区二区三区| 国产精品186在线观看在线播放| 亚洲成人国产精品| 青青青国产在线| 欧美激情在线一区二区三区| 三级视频中文字幕| 91精品国产视频| 国产三区二区一区久久| 亚洲欧洲美洲av| 中文字幕日韩欧美在线视频| 国产精品人妻一区二区三区| 一区二区三区小说| 黄色性生活一级片| 久久精品国产99久久6| 日本a级片在线观看| 国产精品久久久久久久久久白浆| 欧美一级黄色网| 婷婷五月在线视频| 亚洲成人av资源网| 中文字幕理论片| 亚洲综合av网| 亚洲精品国产精品国自产网站| 久久国产人妖系列| 久久99中文字幕| 欧美综合久久| 豆国产97在线| 国产在线|日韩| 九九九久久久久久| 韩国中文免费在线视频| 欧美一区二区在线观看| 久久精品国产成人av| 1024精品合集| 丰满少妇一区二区| 日本黄色网址大全| 亚洲免费大片| 99精品视频网站| 欧美电影免费网站| 国产在线不卡精品| 深夜福利视频一区二区| 久久精品中文字幕一区| 免费在线高清av| 欧美成人vps| 一级黄色小视频| 精品欧美一区二区三区| 蜜桃av.com| 2021中文字幕一区亚洲| 伊人影院在线观看视频| 欧美bbbbb| 国产精品秘入口18禁麻豆免会员| 希岛爱理一区二区三区| 欧美一区1区三区3区公司| 视频在线观看免费影院欧美meiju| 国产mv免费观看入口亚洲| wwww在线观看免费视频| www.亚洲成人| av成人手机在线| 亚洲男人第一av网站| 丰满肉肉bbwwbbww| 91精品在线免费| 探花国产精品一区二区| 欧美日韩在线看| 日韩精品视频免费播放| 亚洲狠狠丁香婷婷综合久久久| www成人啪啪18软件| 久久亚洲一区二区三区明星换脸 | www.久久东京| 91精品视频免费看| 成人午夜毛片| 国产精品大陆在线观看| 蜜桃视频动漫在线播放| 久久久久久欧美| www欧美xxxx| 久久久最新网址| 成人三级小说| 久久久久久高潮国产精品视| a毛片在线观看| 欧美老肥婆性猛交视频| 成人在线播放免费观看| 久久激情视频免费观看| 亚洲视频tv| 中文字幕亚洲情99在线| 成人18在线| 自拍偷拍亚洲精品| 日本在线视频网| 久久天堂av综合合色| 欧美日本一道| 久久精品91久久香蕉加勒比| 日本三级在线播放完整版| 中文字幕国产亚洲| √新版天堂资源在线资源| 中文字幕在线看视频国产欧美在线看完整| 黄网在线观看| zzjj国产精品一区二区| 成年视频在线观看| 欧美黑人又粗大| av电影免费在线看| 欧美性受xxxx白人性爽| 日本免费久久| 国产欧美一区二区白浆黑人| 在线观看欧美| 国产美女99p| 在线看成人短视频| 亚洲欧洲日夜超级视频| 伊人青青综合网| 东北少妇不带套对白| 久久精品一区二区三区中文字幕| 久久婷婷国产91天堂综合精品| 久久国产精品第一页| 波多野结衣在线免费观看| 懂色av一区二区三区免费看| 久久福利小视频| 国产三级精品在线| 一起操在线播放| 午夜电影一区二区三区| 天天操天天操天天操天天| 欧美日韩美女一区二区| 亚洲高清视频在线播放| 亚洲欧美日韩综合| 超碰在线观看免费版| 91干在线观看| 欧美三级电影网址| 国产亚洲欧美另类一区二区三区| 国产精品探花在线观看| 欧美日韩午夜爽爽| 久久精品亚洲一区二区| 日本高清免费观看| 91免费观看在线| 777777国产7777777| 欧美丝袜美女中出在线| 国产精品视频无码| 日韩成人中文电影| 国产在线一区二区视频| 欧亚精品中文字幕| 精品麻豆剧传媒av国产九九九| 久久精品日产第一区二区三区乱码 | 免费看的黄色录像| 亚洲福利视频三区| 一区二区国产欧美| 日韩成人在线观看| av片在线观看| 国产精品久久久久久久久久久久 | 国产清纯白嫩初高生在线观看91| 日本少妇高清视频| 在线免费亚洲电影| 日韩在线视频免费| 欧美成人在线免费视频| www成人在线视频| 精品国产乱码久久久久久88av | 欧美久久精品一级黑人c片| 欧美大片免费高清观看| 国产 高清 精品 在线 a| 日本不卡二三区| www.亚洲天堂网| 9久草视频在线视频精品| 天天综合天天做| 欧美日韩精品一区二区在线播放| 五月天婷婷视频| 欧美激情国产日韩精品一区18| 欧美电影在线观看网站| 欧美污视频久久久| 国产欧美日韩一级| 成人做爰www看视频软件| 亚洲欧美韩国综合色| 一级特黄色大片| 综合激情国产一区| 日本另类视频| 日本一区二区三区视频在线观看 | 国产成人97精品免费看片| 国产精品网站在线看| 国产精品av免费观看| 韩国成人在线视频| 羞羞在线观看视频| 欧美无乱码久久久免费午夜一区 | 97婷婷大伊香蕉精品视频| 日韩一二三区在线观看| 国产一级片91| 成人性视频免费网站| 国产无码精品在线播放| 精品噜噜噜噜久久久久久久久试看 | 国产裸体无遮挡| 久久天天躁狠狠躁老女人| av成人在线网站| 一本—道久久a久久精品蜜桃| 久久国产精品99精品国产| 99自拍偷拍视频| 欧美日韩美少妇| av毛片在线| 波多野结衣精品久久| 在线观看一区视频| 国产精品第七页| 色8久久人人97超碰香蕉987| 免费播放片a高清在线观看| 日韩av色在线| 日韩三级在线| 深爱五月综合网| 性久久久久久久久| 你懂的在线看| 国产一区视频在线播放| 91精品天堂福利在线观看| 台湾佬美性中文| 婷婷成人综合网| 国产无套粉嫩白浆在线2022年 | 中文字幕视频免费观看| 日韩小视频在线| 一区二区三区国产好| 日韩av高清在线看片| 久久久影院官网| 国产日韩欧美91| 国产九一精品| 天天操狠狠操夜夜操| 一区二区三区四区在线| 午夜小视频在线播放| 国产精品久久久久久久天堂 | 91视频免费观看| 中国一级特黄视频| 欧美日韩爱爱视频| 美女毛片一区二区三区四区| 亚洲一级片网站| 亚洲国产一区二区在线播放| 日本视频在线观看一区二区三区| 国产精品久久久久久婷婷天堂| 中文字幕乱码亚洲无线精品一区 | 日本一区高清| 成人性生交大片免费看视频直播| 亚洲福利电影| 狂野欧美性猛交| 亚洲电影天堂av| 欧美黄色成人| 久草热视频在线观看| 国产精品国产三级国产aⅴ入口| 黄色片一区二区三区| 日韩免费不卡av| 欧美91大片| 亚洲女优在线观看| 精品国产自在久精品国产| 成人开心激情| 欧美日韩福利在线| 国产精品网站一区|