主頁 > 移動端開發 > Kotlin Coroutines在Android中的實踐

Kotlin Coroutines在Android中的實踐

2020-09-15 15:36:37 移動端開發

Coroutines在Android中的實踐

前面兩篇文章講了協程的基礎知識和協程的通信.
見:

  • Kotlin Coroutines不復雜, 我來幫你理一理
  • Kotlin協程通信機制: Channel
    舉的例子可能離實際的應用代碼比較遙遠.

這篇我們就從Android應用的角度, 看看實踐中都有哪些地方可以用到協程.

本文被收錄在: https://github.com/mengdd/KotlinTutorials

Coroutines的用途

Coroutines在Android中可以幫我們做什么:

  • 取代callbacks, 簡化代碼, 改善可讀性.
  • 保證Main safety.
  • 結構化管理和取消任務, 避免泄漏.

這有一個例子:

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

這里get是一個suspend方法, 只能在另一個suspend方法或者在一個協程中呼叫.

get方法在主執行緒被呼叫, 它在開始請求之前suspend了協程, 當請求回傳, 這個方法會resume協程, 回到主執行緒. 網路請求不會block主執行緒.

main-safety是如何保證的呢?

dispatcher決定了協程在什么執行緒上執行. 每個協程都有dispatcher. 協程suspend自己, dispatcher負責resume它們.

  • Dispatchers.Main: 主執行緒: UI互動, 更新LiveData, 呼叫suspend方法等.
  • Dispatchers.IO: IO操作, 資料庫操作, 讀寫檔案, 網路請求.
  • Dispatchers.Default: 主執行緒之外的計算任務(CPU-intensive work), 排序, 決議JSON等.

一個好的實踐是使用withContext()來確保每個方法都是main-safe的, 呼叫者可以在主執行緒隨意呼叫, 不用關心里面的代碼到底是哪個執行緒的.

管理協程

之前講Scope和Structured Concurrency的時候提過, scope最典型的應用就是按照物件的生命周期, 自動管理其中的協程, 及時取消, 避免泄漏和冗余操作.

在協程之中再啟動新的協程, 父子協程是共享scope的, 也即scope會track其中所有的協程.

協程被取消會拋出CancellationException.

coroutineScopesupervisorScope可以用來在suspend方法中啟動協程. Structured concurrency保證: 當一個suspend函式回傳時, 它的所有作業都執行完畢.

它們兩者的區別是: 當子協程發生錯誤的時候, coroutineScope會取消scope中的所有的子協程, 而supervisorScope不會取消沒有發生錯誤的其他子協程.

Activity/Fragment & Coroutines

在Android中, 可以把一個螢屏(Activity/Fragment)和一個CoroutineScope關聯, 這樣在Activity或Fragment生命周期結束的時候, 可以取消這個scope下的所有協程, 好避免協程泄漏.

利用CoroutineScope來做這件事有兩種方法: 創建一個CoroutineScope物件和activity的生命周期系結, 或者讓activity實作CoroutineScope介面.

方法1: 持有scope參考:

class Activity {
    private val mainScope = MainScope()
    
    fun destroy() {
        mainScope.cancel()
    }
}    

方法2: 實作介面:

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
    fun destroy() {
        cancel() // Extension on CoroutineScope
    }
}

默認執行緒可以根據實際的需要指定.
Fragment的實作類似, 這里不再舉例.

ViewModel & Coroutines

Google目前推廣的MVVM模式, 由ViewModel來處理邏輯, 在ViewModel中使用協程, 同樣也是利用scope來做管理.

ViewModel在螢屏旋轉的時候并不會重建, 所以不用擔心協程在這個程序中被取消和重新開始.

方法1: 自己創建scope

private val viewModelJob = Job()

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

默認是在UI執行緒.
CoroutineScope的引數是CoroutineContext, 是一個配置屬性的集合. 這里指定了dispatcher和job.

在ViewModel被銷毀的時候:

override fun onCleared() {
    super.onCleared()
    viewModelJob.cancel()
}

這里viewModelJob是uiScope的job, 取消了viewModelJob, 所有這個scope下的協程都會被取消.

一般CoroutineScope創建的時候會有一個默認的job, 可以這樣取消:

uiScope.coroutineContext.cancel()

