Kotlin Coroutines在Android中的實踐

来源:https://www.cnblogs.com/mengdd/archive/2019/12/05/kotlin-coroutines-in-Android.html
-Advertisement-
Play Games

Kotlin coroutines在Android中的應用. 協程在Android中主要用來解決什麼問題; 和Architecture Components, MVVM構架如何完美結合. ...


Coroutines在Android中的實踐

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

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

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 = 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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ElasticSearch 用Scroll(對應資料庫的游標) 一次查出全部數據 ...
  • SQLite 的 DELETE 語句用於刪除表中已有的記錄。可以使用帶有 WHERE 子句的 DELETE 查詢來刪除選定行,否則所有的記錄都會被刪除。 SQLite 要清空表記錄,只能使用Delete來刪除全部表數據。但是與別的資料庫不同,SQlite在為表創建自增列後,會將表自增列的當前序號保存 ...
  • 參考51CTO博客 問題描述:使用scn號恢復誤刪數據 1.查詢系統閃回的scn值以及當前日誌的scn值,因為我這個是測試,創建的表是在在後邊,所以scn值要大於下邊這兩個scn值,所以對我恢複數據沒有用,如果我創建的數據是在下邊這兩個SCN值之前,也就是比這兩個時間點SCN值小,就可以用這兩個sc ...
  • #!/bin/bash env echo "Download msyql5.7 rpm..." sudo yum install wget wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm ... ...
  • 使用CameraLibrary項目,在部分手機或平板上不能正常使用,要報“打開相機失敗”查看debug日誌顯示“setParameters failed”。 找到CameraView.java中的setCameraParameters方法,註釋掉 //自動聚焦模式 //parameters.setF ...
  • 在安卓開發當中,一個十分重要的佈局則是底部標題欄了,擁有了底部標題欄,我們就擁有了整個軟體UI開發的框架,一般而言,整個軟體的佈局首先就是從底部標題欄開始構建,然後再開始其他模塊的編寫,組成一個完善的軟體,那麼如何才能夠編寫一個底部標題欄呢,我這裡使用了碎片來實現,當然是碎片的動態載入的方式,靜態加 ...
  • 問題 Android 設置頁面的啟動模式為 singletask 之後,當按Home 退出時,再重新打開應用,還會進入首啟動頁。就會造成一些應用需要重新登錄,當前頁數據丟失等問題 解決 去除啟動頁的 singletask 的啟動模式(AndroidManifest.xml) 在啟動頁activity ...
  • 11. 基於定時器的動畫 基於定時器的動畫 我可以指導你,但是你必須按照我說的做。 -- 駭客帝國 在第10章“緩衝”中,我們研究了CAMediaTimingFunction,它是一個通過控制動畫緩衝來模擬物理效果例如加速或者減速來增強現實感的東西,那麼如果想更加真實地模擬物理交互或者實時根據用戶輸 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...