Kotlin Coroutines在Android中的實踐
Coroutines在Android中的實踐
前面兩篇文章講了協程的基礎知識和協程的通訊.
見:
- Kotlin Coroutines不復雜, 我來幫你理一理
- Kotlin協程通訊機制: Channel
舉的例子可能離實際的應用程式碼比較遙遠.
這篇我們就從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
coroutineScope
和supervisorScope
可以用來在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層重新註冊獲取結果. viewModelScope
和lifecycleScope
可以結合起來使用.
舉例: 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