# Reactive Settings ViewModel (24-field combine)

## Problem

NuvioTV and similar apps load preferences once in `init {}`:
```kotlin
init {
    viewModelScope.launch {
        val theme = prefs.theme.first()
        val accent = prefs.accentColorName.first()
        // ... one .first() call per field
        _uiState.value = SettingsUiState(theme = ..., accent = ...)
    }
}
```

This is fragile:
- Only reflects the snapshot at load time
- Changing a pref from a sub-screen doesn't propagate to other open settings screens
- Requires manual reload calls

## Solution: Reactive combine() on Dispatchers.IO

```kotlin
@HiltViewModel
class SettingsViewModel @Inject constructor(
    private val prefs: PreferencesManager,
) : ViewModel() {
    @Stable
    data class SettingsUiState(
        val theme: AppTheme = AppTheme.OCEAN,
        val posterShape: String = "poster",
        val colorBlindMode: Boolean = false,
        val defaultPlaybackSpeed: Float = 1.0f,
        val enableAutoPlay: Boolean = true,
        val preferredQuality: String = "1080p",
        val subtitleLanguage: String = "English",
        val subtitleSize: Int = 24,
        val subtitleColor: String = "white",
        val skipIntroMode: String = "show_button",
        val nextEpisodeCountdown: Int = 10,
        // ... 14 more fields
        val isLoading: Boolean = true,
    )

    private val _uiState = MutableStateFlow(SettingsUiState())
    val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()

    init {
        combine(
            prefs.theme,
            prefs.posterShape,
            prefs.colorBlindMode,
            prefs.defaultPlaybackSpeed,
            prefs.enableAutoPlay,
            prefs.preferredQuality,
            prefs.subtitleLanguage,
            prefs.subtitleSize,
            prefs.subtitleColor,
            // ... 14 more flows
            prefs.nextEpisodeCountdown,
        ) { args ->
            SettingsUiState(
                theme = parseTheme(args[0] as? String),
                posterShape = args[1] as? String ?: "poster",
                colorBlindMode = args[2] as? Boolean ?: false,
                defaultPlaybackSpeed = args[3] as? Float ?: 1.0f,
                enableAutoPlay = args[4] as? Boolean ?: true,
                preferredQuality = args[5] as? String ?: "1080p",
                subtitleLanguage = args[6] as? String ?: "English",
                subtitleSize = args[7] as? Int ?: 24,
                subtitleColor = args[8] as? String ?: "white",
                // ... cast every arg explicitly
                nextEpisodeCountdown = args[23] as? Int ?: 10,
                isLoading = false,
            )
        }
            .flowOn(Dispatchers.IO)        // offload DataStore reads from Main
            .onEach { _uiState.value = it } // write to output flow
            .launchIn(viewModelScope)       // collect for ViewModel lifetime
    }
}
```

## Key rules

1. **`combine()` returns `Array<Any?>`** — destructuring only works for ≤5 args. Beyond that, use indexed access with explicit casts.
2. **Always provide fallback values** — `args[N] as? Type ?: default`. A null arg (race condition, cleared pref) shouldn't crash the pipeline.
3. **`.flowOn(Dispatchers.IO)` BEFORE `.onEach`** — moves the combine lambda to IO. `.onEach` runs on the collector's dispatcher (Main via viewModelScope).
4. **Never nest `.stateIn()` inside `viewModelScope.launch`** — the StateFlow is created but never collected. Use `.onEach { _uiState.value = it }.launchIn(viewModelScope)` for ViewModels that own their own MutableStateFlow.
5. **Setters update optimistically** — set `_uiState.update { it.copy(field = value) }` immediately, then persist to DataStore in `viewModelScope.launch`. The combine() will reconcile on the next emission but the UI feels instant.

## When to use

- Settings screens with 10+ preferences
- Multi-screen workflows where pref changes must propagate reactively
- Any ViewModel where DataStore reads significantly outnumber writes

## When NOT to use

- 1-3 simple prefs — `first()` on init is fine
- Single-shot config screens (onboarding, initial setup wizard)
- Cases where the DataStore/Flow overhead would dominate cold-start time (profile for your specific device stack)

## Applied in

- UNSPOOLED `SettingsViewModel` — 24 fields, May 2026 session, branch `nuvio-rebrand-hybrid`
- NuvioTV originally used one-shot `.first()` pattern; UNSPOOLED improved to reactive combine
