精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

天堂99x99es久久精品免费| 成人三级小说| 韩国v欧美v亚洲v日本v| 欧美精品在线视频观看| 扒开伸进免费视频| 日本免费久久| 亚洲天堂2014| 久久久影院一区二区三区| 国产乡下妇女三片| 狠狠入ady亚洲精品| 亚洲欧洲高清在线| 欧美人与性动交α欧美精品| 日韩伦理福利| 亚洲免费在线视频一区 二区| 久久99国产精品| 国产乱人乱偷精品视频a人人澡| 亚洲久久一区| 久久天天躁狠狠躁夜夜躁 | 国产一区二区三区视频在线| 欧美日韩在线第一页| 香蕉视频免费版| 国内精品一区视频| av网站免费线看精品| 亚洲aaaaaa| 国产成人精品一区二区色戒| 亚洲清纯自拍| 欧美激情第1页| 精品无码一区二区三区蜜臀| 精品一区欧美| 欧美精品一区二区三区蜜臀 | 东方aⅴ免费观看久久av| 国产精品久久久久av免费| 91在线看视频| 精品成人在线| 欧美激情国内偷拍| 黄色录像免费观看| 久久国产成人精品| 亚洲视频在线免费看| 中文字幕乱码在线| 豆花视频一区二区| 日韩欧美国产三级| 日韩不卡的av| 国产精品一站二站| 7777女厕盗摄久久久| 乌克兰美女av| 日韩一区二区三区免费| 一本到不卡免费一区二区| 欧美男女爱爱视频| av有码在线观看| 亚洲国产综合91精品麻豆| 大片在线观看网站免费收看| 成人在线视频亚洲| 亚洲日本在线天堂| av磁力番号网| a视频在线观看免费| 亚洲人一二三区| 精品国产三级a∨在线| 免费日本一区二区三区视频| 国产精品麻豆久久久| 亚洲欧美国产不卡| 黄色精品免费看| 中文成人综合网| 亚洲制服欧美久久| 看女生喷水的网站在线观看| 亚洲美女区一区| 红桃一区二区三区| 91九色在线看| 色哟哟一区二区三区| 免费裸体美女网站| 成人在线不卡| 欧美一区三区二区| 久久久久久婷婷| 日韩成人一级| 中文字幕av一区| 日韩视频中文字幕在线观看| 精品成人免费| 日韩美女在线看| 亚洲一区二区三区网站| 国产美女在线观看一区| 99精品99久久久久久宅男| 免费国产黄色片| 久久久久久夜精品精品免费| 亚洲巨乳在线观看| 午夜av在线免费观看| 精品国产成人在线| 欧美三级午夜理伦三级富婆| 精品久久免费| 亚洲美女精品成人在线视频| 99精品全国免费观看| 91精品国产成人观看| 97婷婷大伊香蕉精品视频| 久久久999久久久| 国产一区二区三区免费看 | 国产精品免费网站| 99精品在线看| 久久久精品国产免大香伊| 免费成人深夜夜行网站视频| 英国三级经典在线观看| 69av一区二区三区| 精品中文字幕在线播放| 色男人天堂综合再现| 久久久久国产视频| 一区二区自拍偷拍| 99精品视频一区| 懂色av粉嫩av蜜臀av| 亚洲va中文在线播放免费| 91精品欧美福利在线观看| 亚洲午夜久久久久久久久红桃 | 久久精品.com| 麻豆国产一区二区三区四区| 国产午夜精品全部视频播放 | 69p69国产精品| wwwwww日本| 亚洲高清毛片| 91免费视频国产| 国产系列电影在线播放网址| 亚洲线精品一区二区三区| 午夜久久久精品| 秋霞影视一区二区三区| 九色精品美女在线| 精品国产www| 久久久.com| aa在线免费观看| 给我免费播放日韩视频| 久久在线免费观看视频| 瑟瑟视频在线免费观看| 91小视频免费观看| 91.com在线| 蜜桃精品视频| zzjj国产精品一区二区| 男人天堂视频网| 26uuu亚洲| 亚洲 欧美 日韩 国产综合 在线| 国产麻豆精品| 另类色图亚洲色图| 91久久久久国产一区二区| 国产亚洲成av人在线观看导航| 激情五月宗合网| 精品人人人人| 久久久久久久网站| 高h放荡受浪受bl| 亚洲午夜av在线| 在线精品视频播放| 亚洲一本视频| 国产青春久久久国产毛片| 国产探花在线观看| 日韩一本二本av| 欧美卡一卡二卡三| 国产精品99久久久久| 日本高清xxxx| 高清日韩中文字幕| 97不卡在线视频| 青青操视频在线| 91福利在线免费观看| 亚洲一区 欧美| 久久国产成人午夜av影院| 亚洲欧美日韩国产yyy | 在线播放欧美女士性生活| 91视频最新网址| 久久精品国产网站| 免费观看中文字幕| 中文在线综合| 欧美最猛性xxxxx(亚洲精品)| 免费在线黄色网址| 欧美日韩免费高清一区色橹橹| 国产无遮挡在线观看| 精品中文字幕一区二区小辣椒| 7777在线视频| 国产精品久久久久av蜜臀| 97在线视频观看| 国产在线一在线二| 欧美日韩一区二区欧美激情| 久久国产美女视频| www.欧美色图| 在线免费观看视频黄| 亚洲精品91| 久久草.com| 狠狠久久伊人中文字幕| 欧美国产激情18| 三级视频网站在线| 欧美日韩一区成人| 久久久久久久久久久久久久免费看| 成人美女在线观看| 国模杨依粉嫩蝴蝶150p| 久久久久久免费视频| 成人精品一二区| 国产精欧美一区二区三区蓝颜男同| 色婷婷综合成人| 日本精品久久久久久| 欧美吻胸吃奶大尺度电影| 亚洲色婷婷一区二区三区| 2020国产成人综合网| 污污网站免费观看| 亚洲三级影院| 99精品视频网站| 先锋影音国产精品| 亚洲精品日韩av| 成人影院网站| 欧美日韩国产第一页| 成人激情电影在线看| 欧美tk—视频vk| 中文字幕日韩经典| 亚洲国产成人porn| 四虎永久免费地址| 久久网这里都是精品| 亚洲av毛片在线观看| 日日夜夜免费精品| 国产精品成人久久电影| 99精品美女| 欧美高清性xxxxhdvideosex| youjizz亚洲| 96精品久久久久中文字幕| 吞精囗交69激情欧美| 国外成人免费在线播放| 搞黄网站在线观看| 一区二区成人av| 青青草在线视频免费观看| 日韩精品一区二区三区视频| 中文字幕av片| 色av成人天堂桃色av| 97人人澡人人爽人人模亚洲| 亚洲精品欧美激情| 久久av红桃一区二区禁漫| 久久久久九九视频| 亚洲欧美在线不卡| 国产aⅴ综合色| 无人码人妻一区二区三区免费| 蜜臀国产一区二区三区在线播放| 日韩avxxx| 亚洲美洲欧洲综合国产一区| 国产欧美123| 中文字幕一区二区三区在线视频| 亚洲成人精品电影在线观看| 亚洲资源网站| 麻豆久久久9性大片| 精品淫伦v久久水蜜桃| 成人av男人的天堂| 欧美国产亚洲精品| 亚洲一区二区中文| 999色成人| 91久久久国产精品| 91精品亚洲一区在线观看| 国产日韩欧美在线| 日日狠狠久久| 成人信息集中地欧美| 国产精品国产三级在线观看| 亚洲一区二区三区四区在线播放| 免费一级欧美在线大片| 51国产成人精品午夜福中文下载| 日韩亚洲精品在线观看| 91在线免费看片| 大香伊人久久精品一区二区| 不卡视频一区二区三区| 国产成人高清精品免费5388| 国产在线播放一区二区| 午夜a一级毛片亚洲欧洲| 精品毛片久久久久久| 亚洲人成亚洲精品| 日韩不卡av| 日韩在线欧美| 国产精品久久成人免费观看| 欧美精品观看| 91九色丨porny丨国产jk| 性久久久久久| 97公开免费视频| 久草热8精品视频在线观看| www.日本久久| 成人sese在线| 国产美女免费无遮挡| 国产精品视频一区二区三区不卡| 日本激情视频一区二区三区| 一区二区三区国产精品| 在线观看 中文字幕| 在线免费不卡电影| 国产精品久久久久久免费免熟| 欧美大片一区二区| 黄色影院在线播放| 美女精品视频一区| 男人的天堂免费在线视频| 国产精品你懂得| 日韩不卡在线视频| 久久精品ww人人做人人爽| 成人在线视频免费观看| 黄色网在线视频| 久久国产一二区| 99精品视频国产| 99re热视频精品| 天天操夜夜操av| 精品露脸国产偷人在视频| 中文字幕在线2018| 亚洲第一视频网站| av在线日韩国产精品| 久久久久久久成人| 成人黄色在线| 精品日本一区二区| 99久久亚洲精品| 国产日产欧美视频| 国产精品一区二区视频| a级片在线观看| 亚洲午夜精品网| 91tv国产成人福利| 日韩精品在线免费观看视频| 18+激情视频在线| 国产精品流白浆视频| 九九热播视频在线精品6| 伊人av成人| 玖玖在线精品| 亚洲精品乱码久久久久久蜜桃欧美| 亚洲国产精品v| 日韩成人av毛片| 日韩一区二区高清| 在线看av的网址| 欧美在线观看日本一区| 爱爱精品视频| 777久久精品一区二区三区无码| 美国三级日本三级久久99| 亚洲国产果冻传媒av在线观看| 亚洲欧美二区三区| 中文字幕av影视| 亚洲人成电影网站| 毛片电影在线| 国产精品国产一区二区| 欧美一区二区| 色呦色呦色精品| 国产精品色呦呦| 久久影视中文字幕| 精品视频偷偷看在线观看| 超碰中文在线| 国产91社区| 欧美大片专区| 天天综合成人网| 中文字幕一区二区三区四区不卡| 欧美一区二区三区久久久| 日韩经典一区二区三区| 国产白浆在线免费观看| 国产欧美日本在线| 国内精品美女在线观看| 国产又粗又猛又爽又黄| 亚洲黄色免费网站| jlzzjlzzjlzz亚洲人| 操人视频在线观看欧美| 精品一区二区三区中文字幕| 中文字幕一区二区三区有限公司| 免费观看在线色综合| 综合 欧美 亚洲日本| 欧美日韩中字一区| 欧美成人三区| 91亚洲精品在线观看| 永久91嫩草亚洲精品人人| 成人三级做爰av| 夜夜揉揉日日人人青青一国产精品| 国产成人麻豆精品午夜在线| 久久亚洲国产精品成人av秋霞| 国产日韩在线观看视频| 欧美一级爱爱视频| 成人手机电影网| 99久热在线精品996热是什么| 精品亚洲一区二区三区在线观看| 亚洲伊人av| 亚洲人成网站在线播放2019| 狠狠v欧美v日韩v亚洲ⅴ| 日韩黄色免费观看| 亚洲精品www| 日本在线中文字幕一区二区三区| 亚洲高清在线播放| 国产在线精品一区在线观看麻豆| 麻豆亚洲av熟女国产一区二| 亚洲精品国精品久久99热| 桃花岛成人影院| 可以在线看黄的网站| 不卡视频免费播放| 国产精品午夜一区二区| 久久久999成人| 久久久久97| 色综合天天色综合| 亚洲乱码日产精品bd| 亚洲日本国产精品| 国产精品欧美在线| 国产精品啊啊啊| 久久久久亚洲av无码专区桃色| 欧美日韩免费高清一区色橹橹| 久久99亚洲网美利坚合众国| 久热国产精品视频一区二区三区| 久久精品噜噜噜成人av农村| 国产在线综合网| 一区二区三区动漫| 亚洲高清999| 美女黄色片视频| 亚洲一区二区视频在线观看| 黄色网址在线播放| 超碰国产精品久久国产精品99| 日韩精品国产精品| 免费人成视频在线| 成人黄色777网| 麻豆av一区| 亚洲一区一卡| 一级黄色片日本| 亚洲欧美国产精品久久久久久久| 只有精品亚洲| 亚洲国产精品毛片av不卡在线|