@@ -24,6 +24,95 @@ automatically when the owner is destroyed, so no work touches a dead view.
2424Use ` Dispatchers.IO ` for disk/network/DB, ` Dispatchers.Main ` (or ` withContext(Main) ` ) to
2525touch views.
2626
27+ ## ` AsyncTask ` → Coroutines
28+
29+ ` AsyncTask ` is deprecated. Map its callbacks onto a single ` launch ` :
30+
31+ | ` AsyncTask ` | Coroutine |
32+ | ---| ---|
33+ | ` onPreExecute() ` | code before ` withContext(IO) ` (runs on Main) |
34+ | ` doInBackground() ` | ` withContext(Dispatchers.IO) { ... } ` |
35+ | ` onPostExecute(result) ` | code after, back on ` Dispatchers.Main ` |
36+ | ` isCancelled() ` | ` isActive ` (or ` ensureActive() ` ) |
37+ | ` cancel(true) ` | ` job.cancel() ` |
38+ | the ` AsyncTask ` instance field | the returned ` Job ` |
39+
40+ A self-contained task class stops extending ` AsyncTask ` and holds the lifecycle owner so
41+ its scope cancels with the screen:
42+
43+ ``` kotlin
44+ // BEFORE: class GallerySearchTask extends AsyncTask<Void, Void, Result> { ... }
45+ class GallerySearchTask (
46+ private val fragment : GalleryFragment ,
47+ private val user : User ,
48+ private val storageManager : FileDataStorageManager ,
49+ private val endDate : Long ,
50+ private val limit : Int
51+ ) {
52+ fun execute (): Job = fragment.lifecycleScope.launch(Dispatchers .IO ) {
53+ if (! isActive) return @launch
54+ val context = fragment.context ? : return @launch
55+ val result = performSearch(context)
56+ withContext(Dispatchers .Main ) {
57+ fragment.searchCompleted(result.emptySearch, result.lastTimestamp)
58+ }
59+ }
60+ }
61+ ```
62+
63+ The caller keeps the ` Job ` and cancels it with the view:
64+
65+ ``` kotlin
66+ private var photoSearchTask: Job ? = null
67+ // ...
68+ photoSearchTask = GallerySearchTask (this , user, storageManager, endDate, limit).execute()
69+
70+ override fun onDestroyView () {
71+ photoSearchTask?.cancel()
72+ photoSearchTask = null
73+ }
74+ ```
75+
76+ Because ` lifecycleScope ` cancels on destroy, the old ` WeakReference<Fragment> ` leak-guard
77+ disappears — a direct ` fragment ` reference plus ` fragment.context ?: return@launch ` is
78+ enough. (PR #16908 )
79+
80+ ## ` AsyncTask ` Behind a Callback API → ` runCatching ` + ` fold `
81+
82+ When a service method hid an ` AsyncTask ` that set mutable fields (a boolean success flag,
83+ ` errorMessage ` , result fields) and dispatched them in ` onPostExecute ` , keep the public
84+ callback signature but drive it from a coroutine. Return an ** immutable** result from the
85+ background function and branch with ` fold ` :
86+
87+ ``` kotlin
88+ private data class ActivitiesResult (
89+ val activities : List <Any >,
90+ val client : NextcloudClient ,
91+ val lastGiven : Long
92+ )
93+
94+ override fun getActivities (lastGiven : Long , callback : ActivitiesServiceCallback ) {
95+ scope.launch {
96+ runCatching { withContext(Dispatchers .IO ) { fetchActivities(lastGiven) } }
97+ .fold(
98+ onSuccess = { (activities, client, updated) ->
99+ callback.onLoaded(activities, client, updated)
100+ },
101+ onFailure = { callback.onError(it.message ? : " " ) }
102+ )
103+ }
104+ }
105+ ```
106+
107+ ` runCatching ` / ` fold ` replaces the boolean-success + ` errorMessage ` -field idiom; the
108+ background function ` fetchActivities ` throws on failure instead of returning ` false ` , and
109+ the immutable ` ActivitiesResult ` destructures straight into ` onSuccess ` . (PR #16654 )
110+
111+ > ** Scope caveat:** that PR launched on an ad-hoc ` CoroutineScope(Dispatchers.Main) ` with
112+ > no lifecycle owner — it never cancels and can leak. Prefer an injected ` CoroutineScope `
113+ > or expose a ` suspend ` function the presenter runs on ` lifecycleScope ` / ` viewModelScope ` .
114+ > Flag ad-hoc scopes in review.
115+
27116## ` new Thread { ... runOnUiThread(...) } ` → ` lifecycleScope.launch ` + ` withContext `
28117
29118``` kotlin
@@ -115,6 +204,9 @@ private suspend fun loadAndPartitionShares(): Pair<List<OCShare>, List<OCShare>>
115204## Pitfalls
116205
117206- Never launch on ` GlobalScope ` — it outlives the screen and leaks.
207+ - An ad-hoc ` CoroutineScope(Dispatchers.Main) ` created inside a service/presenter has the
208+ same problem: no lifecycle owner, no cancellation, a leak. Use an injected scope or a
209+ ` suspend ` function the caller runs on ` lifecycleScope ` / ` viewModelScope ` .
118210- Keep ` try/catch ` semantics: coroutine cancellation throws ` CancellationException ` ;
119211 rethrow it (don't swallow in a broad ` catch (e: Exception) ` that logs and continues) or
120212 catch ` Exception ` only around the real work, not around the whole ` launch ` .
0 commit comments