方法2: 利用viewModelScope

如果我們用上面的方法, 我們需要給每個ViewModel都這樣寫. 為了避免這些boilerplate code, 我們可以用viewModelScope.

注: 要使用viewModelScope需要添加相應的KTX依賴.

  • For ViewModelScope, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 or higher.

viewModelScope系結的是Dispatchers.Main, 會自動在ViewModel clear的時候自動取消.

用的時候直接用就可以了:

class MainViewModel : ViewModel() {
    // Make a network request without blocking the UI thread
    private fun makeNetworkRequest() {
       // launch a coroutine in viewModelScope 
        viewModelScope.launch(Dispatchers.IO) {
            // slowFetch()
        }
    }

    // No need to override onCleared()
}

所有的setting up和clearing作業都是庫完成的.

LifecycleScope & Coroutines

每一個Lifecycle物件都有一個LifecycleScope.

同樣也需要添加依賴:

  • For LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 or higher.

要訪問CoroutineScope可以用lifecycle.coroutineScope或者lifecycleOwner.lifecycleScope屬性.

比如:

activity.lifecycleScope.launch {}
fragment.lifecycleScope.launch {}
fragment.viewLifecycleOwner.launch {}

lifecycleScope可以啟動協程, 當Lifecycle結束的時候, 任何這個scope中啟動的協程都會被取消.

這比較適合于處理一些帶delay的UI操作, 比如需要用handler.postDelayed的更新UI的操作, 有多個操作的時候嵌套難看, 還容易有泄漏問題.

用了lifecycleScope之后, 既避免了嵌套代碼, 又自動處理了取消.

lifecycleScope.launch {
    delay(DELAY)
    showFullHint()
    delay(DELAY)
    showSmallHint()
}

LifecycleScope和ViewModelScope

但是LifecycleScope啟動的協程卻不適合呼叫repository的方法. 因為它的生命周期和Activity/Fragment是一致的, 太碎片化了, 容易被取消, 造成浪費.

設備旋轉時, Activity會被重建, 如果取消請求再重新開始, 會造成一種浪費.

可以把請求放在ViewModel中, UI層重新注冊獲取結果. viewModelScopelifecycleScope可以結合起來使用.

舉例: ViewModel這樣寫:

class NoteViewModel: ViewModel {
    val noteDeferred = CompletableDeferred<Note>()
    
    viewModelScope.launch {
        val note = repository.loadNote()
        noteDeferred.complete(note)
    }
    
    suspend fun loadNote(): Note = noteDeferred.await()
}

而我們的UI中:

fun onCreate() {
    lifecycleScope.launch {
        val note = userViewModel.loadNote()
        updateUI(note)
    }
}

這樣做之后的好處:

  • ViewModel保證了資料請求沒有浪費, 螢屏旋轉不會重新發起請求.
  • lifecycleScope保證了view沒有leak.

特定生命周期階段

盡管scope提供了自動取消的方式, 你可能還有一些需求需要限制在更加具體的生命周期內.

比如, 為了做FragmentTransaction, 你必須等到Lifecycle至少是STARTED.

上面的例子中, 如果需要打開一個新的fragment:

fun onCreate() {
    lifecycleScope.launch {
        val note = userViewModel.loadNote()
        fragmentManager.beginTransaction()....commit() //IllegalStateException
    }
}

很容易發生IllegalStateException.

Lifecycle提供了:
lifecycle.whenCreated, lifecycle.whenStarted, lifecycle.whenResumed.

如果沒有至少達到所要求的最小生命周期, 在這些塊中啟動的協程任務, 將會suspend.

所以上面的例子改成這樣:

fun onCreate() {
    lifecycleScope.launchWhenStarted {
        val note = userViewModel.loadNote()
        fragmentManager.beginTransaction()....commit()
    }
}

如果Lifecycle物件被銷毀(state==DESTROYED), 這些when方法中的協程也會被自動取消.

LiveData & Coroutines

LiveData是一個供UI觀察的value holder.

LiveData的資料可能是異步獲得的, 和協程結合:

val user: LiveData<User> = liveData {
    val data = https://www.cnblogs.com/mengdd/p/database.loadUser() // loadUser is a suspend function.
    emit(data)
}

