# remember → rememberSaveable Audit Pattern

## When to apply

Run this audit when:
- Focus or selection state resets unexpectedly on back-stack re-entry
- Tab/panel indices reset to 0 after navigating away and back
- User text input disappears on configuration change (rotation, language)
- Dialog state doesn't survive recomposition
- You're doing a post-feature cleanup pass across the codebase

## Audit commands

```bash
# Find all remember { mutableStateOf/mutableIntStateOf in the UI layer
rg -n 'remember\s*\{\s*mutable(StateOf|IntStateOf)' app/src/main/java/com/unspooled/app/ui/ --include='*.kt'
```

## Classification rules

### ✅ CONVERT to rememberSaveable (survive recomposition, back-stack, config changes)

| State type | Pattern | Example |
|-----------|---------|---------|
| Int index | `rememberSaveable { mutableIntStateOf(0) }` | `selectedTabIndex`, `lastFocusedRowIndex` |
| Boolean dialog | `rememberSaveable { mutableStateOf(false) }` | `showResetDialog`, `showCreator` |
| String input | `rememberSaveable { mutableStateOf("") }` | `searchQuery`, `pinInput`, `key` |
| User selection | `rememberSaveable { mutableStateOf<T?>(null) }` | `selectedRating`, `focusedColor` |
| Focus position | `rememberSaveable { mutableStateOf(0) }` | `lastFocusedProfileIndex` |

### ❌ EXEMPT — keep as `remember` (transient, no meaningful saved value)

| State type | Reason | Example |
|-----------|--------|---------|
| `isFocused` / `*Focused` | Visual focus ring — resets every focus change | `var focused by remember { mutableStateOf(false) }` |
| Video player state | Playback-transient | `isPaused`, `seekToken`, `autoHidden` |
| Animation state | Tied to composition lifecycle | `animateFloatAsState`, `graphicsLayer` |
| FocusRequester | Ephemeral — garbage collected on leave | `remember { FocusRequester() }` |
| LazyListState | Use `rememberLazyListState()` (handles save internally) | `rememberLazyListState()` |
| HazeState | Tied to composition | `remember { HazeState() }` |
| Layout measurement | Re-derived every layout pass | `textLayoutResult`, `containerWidth`, `sizeInDp` |
| Debug counters | Anti-pattern to persist | `RecompositionTracker.count` |
| Non-serializable objects | `MediaPlayer`, custom classes without Saver | `mediaPlayer`, raw `Profile?` |

## Non-Parcelable types — Option A (save ID, derive from list)

When a `rememberSaveable` target type is not `@Parcelize`/`Parcelable`:
```kotlin
// BEFORE (breaks — Profile? is not Parcelable)
var pinTarget by remember { mutableStateOf<Profile?>(null) }

// AFTER — save the stable ID, derive the object
var pinTargetId by rememberSaveable { mutableStateOf<Int?>(null) }
val pinTarget = profiles.find { it.id == pinTargetId }

// Assignments:
pinTarget = profile        →   pinTargetId = profile.id
pinTarget = null           →   pinTargetId = null
```

**Constraint:** The derived `val` must be declared AFTER the list it depends on:
```kotlin
// WRONG — profiles not yet defined
val pinTarget = profiles.find { it.id == pinTargetId }
val profiles = listState.profiles

// CORRECT
val profiles = listState.profiles
val pinTarget = profiles.find { it.id == pinTargetId }
```

## Import pitfall

`rememberSaveable` is in `androidx.compose.runtime.saveable`, NOT `androidx.compose.runtime`:
```kotlin
// Does NOT include rememberSaveable
import androidx.compose.runtime.*

// Must add explicitly
import androidx.compose.runtime.saveable.rememberSaveable
```

**Avoid replacing wildcard imports with explicit ones** — removing `import androidx.compose.runtime.*` and adding only `remember` + `rememberSaveable` deletes `Composable`, `LaunchedEffect`, `mutableStateOf`, etc. causing cascading compile errors. Instead, add `rememberSaveable` alongside existing imports.

## Build-after-each rule

Never batch `remember` → `rememberSaveable` conversions. Convert all instances in one file → `./gradlew :app:compileDebugKotlin` → confirm pass → next file.

## Validation checklist

After audit + conversion:
- [ ] `./gradlew :app:compileDebugKotlin :app:testDebugUnitTest` passes
- [ ] No `rememberSaveable` applied to `FocusRequester`, `LazyListState`, `HazeState`
- [ ] All `isFocused` / `*Focused` visual state left as `remember`
- [ ] No derived vals declared before their dependency lists
- [ ] `rememberSaveable` import present in every modified file
