feat(ui): add launchIO helper to BaseViewModel for background operations#129
feat(ui): add launchIO helper to BaseViewModel for background operations#129therajanmaurya wants to merge 1 commit intodevfrom
Conversation
Add a launchIO helper function to BaseViewModel that launches coroutines
on Dispatchers.Default. This prevents UI freezing when performing network
or database operations by ensuring they run on background threads.
Uses Dispatchers.Default instead of Dispatchers.IO for multiplatform
compatibility (IO is not available on JS/WasmJS targets).
Usage:
- Replace `viewModelScope.launch { }` with `launchIO { }` for I/O operations
- StateFlow updates remain thread-safe
- Progress indicators animate smoothly during API calls
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughA new protected method Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt (1)
114-116: Consider making the dispatcher injectable for unit testing.Hardcoding
Dispatchers.Defaultmakes it impossible to inject aTestDispatcherduring unit tests without monkey-patching. A common pattern is to accept an optionalCoroutineDispatcherparameter in theBaseViewModelconstructor (or through aCoroutineDispatcherswrapper injected via the DI graph) so tests can passUnconfinedTestDispatcherorStandardTestDispatcher.♻️ Example approach
abstract class BaseViewModel<S, E, A>( initialState: S, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel() { ... - protected fun launchBackground(block: suspend CoroutineScope.() -> Unit): Job { - return viewModelScope.launch(Dispatchers.Default, block = block) + protected fun launchBackground(block: suspend CoroutineScope.() -> Unit): Job { + return viewModelScope.launch(ioDispatcher, block = block) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt` around lines 114 - 116, The launchIO method in BaseViewModel hardcodes Dispatchers.Default which prevents injecting test dispatchers; update BaseViewModel to accept an injectable CoroutineDispatcher (or a CoroutineDispatchers wrapper) via its constructor or DI and use that dispatcher in launchIO (replace Dispatchers.Default with the injected dispatcher), keep a default value of Dispatchers.Default for production but allow tests to pass UnconfinedTestDispatcher/StandardTestDispatcher; refer to BaseViewModel, launchIO, and viewModelScope when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt`:
- Around line 104-116: The function launchIO currently uses Dispatchers.Default
and has misleading KDoc; rename launchIO to launchBackground (or
launchOnDefault), update its KDoc to state it launches on the
platform-appropriate background dispatcher via DispatcherManager.default (not
"I/O-bound" wording), and change the implementation to use
dispatcherManager.default (e.g.,
viewModelScope.launch(dispatcherManager.default, block = block)) so callers get
the correct dispatcher mapping across platforms; update any call sites of
launchIO to the new name.
---
Nitpick comments:
In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt`:
- Around line 114-116: The launchIO method in BaseViewModel hardcodes
Dispatchers.Default which prevents injecting test dispatchers; update
BaseViewModel to accept an injectable CoroutineDispatcher (or a
CoroutineDispatchers wrapper) via its constructor or DI and use that dispatcher
in launchIO (replace Dispatchers.Default with the injected dispatcher), keep a
default value of Dispatchers.Default for production but allow tests to pass
UnconfinedTestDispatcher/StandardTestDispatcher; refer to BaseViewModel,
launchIO, and viewModelScope when making the change.
| /** | ||
| * Launches a coroutine on [Dispatchers.Default] for network or database operations. | ||
| * Use this instead of `viewModelScope.launch` for any I/O-bound work to avoid | ||
| * blocking the main thread and causing UI freezes. | ||
| * | ||
| * Note: Uses Default dispatcher for multiplatform compatibility (IO is not available on JS). | ||
| * | ||
| * @param block The suspending block to execute on the background dispatcher. | ||
| * @return The [Job] representing the coroutine. | ||
| */ | ||
| protected fun launchIO(block: suspend CoroutineScope.() -> Unit): Job { | ||
| return viewModelScope.launch(Dispatchers.Default, block = block) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the BaseViewModel.kt file
find . -name "BaseViewModel.kt" -type fRepository: openMF/kmp-project-template
Length of output: 146
🏁 Script executed:
# Search for launchIO function definition and any usages
rg -n "launchIO" --type=kt -B 2 -A 5Repository: openMF/kmp-project-template
Length of output: 97
🏁 Script executed:
# Check the specific file to see the actual current state
cat -n core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt | sed -n '100,120p'Repository: openMF/kmp-project-template
Length of output: 950
🏁 Script executed:
# Search for viewModelScope.launch usage patterns in ViewModels to understand practices
rg -n "viewModelScope\.launch\|launchIO" --type=kt -C 2 | head -100Repository: openMF/kmp-project-template
Length of output: 97
🏁 Script executed:
# Search for launchIO usages without type filter
rg -n "launchIO" -C 3Repository: openMF/kmp-project-template
Length of output: 916
🏁 Script executed:
# Search for Dispatchers.Default and Dispatchers.IO usage to understand context
rg -n "Dispatchers\." --glob="*.kt" --glob="*.kts" | head -50Repository: openMF/kmp-project-template
Length of output: 3685
🏁 Script executed:
# Check for any blocking I/O patterns (Thread.sleep, file I/O, etc) in ViewModels
rg -n "Thread\.sleep|readText|readBytes|blockingGet|runBlocking" --glob="*.kt" --glob="*.kts" | head -30Repository: openMF/kmp-project-template
Length of output: 510
🏁 Script executed:
# Examine DispatchManagerImpl to understand the dispatcher abstraction
cat -n core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.ktRepository: openMF/kmp-project-template
Length of output: 1383
🏁 Script executed:
# Check androidMain version
cat -n core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.ktRepository: openMF/kmp-project-template
Length of output: 1378
🏁 Script executed:
# Check if BaseViewModel has access to DispatchManager or similar abstractions
rg -n "DispatchManager\|CoroutineDispatcher" core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt -B 3 -A 3Repository: openMF/kmp-project-template
Length of output: 53
🏁 Script executed:
# Check full BaseViewModel to understand its dependencies
cat -n core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt | head -50Repository: openMF/kmp-project-template
Length of output: 2401
🏁 Script executed:
# Check the DispatcherManager interface
rg -n "interface DispatcherManager" -A 10Repository: openMF/kmp-project-template
Length of output: 1380
🏁 Script executed:
# Check if BaseViewModel should have access to DispatcherManager
rg -n "DispatcherManager" --glob="*.kt" | grep -E "(inject|provide|Module)" | head -20Repository: openMF/kmp-project-template
Length of output: 1032
🏁 Script executed:
# Look for imports of DispatcherManager in UI layer
rg -n "DispatcherManager" core-base/ui/ -B 2 -A 2Repository: openMF/kmp-project-template
Length of output: 53
🏁 Script executed:
# Check if there's a pattern for injecting DispatcherManager into ViewModels
rg -n "ViewModel" core-base/common/src/commonMain/kotlin/template/core/base/common/manager/ -A 5 -B 5Repository: openMF/kmp-project-template
Length of output: 53
launchIO name and KDoc are semantically incorrect for Dispatchers.Default.
Dispatchers.Default is a CPU-bound dispatcher (thread pool capped at CPU cores). Dispatchers.IO — available on JVM and Native targets — is the dispatcher designed for offloading blocking I/O-intensive operations. The codebase already has a DispatcherManager abstraction that handles this: on Android, manager.default maps to Dispatchers.IO; on non-Android platforms, it falls back to Dispatchers.Default for compatibility.
Two concrete problems with the current implementation:
-
Name
launchIOimplies IO-dispatcher semantics. Developers reading call sites will expect optimal behavior for I/O operations (on Android, the 64-thread IO pool); instead they get a pool capped at CPU cores. A name likelaunchBackgroundorlaunchOnDefaultwould be accurate. -
KDoc is inaccurate and dangerous. The comment reads "for network or database operations" and "I/O-bound work" — the exact description of
Dispatchers.IO. If a developer follows the KDoc literally and performs blocking I/O (e.g., legacy JDBC,File.readText) insidelaunchIO,Dispatchers.Default's small thread pool will be exhausted, starving all coroutines in the app.Additionally, the function bypasses the existing
DispatcherManagerabstraction, using rawDispatchers.Defaultinstead of injecting the platform-appropriate dispatcher.
✏️ Suggested rename and corrected KDoc
- /**
- * Launches a coroutine on [Dispatchers.Default] for network or database operations.
- * Use this instead of `viewModelScope.launch` for any I/O-bound work to avoid
- * blocking the main thread and causing UI freezes.
- *
- * Note: Uses Default dispatcher for multiplatform compatibility (IO is not available on JS).
- *
- * `@param` block The suspending block to execute on the background dispatcher.
- * `@return` The [Job] representing the coroutine.
- */
- protected fun launchIO(block: suspend CoroutineScope.() -> Unit): Job {
- return viewModelScope.launch(Dispatchers.Default, block = block)
- }
+ /**
+ * Launches a coroutine on [Dispatchers.Default] for background work such as
+ * calling suspending repository or data-source APIs off the main thread.
+ *
+ * **Important:** The [block] must only invoke *suspending* (non-blocking) functions.
+ * Performing blocking I/O (e.g. `Thread.sleep`, blocking file/network APIs) will
+ * exhaust the Default dispatcher's CPU-sized thread pool and starve other coroutines.
+ *
+ * [Dispatchers.Default] is used instead of [Dispatchers.IO] for Kotlin Multiplatform
+ * compatibility — `Dispatchers.IO` is not available on JS/WasmJS targets.
+ *
+ * `@param` block The suspending block to execute on the background dispatcher.
+ * `@return` The [Job] representing the coroutine.
+ */
+ protected fun launchBackground(block: suspend CoroutineScope.() -> Unit): Job {
+ return viewModelScope.launch(Dispatchers.Default, block = block)
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt`
around lines 104 - 116, The function launchIO currently uses Dispatchers.Default
and has misleading KDoc; rename launchIO to launchBackground (or
launchOnDefault), update its KDoc to state it launches on the
platform-appropriate background dispatcher via DispatcherManager.default (not
"I/O-bound" wording), and change the implementation to use
dispatcherManager.default (e.g.,
viewModelScope.launch(dispatcherManager.default, block = block)) so callers get
the correct dispatcher mapping across platforms; update any call sites of
launchIO to the new name.
There was a problem hiding this comment.
@therajanmaurya This review is correct, and ViewModelScope already bounded to the lifecycle of the app with the required dispatcher, and we won't need this anti-pattern, and hardcoding the dispatcher will cause the codebase to test as while testing we need to swap out with unconfined dispather.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| /** | ||
| * Launches a coroutine on [Dispatchers.Default] for network or database operations. | ||
| * Use this instead of `viewModelScope.launch` for any I/O-bound work to avoid | ||
| * blocking the main thread and causing UI freezes. | ||
| * | ||
| * Note: Uses Default dispatcher for multiplatform compatibility (IO is not available on JS). | ||
| * | ||
| * @param block The suspending block to execute on the background dispatcher. | ||
| * @return The [Job] representing the coroutine. | ||
| */ | ||
| protected fun launchIO(block: suspend CoroutineScope.() -> Unit): Job { | ||
| return viewModelScope.launch(Dispatchers.Default, block = block) | ||
| } |
There was a problem hiding this comment.
@therajanmaurya This review is correct, and ViewModelScope already bounded to the lifecycle of the app with the required dispatcher, and we won't need this anti-pattern, and hardcoding the dispatcher will cause the codebase to test as while testing we need to swap out with unconfined dispather.
Summary
launchIOhelper function toBaseViewModelthat launches coroutines onDispatchers.DefaultProblem
When using
viewModelScope.launch { }without specifying a dispatcher, coroutines run onDispatchers.Mainby default. This causes the UI to freeze while waiting for API responses, even if the repository layer useswithContext(Dispatchers.IO), because the coroutine resumes on the Main thread.Solution
Add a
launchIOhelper function that all ViewModels inheriting fromBaseViewModelcan use:Note: Uses
Dispatchers.Defaultinstead ofDispatchers.IOfor multiplatform compatibility.IOdispatcher is only available on JVM/Android/Native targets, not on JavaScript/WasmJS.Usage
Replace:
viewModelScope.launch { val result = repository.makeApiCall() // UI frozen while waiting }With:
launchIO { val result = repository.makeApiCall() // Runs on background thread, UI stays responsive }Why This Works
StateFlow.update {}is thread-safe and can be called from any threadcollectAsStateWithLifecycle()automatically observes on Main threadDispatchers.Defaultprovides background thread execution on all KMP targetsTest plan
launchIOfor I/O operations🤖 Generated with Claude Code
Summary by CodeRabbit