Skip to content

feat(ui): add launchIO helper to BaseViewModel for background operations#129

Closed
therajanmaurya wants to merge 1 commit intodevfrom
feat/add-launchIO-helper-v2
Closed

feat(ui): add launchIO helper to BaseViewModel for background operations#129
therajanmaurya wants to merge 1 commit intodevfrom
feat/add-launchIO-helper-v2

Conversation

@therajanmaurya
Copy link
Member

@therajanmaurya therajanmaurya commented Feb 25, 2026

Summary

  • Add launchIO helper function to BaseViewModel that launches coroutines on Dispatchers.Default
  • Prevents UI freezing when performing network or database operations by ensuring they run on background threads

Problem

When using viewModelScope.launch { } without specifying a dispatcher, coroutines run on Dispatchers.Main by default. This causes the UI to freeze while waiting for API responses, even if the repository layer uses withContext(Dispatchers.IO), because the coroutine resumes on the Main thread.

Solution

Add a launchIO helper function that all ViewModels inheriting from BaseViewModel can use:

protected fun launchIO(block: suspend CoroutineScope.() -> Unit): Job {
    return viewModelScope.launch(Dispatchers.Default, block = block)
}

Note: Uses Dispatchers.Default instead of Dispatchers.IO for multiplatform compatibility. IO dispatcher 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 thread
  • Compose's collectAsStateWithLifecycle() automatically observes on Main thread
  • Event channels handle cross-thread communication properly
  • Dispatchers.Default provides background thread execution on all KMP targets

Test plan

  • Build project successfully (all platforms including Web)
  • Verify existing ViewModels continue to work
  • Update ViewModels to use launchIO for I/O operations

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Enhanced asynchronous operation handling to deliver improved app responsiveness and more efficient background task processing, with optimized resource utilization across all supported platforms.

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>
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

A new protected method launchIO was added to BaseViewModel to execute coroutine blocks on Dispatchers.Default, returning a Job for task management. Supporting imports for CoroutineScope, Dispatchers, and Job were included.

Changes

Cohort / File(s) Summary
BaseViewModel Enhancement
core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt
Added launchIO protected method with documentation to launch CPU-bound work on Dispatchers.Default via viewModelScope. Includes necessary coroutine imports.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A new dispatcher arrives with grace,
launchIO finds its special place,
CPU work on Default so fast,
In viewModelScope, forever to last! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a launchIO helper function to BaseViewModel for background operations, which directly aligns with the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-launchIO-helper-v2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Default makes it impossible to inject a TestDispatcher during unit tests without monkey-patching. A common pattern is to accept an optional CoroutineDispatcher parameter in the BaseViewModel constructor (or through a CoroutineDispatchers wrapper injected via the DI graph) so tests can pass UnconfinedTestDispatcher or StandardTestDispatcher.

♻️ 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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4a85c9 and c818a35.

📒 Files selected for processing (1)
  • core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt

Comment on lines +104 to +116
/**
* 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)
}
Copy link

@coderabbitai coderabbitai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the BaseViewModel.kt file
find . -name "BaseViewModel.kt" -type f

Repository: 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 5

Repository: 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 -100

Repository: openMF/kmp-project-template

Length of output: 97


🏁 Script executed:

# Search for launchIO usages without type filter
rg -n "launchIO" -C 3

Repository: 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 -50

Repository: 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 -30

Repository: 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.kt

Repository: 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.kt

Repository: 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 3

Repository: 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 -50

Repository: openMF/kmp-project-template

Length of output: 2401


🏁 Script executed:

# Check the DispatcherManager interface
rg -n "interface DispatcherManager" -A 10

Repository: 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 -20

Repository: 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 2

Repository: 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 5

Repository: 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:

  1. Name launchIO implies 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 like launchBackground or launchOnDefault would be accurate.

  2. 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) inside launchIO, Dispatchers.Default's small thread pool will be exhausted, starving all coroutines in the app.

    Additionally, the function bypasses the existing DispatcherManager abstraction, using raw Dispatchers.Default instead 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment on lines +104 to +116
/**
* 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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants