## Android/Kotlin Component Generation Guide

> **MUST: Figma MCP-first (BLOCKING).** If the task references any Figma frame (URL, node ID, or "from the design"), the Dev phase MUST call `mcp__claude_ai_Figma__get_design_context` for every frame BEFORE writing a single Composable line. Use the `CodeConnectSnippet` component name verbatim - no sound-alike substitutions. Authentication failure is not a skip path. Full rule, trigger conditions, and gate failure modes: `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma MCP-first (BLOCKING)". Phase wiring: `$HOME/.claude/multi-agent-refs/phases/phase-3-dev.md` "MUST: Figma MCP-first (BLOCKING pre-step)".

When the task involves creating an Android UI component (Jetpack Compose), follow this architecture.

### Component Architecture: State / Screen / Content

| File                 | When         | Content                                             |
| -------------------- | ------------ | --------------------------------------------------- |
| `{Name}UiState.kt`   | Always       | Data class representing screen/component state      |
| `{Name}ViewModel.kt` | Screen-level | ViewModel with StateFlow, business logic            |
| `{Name}Screen.kt`    | Screen-level | Stateful wrapper: collects state, passes to Content |
| `{Name}Content.kt`   | Always       | Stateless @Composable, receives state + lambdas     |
| `{Name}Preview.kt`   | Always       | @Preview functions for all meaningful variants      |
| `{Name}Test.kt`      | Always       | Compose testing + ViewModel tests                   |

### Simple vs Complex Decision

- **Screen-level** (has ViewModel, navigation) -> Full architecture: UiState + ViewModel + Screen + Content
- **Reusable component** (no ViewModel, receives props) -> Content only, stateless composable
- **Variant-driven** (e.g. ButtonType enum with 5+ cases) -> Extract to separate component file

### State Pattern

```kotlin
// UiState: immutable data class
data class LoginUiState(
    val email: String = "",
    val password: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

// ViewModel: single StateFlow
class LoginViewModel @Inject constructor(
    private val authRepo: AuthRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()

    fun onEmailChanged(email: String) {
        _uiState.update { it.copy(email = email) }
    }
}

// Screen: stateful wrapper (never pass ViewModel to children)
@Composable
fun LoginScreen(viewModel: LoginViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    LoginContent(
        uiState = uiState,
        onEmailChanged = viewModel::onEmailChanged,
        onLoginClicked = viewModel::onLogin
    )
}

// Content: stateless, testable, previewable
@Composable
fun LoginContent(
    uiState: LoginUiState,
    onEmailChanged: (String) -> Unit,
    onLoginClicked: () -> Unit
) { ... }
```

### Token Discipline

**Zero magic numbers. Zero hardcoded colors. Zero hardcoded strings.**

| Bad                     | Good                                                       |
| ----------------------- | ---------------------------------------------------------- |
| `padding = 16.dp`       | `padding = MaterialTheme.spacing.medium` or named constant |
| `Color(0xFFE31837)`     | `MaterialTheme.colorScheme.primary`                        |
| `.fontSize = 14.sp`     | `MaterialTheme.typography.bodyMedium`                      |
| `"Login"` in composable | `stringResource(R.string.login)`                           |

If the project has no spacing system:

```kotlin
private object Spacing {
    val small = 8.dp
    val medium = 16.dp
    val large = 24.dp
}
```

### Stability for Performance

```kotlin
// Mark as @Immutable if all fields are val and immutable types
@Immutable
data class ButtonConfig(
    val text: String,
    val type: ButtonType,
    val enabled: Boolean = true
)

// Use remember for expensive computations
val formattedDate = remember(timestamp) { dateFormatter.format(timestamp) }
```

### Accessibility

Every interactive element MUST have:

- `contentDescription` for images/icons
- `semantics { }` block for custom accessibility info
- `testTag` for UI testing
- Minimum touch target: **48x48dp** (Material guidelines)

```kotlin
Icon(
    imageVector = Icons.Default.Close,
    contentDescription = stringResource(R.string.close),
    modifier = Modifier
        .testTag("close_button")
        .size(48.dp)
)
```

### Preview Best Practices

```kotlin
@Preview(name = "Default")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Large Font", fontScale = 2f)
@Preview(name = "RTL", locale = "ar")
@Composable
private fun LoginContentPreview() {
    AppTheme {
        LoginContent(
            uiState = LoginUiState(email = "test@example.com"),
            onEmailChanged = {},
            onLoginClicked = {}
        )
    }
}
```

### Testing

```kotlin
// Compose UI test
@get:Rule val composeTestRule = createComposeRule()

@Test
fun loginButton_displaysCorrectText() {
    composeTestRule.setContent {
        LoginContent(uiState = LoginUiState(), ...)
    }
    composeTestRule.onNodeWithText("Login").assertIsDisplayed()
}

// ViewModel test with Turbine
@Test
fun onEmailChanged_updatesState() = runTest {
    val viewModel = LoginViewModel(FakeAuthRepository())
    viewModel.uiState.test {
        assertEquals("", awaitItem().email)
        viewModel.onEmailChanged("test@example.com")
        assertEquals("test@example.com", awaitItem().email)
    }
}
```

### Build Verification

After implementation:

- `./gradlew assembleDebug`  -  zero errors
- `./gradlew testDebugUnitTest`  -  all tests pass
- `./gradlew ktlintCheck`  -  lint clean
- Previews render in Android Studio

### Component Quality Checklist

1. No magic numbers  -  all values are theme tokens or named constants
2. State hoisting  -  stateful wrapper + stateless content pattern
3. No ViewModel in child composables  -  pass state + lambdas
4. Accessibility  -  contentDescription, testTag, 48dp touch targets
5. Preview coverage  -  default, dark mode, large font, RTL
6. Tests  -  Compose UI tests + ViewModel unit tests
7. Dark mode  -  correct with MaterialTheme.colorScheme
8. Strings  -  all use `stringResource()`, not hardcoded
9. Stability  -  `@Immutable`/`@Stable` where appropriate
10. Build passes  -  zero errors, lint clean

### Compliance Rules (maps to multi-agent-toolkit MCP audit tools)

These rules ensure your code passes `android_accessibility_audit` and `android_apk_audit` without issues.

#### Accessibility (validated by `android_accessibility_audit`)

| Rule                                             | What Audit Checks          | How to Pass                                                |
| ------------------------------------------------ | -------------------------- | ---------------------------------------------------------- |
| Every clickable element has `contentDescription` | Missing on clickable views | `contentDescription = stringResource(R.string.close)`      |
| Every interactive element has `testTag`          | Missing resource-id        | `Modifier.testTag("button_submit")`                        |
| Minimum touch target 48x48dp                     | Bounds < 48x48             | `Modifier.size(48.dp)` or `Modifier.defaultMinSize(48.dp)` |

```kotlin
// This PASSES audit:
IconButton(
    onClick = onClose,
    modifier = Modifier
        .testTag("button_close")
        .size(48.dp)
) {
    Icon(
        imageVector = Icons.Default.Close,
        contentDescription = stringResource(R.string.close)
    )
}

// This FAILS audit:
IconButton(onClick = onClose) {       // no testTag, might be < 48dp
    Icon(Icons.Default.Close, null)    // contentDescription = null
}
```

#### Release Compliance (validated by `android_apk_audit`)

| Rule                      | What Audit Checks              | How to Pass                                            |
| ------------------------- | ------------------------------ | ------------------------------------------------------ |
| Target SDK >= 34          | `targetSdkVersion` in manifest | `targetSdk = 34` in build.gradle                       |
| Not debuggable in release | `android:debuggable=true`      | `debuggable = false` in release buildType (default)    |
| R8/ProGuard enabled       | Large DEX file count           | `isMinifyEnabled = true` in release buildType          |
| APK size < 150MB          | APK file size                  | Use App Bundle (.aab), enable R8, shrink resources     |
| v2+ signing               | Only v1 signature              | Configure `signingConfigs` with v2 signing enabled     |
| No excessive permissions  | Dangerous permission count     | Audit `AndroidManifest.xml`, remove unused permissions |

```kotlin
// build.gradle.kts
android {
    defaultConfig {
        targetSdk = 35
    }
    buildTypes {
        release {
            isMinifyEnabled = true        // R8 enabled
            isShrinkResources = true      // remove unused resources
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}
```

```xml
<!-- AndroidManifest.xml: only declare permissions you actually use -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Remove unused: RECORD_AUDIO, READ_CONTACTS, etc. if not needed -->
```
