suspend fun,而不是在內部 launch 前言
--
寫這篇文章,其實是因為最近看到有些公眾號在介紹 Kotlin 協程時,都推薦在 Repository 中這樣寫:
kotlin 體驗AI代碼助手 代碼解讀複製代碼fun load() {
repositoryScope.launch {
// do something
}
}
甚至把這種寫法包裝成一種「最佳實踐」,理由通常是:
launch,呼叫更簡單;看到這樣的文章越來越多,我實在忍不住了。
因為它真正改變的並不是「是否非同步」,而是任務的生命週期歸屬。
很多初學者會誤以為:
體驗AI代碼助手 代碼解讀複製代碼耗時操作 = launch
於是只要存取資料庫、發起網路請求,就習慣性在 Repository 裡偷偷 launch 一個新的協程。
但現代 Android 官方推薦的設計思想並不是這樣。
對於絕大多數一次性資料操作,Repository 更應該暴露 suspend fun,由呼叫方決定在哪個 CoroutineScope 中啟動協程。
因為:
suspend表達的是能力,而launch表達的是任務歸屬。
真正應該思考的問題從來不是「哪裡寫 launch 更方便」,而是:
誰應該擁有這次任務的生命週期、取消權、完成時機以及例外邊界?
這也是我寫這篇文章最想討論的問題。
如果把 Android 官方近幾年的協程建議壓縮成一句話,可以概括為:
Data / Domain 層對外暴露「掛起函式和 Flow」,由呼叫方控制協程的建立、取消和生命週期。
也就是:
suspend funFlowViewModel、UseCase 入口、WorkManager、應用程式級協調者所以從職責上看:
這就是為什麼更推薦:
kotlin 體驗AI代碼助手 代碼解讀複製代碼suspend fun load()
而不是:
kotlin 體驗AI代碼助手 代碼解讀複製代碼fun load() {
repositoryScope.launch { }
}
看起來只是少寫一個 launch,但它們表達的是兩種完全不同的架構語義。
suspend fun load()
呼叫方:

這個設計表示:
viewModelScope 管這是結構化並發的典型寫法。
launch
呼叫方:

這個設計實際上表示:
所以,差別不是「誰寫 launch 更方便」,而是:
誰擁有這次非同步任務的控制權。
suspend fun 更符合現代 Android 架構?在 Android 裡,不同層的生命週期完全不同:
Fragment / Activity 生命週期短ViewModel 生命週期通常跟頁面綁定WorkManager 甚至希望任務在程序重啟後繼續同一個 Repository,可能被很多不同呼叫方使用。
如果 Repository 自己偷偷 launch,它就等於假設:
「這份任務的生命週期由我來定。」
但這就是問題所在。
更合理的方式是讓呼叫方決定:

這樣:
這正是結構化並發要解決的問題。
suspend 天然支援結構化並發suspend fun 最大的價值不是「非同步」,而是:
它把非同步操作保留在目前呼叫鏈裡。
例如:

這裡 repository.loadUser() 和 repository.loadNotice() 都是可組合的。
因為它們是 suspend fun,所以我們可以:
coroutineScope 做並發組合supervisorScope 控制失敗隔離withTimeout 控制逾時retry、runCatching、Result 包裝錯誤策略但如果 Repository 內部直接 launch,那這些組合能力會明顯變差。因為你已經拿不到這個任務的完成時機了。
launch 會讓完成時機變得模糊suspend 版本:

呼叫方很清楚:

內部 launch 版本:

呼叫方:

於是你就會開始補各種東西:回呼、事件通知、額外狀態流、手寫完成監聽……
而這些問題,本來 suspend fun 天然就已經解決了。
Repository 內部
launch並沒有減少複雜度,只是把時序複雜度藏起來了。
suspend fun 的一個重要優點是:
例外可以沿呼叫鏈自然向上傳遞。
例如:
這條鏈路是很自然的:Repository 丟錯 → ViewModel 捕獲 → UI 決定如何顯示。
但如果 Repository 自己 launch,例外處理就變成一個問題:是 Repository 自己吃掉?還是印日志?還是發事件通知?還是依賴 CoroutineExceptionHandler?更致命的是如果上層 try catch 其實是包不住的。
錯誤邊界開始變得不透明,而不透明往往就是可維護性開始下滑的地方。
如果呼叫鏈保持結構化:

