This specification is maintained by the AgentRules Working Group. Have enhancements or want to claim maintainership?Submit Specification →
mobilecritical PriorityID: kmp-main-actor-thread-affinity-guardrail

KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement

Enforces strict compile-time and runtime isolation boundaries between native platform dispatchers and shared Compose Multiplatform UI trees. Prevents cross-thread UI mutations, iOS Darwin main queue deadlocks, and background freeze panics caused by leaking non-Main dispatchers into ViewModel state holders.

Prohibited Anti-Pattern: Dispatching Mutable UI State Updates on Non-Main Dispatchers Across iOS and Desktop Targets

Export Ready-to-Drop Configuration

Select format below to copy code or download file directly.

---
description: KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement - Enforces strict compile-time and runtime isolation boundaries between native platform dispatchers and shared Compose Multiplatform UI trees. Prevents cross-thread UI mutations, iOS Darwin main queue deadlocks, and background freeze panics caused by leaking non-Main dispatchers into ViewModel state holders.
globs: commonMain/**/*.kt, iosMain/**/*.kt, androidMain/**/*.kt, **/*ViewModel.kt, **/*StateHolder.kt, **/*UiState.kt
alwaysApply: true
verified_by: "agentrules.org"
license: "CC-BY-4.0"
source: "https://agentrules.org/rules/kmp-main-actor-thread-affinity-guardrail"
---

# KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement

> **Severity**: CRITICAL | **Category**: mobile
> **Anti-Pattern Warning**: Dispatching Mutable UI State Updates on Non-Main Dispatchers Across iOS and Desktop Targets

## System Constraints
You are a Principal Mobile Architect enforcing strict thread affinity and dispatcher boundaries in Kotlin Multiplatform (KMP) and Compose Multiplatform codebases. You must eradicate illegal cross-thread state mutation, unconfined Flow emissions into UI state, and Darwin main queue threading panics on iOS. Enforce Dispatchers.Main.immediate confinement for all StateFlow updates and UI state mutations.

## Strict Guidelines
- Engineers MUST ALWAYS collect and assign Compose-consumed StateFlow and SharedFlow instances on Dispatchers.Main.immediate.
- Engineers MUST NOT expose MutableStateFlow or MutableState instances directly outside the boundary of the owning ViewModel or StateHolder.
- Engineers MUST ALWAYS isolate platform-native callbacks (e.g., Darwin GCD queues, CoreLocation, Android BroadcastReceivers) into structured coroutine channels or flows routed through Dispatchers.Default or Dispatchers.IO before re-dispatching to Dispatchers.Main.immediate.
- ViewModels MUST NEVER execute state mutations on Dispatchers.IO or Dispatchers.Default directly; data loading may occur on background dispatchers, but final reduction into UI state MUST execute under Dispatchers.Main.immediate.
- Code SHOULD leverage context-preserving operators like flowOn() and make explicit use of CoroutineScope pinned to Dispatchers.Main.immediate for UI-bound scopes.
- Components NEVER trigger side-effects from Compose UI compositions without binding to LaunchedEffect or rememberCoroutineScope with explicit Main dispatcher semantics.

## Bad vs Good Implementations
### Thread-safe UI State Emission in KMP ViewModel

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Directly setting uiState.value from Dispatchers.IO triggers race conditions on Android and panics or unrendered state drops on iOS Compose Multiplatform due to non-main thread access to the backing Compose state nodes.
```kotlin
// ANTI-PATTERN: Leaking background dispatcher mutations directly to UI state
class AccountViewModel(
    private val repository: AccountRepository
) : ViewModel() {
    val uiState = MutableStateFlow<AccountState>(AccountState.Loading)

    fun loadAccount() {
        viewModelScope.launch(Dispatchers.IO) {
            val data = repository.fetchAccountDetails()
            // FATAL: Directly updating StateFlow from Dispatchers.IO
            // On iOS (Darwin), this triggers NSInternalInconsistencyException
            // or erratic Compose Multiplatform recomposition drops.
            uiState.value = AccountState.Success(data)
        }
    }
}
```

#### Production Standard (Correct)
> **Standard**: Confines coroutine execution to Dispatchers.Main.immediate for all state modifications while delegating the I/O operation via structured withContext(ioDispatcher). Injects dispatchers for testability and guarantees Darwin main-queue affinity.
```kotlin
// PRODUCTION-GRADE: Explicit dispatcher confinement with Main.immediate reduction
class AccountViewModel(
    private val repository: AccountRepository,
    private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
    private val _uiState = MutableStateFlow<AccountState>(AccountState.Loading)
    val uiState: StateFlow<AccountState> = _uiState.asStateFlow()

    fun loadAccount() {
        viewModelScope.launch(mainDispatcher) {
            _uiState.value = AccountState.Loading
            val result = withContext(ioDispatcher) {
                repository.fetchAccountDetails()
            }
            // GUARANTEED: Mutated exclusively on the Main thread/queue
            _uiState.value = AccountState.Success(result)
        }
    }
}
```

### Bridging iOS Platform Native Callbacks into KMP Coroutine Flow

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Platform delegate methods execute on whatever queue native frameworks dispatch to. Direct callback invocation without channel confinement leaks foreign threads into Compose Multiplatform state.
```kotlin
// ANTI-PATTERN: Direct bridging of Darwin callback to unconfined observer
class DarwinLocationObserver(private val locationManager: CLLocationManager) {
    var onLocationChanged: ((CLLocation) -> Unit)? = null

    fun start() {
        locationManager.delegate = object : NSObject(), CLLocationManagerDelegateProtocol {
            override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) {
                val location = didUpdateLocations.lastOrNull() as? CLLocation ?: return
                // DANGEROUS: Invoked on iOS native arbitrary queue, mutating KMP UI state
                onLocationChanged?.invoke(location)
            }
        }
    }
}
```

#### Production Standard (Correct)
> **Standard**: Uses callbackFlow to encapsulate the native platform callback. Buffers and normalizes native events through Dispatchers.Default, allowing downstreams to safely collect on Dispatchers.Main.immediate without thread affinity violations.
```kotlin
// PRODUCTION-GRADE: callbackFlow with safe channel confinement and main-actor routing
class DarwinLocationObserver(
    private val locationManager: CLLocationManager
) {
    fun observeLocations(): Flow<CLLocation> = callbackFlow {
        val delegate = object : NSObject(), CLLocationManagerDelegateProtocol {
            override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) {
                val location = didUpdateLocations.lastOrNull() as? CLLocation ?: return
                trySend(location)
            }
        }
        locationManager.delegate = delegate
        locationManager.startUpdatingLocation()
        awaitClose {
            locationManager.stopUpdatingLocation()
            locationManager.delegate = null
        }
    }.flowOn(Dispatchers.Default)
```

Recommended MCP Servers (mcp-serverlist.com)

Equip agent with system tools to verify code for this stack.

Recommended Agent Skills (askills.dev)

Ready-to-use executable skill functions tailored to this stack.

Architectural Specification & Invariant Rationale

Architectural Invariant Design: KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement

KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement defines an industrial-grade behavioral standard for mobile engineering. Enforces strict compile-time and runtime isolation boundaries between native platform dispatchers and shared Compose Multiplatform UI trees. Prevents cross-thread UI mutations, iOS Darwin main queue deadlocks, and background freeze panics caused by leaking non-Main dispatchers into ViewModel state holders. Modern software architectures require strict adherence to deterministic patterns to guarantee high availability, maintainability, and zero security vulnerabilities.

Framework Paradigm Shift & Drift MechanicsLLMs are trained on millions of open-source repositories containing outdated code, legacy idioms, and unpatched antipatterns. When coding assistants generate implementations for this stack, they default to common statistical tokens: Dispatching Mutable UI State Updates on Non-Main Dispatchers Across iOS and Desktop Targets Without an explicit system instruction file, developers spend hours debugging silent failures.
Failure Modes & Production Blast Radius

Hazard Analysis: What Breaks Without This Rule

The following failure modes have been cataloged from hundreds of hours of production code reviews where AI assistants produced non-compliant implementations:

Failure ModeAI Symptom (The Anti-Pattern)SeverityProduction Impact & Remedy
AI Hallucination & Deprecation DriftDispatching Mutable UI State Updates on Non-Main Dispatchers Across iOS and Desktop TargetsCritical
Violating this industrial standard introduces regression bugs, security risks, and unexpected runtime failures in production.
Enforce explicit behavioral system directives across all AI coding assistants in your repository.
Silent Architecture InconsistencyAI assistants produce mixed paradigms across files, creating fragmented codebase conventions.Medium
Increases cognitive load during code reviews and leads to maintenance debt across engineering teams.
Pin project conventions in .cursor/rules, CLAUDE.md, and AGENTS.md so all team agents follow identical guardrails.
Architectural Directives in Detail

Step-by-Step Production Directives

Each rule constraint below is injected into the AI agent's system prompt to establish deterministic boundaries:

1

Engineers MUST ALWAYS collect and assign Compose-consumed StateFlow and SharedFlow instances on Dispatchers.Main.immediate.

Engineers MUST ALWAYS collect and assign Compose-consumed StateFlow and SharedFlow instances on Dispatchers.Main.immediate.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
2

Engineers MUST NOT expose MutableStateFlow or MutableState instances directly outside the boundary of the owning ViewModel or StateHolder.

Engineers MUST NOT expose MutableStateFlow or MutableState instances directly outside the boundary of the owning ViewModel or StateHolder.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
3

Engineers MUST ALWAYS isolate platform-native callbacks (e.g., Darwin GCD queues, CoreLocation, Android BroadcastReceivers) into structured coroutine channels or flows routed through Dispatchers.Default or Dispatchers.IO before re-dispatching to Dispatchers.Main.immediate.

Engineers MUST ALWAYS isolate platform-native callbacks (e.g., Darwin GCD queues, CoreLocation, Android BroadcastReceivers) into structured coroutine channels or flows routed through Dispatchers.Default or Dispatchers.IO before re-dispatching to Dispatchers.Main.immediate.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
4

ViewModels MUST NEVER execute state mutations on Dispatchers.IO or Dispatchers.Default directly

ViewModels MUST NEVER execute state mutations on Dispatchers.IO or Dispatchers.Default directly; data loading may occur on background dispatchers, but final reduction into UI state MUST execute under Dispatchers.Main.immediate.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
5

Code SHOULD leverage context-preserving operators like flowOn() and make explicit use of CoroutineScope pinned to Dispatchers.Main.immediate for UI-bound scopes.

Code SHOULD leverage context-preserving operators like flowOn() and make explicit use of CoroutineScope pinned to Dispatchers.Main.immediate for UI-bound scopes.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
6

Components NEVER trigger side-effects from Compose UI compositions without binding to LaunchedEffect or rememberCoroutineScope with explicit Main dispatcher semantics.

Components NEVER trigger side-effects from Compose UI compositions without binding to LaunchedEffect or rememberCoroutineScope with explicit Main dispatcher semantics.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
Technical Questions & Answers

Frequently Asked Technical Questions

Common questions developers face when configuring AI coding assistants for this stack:

Q:Why is KMP Compose Main-Actor Thread Affinity & Dispatcher Boundary Enforcement critical for AI coding assistants?

Enforces strict compile-time and runtime isolation boundaries between native platform dispatchers and shared Compose Multiplatform UI trees. Prevents cross-thread UI mutations, iOS Darwin main queue deadlocks, and background freeze panics caused by leaking non-Main dispatchers into ViewModel state holders. Without explicit behavioral boundaries, AI coding assistants (Cursor, Claude Code, Windsurf, Codex) rely on training data averages, which frequently produce deprecated, inefficient, or vulnerable code patterns.

Q:How does adopting this rule prevent AI hallucinations?

This rule explicitly declares what the AI must avoid (Dispatching Mutable UI State Updates on Non-Main Dispatchers Across iOS and Desktop Targets) and prescribes verified positive examples. Grounding the LLM with negative and positive pairs eliminates guesswork.

Q:Which AI coding environments are supported?

This specification is multi-platform compatible: Cursor (.cursor/rules/*.mdc & .cursorrules), Anthropic Claude Code (CLAUDE.md), Windsurf (.windsurfrules), Codex / Cline (AGENTS.md), and GitHub Copilot (.github/copilot-instructions.md).

Q:Can I combine this rule with other technology stacks?

Yes. Use the Interactive Stack Builder or download an Agent Pack to merge this rule with other frontend, backend, database, security, and DevOps rules without syntax collision.

Open Specification · CC BY 4.0·Free for Personal, Team & Commercial In-Repo Use

Citation & Open Source Attribution Guidelines

This specification is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). You are completely free to adapt and share this rule in your workspace. If citing, including in public AI benchmark datasets, or redistributing, attribution to AgentRules.org is required.

Related MOBILE Rules

View all rules →
1 Rule in Stack

KMP