這個例子中的liveData是一個builder function, 它呼叫了讀取資料的方法(一個suspend方法), 然后用emit()來發射結果.

同樣也是需要添加依賴的:

  • For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 or higher.

實際上使用時, 可以emit()多次:

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

每次emit()呼叫都會suspend這個塊, 直到LiveData的值在主執行緒被設定.

LiveData還可以做變換:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

如果資料庫的方法回傳的型別是LiveData型別, emit()方法可以改成emitSource(). 例子見: Use coroutines with LiveData.

網路/資料庫 & Coroutines

根據Architecture Components的構建模式:

  • ViewModel負責在主執行緒啟動協程, 清理時取消協程, 收到資料時用LiveData傳給UI.
  • Repository暴露suspend方法, 確保方法main-safe.
  • 資料庫和網路暴露suspend方法, 確保方法main-safe. Room和Retrofit都是符合這個pattern的.

Repository暴露suspend方法, 是主執行緒safe的, 如果要對結果做一些heavy的處理, 比如轉換計算, 需要用withContext自行確定主執行緒不被阻塞.

Retrofit & Coroutines

Retrofit從2.6.0開始提供了對協程的支持.

定義方法的時候加上suspend關鍵字:

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(
        @Path("org") org: String
    ): List<Repo>
}

suspend方法進行請求的時候, 不會阻塞執行緒.
回傳值可以直接是結果型別, 或者包一層Response:

@GET("orgs/{org}/repos?per_page=100")
suspend fun getOrgRepos(
    @Path("org") org: String
): Response<List<Repo>>

Room & Coroutines

Room從2.1.0版本開始提供對協程的支持. 具體就是DAO方法可以是suspend的.

@Dao
interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsers(): List<User>

    @Insert
    suspend fun insertUser(user: User)

    @Update
    suspend fun updateUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)
}

Room使用自己的dispatcher來確定查詢運行在后臺執行緒.
所以你的代碼不應該使用withContext(Dispatchers.IO), 會讓代碼變得復雜并且查詢變慢.

更多內容可見: Room ?? Coroutines.

WorkManager & Coroutines

WorkManager也有協程版本, 添加work-runtime-ktx依賴, 然后改變基類, 以前繼承Worker, 現在繼承CoroutineWorker.
比如:

class UploadNotesWorker(...) : CoroutineWorker(...) {
    suspend fun doWork(): Result {
        val newNotes = db.queryNewNotes()
        noteService.uploadNotes(newNotes)
        db.markAsSynced(newNotes)
        return Result.success()
    }
}

這段代碼其中資料庫用Room, 網路用Retrofit, 這樣3個方法都是suspend的.

用了協程的版本之后, 取消操作更容易.

更詳細的請看: Threading in CoroutineWorker

例外處理

suspend方法中的例外將會resume到呼叫者.
更一般的, 協程中的錯誤會通知到它的呼叫者或者scope.

launchasync的例外處理不同.
這是因為async回傳值, 是期待await呼叫的, 所以會持有例外, 在呼叫await()的時候才回傳(結果或例外).
所以如果await()沒有被呼叫的話, 例外就會被吃了.

測驗

推薦使用runBlockingTest來替換runBlocking, 將會利用virtual time, 節省測驗時間.

更多關于測驗的詳細內容見: kotlinx-coroutines-test

參考

  • Codelab: Using Kotlin Coroutines in your Android App
  • Improve app performance with Kotlin coroutines
  • Use Kotlin coroutines with Architecture components
  • Coroutine Context and Dispatchers
  • Threading in CoroutineWorker

博客:

  • Kotlin Coroutines patterns & anti-patterns
  • Coroutines on Android (part II): Getting started
  • Coroutines On Android (part III): Real work
  • Part 2 — Coroutine Cancellation and Structured Concurrency
  • Room ?? Coroutines

Google的視頻:

  • LiveData with Coroutines and Flow (Android Dev Summit '19)
  • Understand Kotlin Coroutines on Android (Google I/O'19)

歡迎關注微信公眾號: 圣騎士Wind
微信公眾號

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/47857.html

標籤:Android

上一篇:去除TextView設定lineSpacingExtra后,最后一行多出的空白

下一篇:Android 再次打開APP進入按Home鍵退出時的界面(thisTaskRoot)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more