那當 viewModelScope 被取消時,repository.load() 也會跟著取消。
但如果 Repository 使用自己的 repositoryScope,那呼叫方取消了自己,也不一定能取消 Repository 內部那個任務。
這會帶來幾個常見問題:
withContext 和 launch 搞混了這是整篇文章裡一個非常關鍵的點,值得單獨拿出來說。
很多開發者把:

和:

認為是差不多的東西——「反正都是把耗時操作放到背景跑」。
實際上,它們解決的是完全不同的問題。
很多開發者真正想表達的是:

結果卻寫成了:

withContext 只是切換執行上下文
這裡雖然執行緒可能從 Main 切到了 IO,但整個任務仍然屬於同一個協程。也就是說:
withContext 只是暫時切換了執行環境,並沒有建立新的任務。整個呼叫鏈仍然保持結構化並發。
launch 則會建立新的協程如果改成:

情況就完全不同了。Repository 已經主動建立了一個新的 Job,這意味著:
呼叫方取消自己的協程,並不一定能夠取消 Repository 內部那個新的任務。呼叫方也無法天然知道它什麼時候完成。
withContext改變的是執行執行緒;launch改變的是任務歸屬。
很多公眾號把這兩者混為一談,這是導致 Repository 濫用 launch 的重要原因之一。
真正需要執行緒切換時,優先考慮 withContext;真正需要建立一個擁有獨立生命週期的新任務時,再考慮 launch。
launch 最大的問題:職責越界直覺上覺得自己只是「封裝了一下非同步」。但本質上,你已經額外承擔了這些職責:
Scope 裡跑這些其實都不是普通 Repository 的核心職責。普通 Repository 更像:
而不是:
launch 應該放在哪裡?一個很實用的經驗是:
launch放在邊界層;suspend留在可組合的業務能力層。
kotlin 體驗AI代碼助手 代碼解讀複製代碼fun refresh() {
viewModelScope.launch {
repository.load()
}
}
適用於頁面按鈕點擊、首次載入、下拉重新整理、頁面生命週期觸發的任務。

這裡 launch 是合理的,因為 UseCase 正在定義一個更高層的業務執行過程。
WorkManager / 應用程式級任務協調者如果任務天然就應該脫離頁面生命週期存在(日志上傳、離線同步、大檔案下載、資料遷移),那麼它本來就不該綁定到 viewModelScope,而應該放在 WorkManager 或 application scope 等元件中。
也不是。這裡要區分兩件事。
suspendRepository 內部可以使用 withContext、coroutineScope、supervisorScope、async 等在目前呼叫鏈內部組織子任務:

這裡 Repository 是在「目前呼叫鏈內部組織子任務」,而不是偷偷建立一個脫離呼叫方的頂層任務。這兩者差別非常大。
這才是 Repository 內部啟動協程可能合理的場景,例如使用者點擊「收藏」,希望即使離開頁面也盡量完成寫入。
這時更準確的設計是:

重點不是「終於可以在 Repository 裡 launch 了」,而是:
能在 Repository 內部啟動協程,不代表「預設就該這麼寫」;它只適合那些明確需要脫離呼叫方生命週期的工作。
在程式碼審查時糾結 Repository 要不要自己 launch,可以直接問 4 個問題:
問題 1:這是一次性操作,還是持續資料源?
一次性操作 → 優先 suspend fun;持續資料源 → 優先 Flow。
問題 2:誰最適合決定它的生命週期?
頁面相關 → ViewModel;業務動作入口 → UseCase;脫離頁面的背景任務 → WorkManager / application scope。如果答案不是 Repository,那 Repository 就不該預設自己 launch。
問題 3:呼叫方需不需要知道它何時完成、是否失敗?
只要答案是「需要」,那內部 launch 通常就不是好主意。一旦內部 launch,完成與失敗邊界就會立刻變模糊。
問題 4:這個任務是否必須活得比呼叫方更久?
不需要 → suspend fun;需要 → 才考慮外部注入長生命週期 scope 或背景任務框架。
suspend funFlowcoroutineScope / withContext 組織實作,但不要預設偷開頂層 launchScope這樣做的好處:生命週期歸屬清晰、取消語義清晰、例外傳播清晰、完成時機清晰、分層職責更穩定。
現代 Android 推薦 Repository 暴露
suspend fun,不是因為launch不能用,而是因為一次性資料操作的生命週期,預設應該由呼叫方而不是 Repository 來控制。