Claude Code Mobile Development Flutter React Native Swift Kotlin Compose CLAUDE.md AI Tools Cursor

AI Tools for Mobile Development: Flutter, React Native, and Native (2026)

The Prompt Shelf ·

Mobile development is where AI coding tools earn their keep — or fail visibly. The frameworks are opinionated, the build toolchains are fragile, and the code generation traps are framework-specific enough that a generic CLAUDE.md will send Claude down the wrong path on every third task.

This guide covers how Claude Code, Cursor, OpenAI Codex CLI, and Aider perform across the four major mobile stacks — Flutter, React Native, Swift/SwiftUI, and Kotlin/Compose — with concrete workflow patterns and CLAUDE.md / AGENTS.md templates you can copy directly into your project.

If you’re already using Claude Code for Flutter and want the deep-dive on that specific stack, the Claude Code Flutter Guide covers Riverpod patterns, clean architecture, and testing in detail. This article is the multi-framework complement: what changes when you switch stacks, and how to configure AI tools for each one.

The Mobile AI Tooling Landscape in 2026

AI coding tools have matured enough that the question is no longer “does this work for mobile?” — it’s “how do I configure it to work well for my specific stack?”

The core challenge is that mobile frameworks have tight conventions that AI tools don’t know about by default:

  • Flutter: Dart’s type system, Riverpod’s reactive model, code generation with build_runner
  • React Native: The JavaScript/TypeScript bridge (now largely replaced by JSI/the New Architecture), Metro bundler quirks, platform-specific native modules
  • Swift/SwiftUI: Swift’s value semantics, SwiftData/Core Data patterns, async/await concurrency model, Combine vs. Swift Concurrency
  • Kotlin/Compose: Coroutines, Flow, Compose recomposition, Hilt dependency injection, Jetpack patterns

Without explicit rules, Claude Code will produce code that compiles but violates your architecture — which is often worse than a compile error, because it’s harder to catch.

Tool Selection Matrix

Here’s how the four major tools compare across mobile stacks in 2026:

Claude CodeCursorCodex CLIAider
FlutterExcellent (Dart 3 aware, code-gen friendly)Good (autocompletion strong)Fair (no build_runner hooks)Good (git-native, diff-based)
React NativeExcellent (New Architecture aware)Excellent (JS/TS ecosystem strength)Good (npm script integration)Good
Native iOS (Swift)Very Good (Swift 6 concurrency aware)Excellent (Xcode context)FairFair (no Xcode project model)
Native Android (Kotlin)Very Good (Compose recomposition aware)Very GoodGoodGood

When to use which tool:

  • Claude Code: Best for architecture work, refactoring, writing CLAUDE.md / AGENTS.md rules, and tasks requiring deep context. The 1M context window is genuinely useful for large mobile codebases.
  • Cursor: Best for rapid iteration with autocomplete, inline edits, and when you’re working file-by-file in an editor-first workflow.
  • Codex CLI: Best for scripted, repeatable tasks — “generate a CRUD screen for this model” — and CI/CD pipeline integration.
  • Aider: Best for git-native workflows where you want changes as clean diffs. Excellent for code review prep.

Most experienced mobile developers end up using Claude Code and Cursor in combination: Cursor for the editor, Claude Code for architectural decisions and large-scale refactors.

Framework × Tool Compatibility Matrix

Before the per-framework sections, here’s a summary of what works, what requires configuration, and what to avoid:

Flutter

TaskClaude CodeCursorCodex CLIAider
Widget generationWorks wellWorks wellGoodGood
Riverpod providersNeeds rulesNeeds completion configStrugglesNeeds rules
build_runner workflowNeeds explicit rulesNeeds explicit rulesPoorNeeds explicit rules
Clean architecture routingWorks well with rulesWorks wellFairWorks well
Platform-specific codeNeeds rulesGoodFairGood

React Native

TaskClaude CodeCursorCodex CLIAider
Component generationExcellentExcellentGoodGood
New Architecture (JSI/TurboModules)GoodVery GoodFairFair
Navigation (React Navigation)Works wellWorks wellGoodGood
Native module bridgingNeeds heavy rulesNeeds heavy rulesPoorPoor
Expo vs bare workflowNeeds rulesGoodGoodGood

Native iOS (Swift/SwiftUI)

TaskClaude CodeCursorCodex CLIAider
SwiftUI view compositionVery GoodExcellentGoodGood
Swift 6 concurrencyNeeds rulesVery GoodFairFair
SwiftData / Core DataNeeds rulesGoodFairPoor
Package.swift dependenciesGoodGoodGoodGood
Combine vs async/awaitNeeds explicit rulesNeeds rulesPoorFair

Native Android (Kotlin/Compose)

TaskClaude CodeCursorCodex CLIAider
Composable functionsVery GoodExcellentGoodGood
Hilt DI wiringNeeds rulesVery GoodFairGood
ViewModel + Flow patternsWorks wellWorks wellGoodGood
Room databaseNeeds rulesGoodGoodGood
Gradle configurationNeeds rulesGoodGoodGood

Per-Framework Workflow Recipes

Flutter

Flutter’s combination of Dart’s type system and code generation creates a workflow with more friction points than other frameworks. The good news: once you’ve configured Claude Code correctly, it handles boilerplate at a level that’s hard to match.

The core setup problem with Flutter and AI tools:

Flutter projects use build_runner to generate code from annotations (@freezed, @riverpod, @RestApi, @JsonSerializable). AI tools don’t know to run build_runner after modifying annotated files, and they don’t know that .g.dart / .freezed.dart files are generated — they’ll try to edit them directly.

Your CLAUDE.md needs to address this explicitly. The following extends the Flutter guide for teams using multiple AI tools:

## Code Generation — AI Tool Rules

### What Requires build_runner
After modifying any file with these annotations, build_runner MUST run:
- @freezed (Freezed models)
- @riverpod (Riverpod providers, notifiers)
- @RestApi (Retrofit clients)
- @JsonSerializable (JSON models)
- @TypedGoRoute (go_router type-safe routes)

### Rules for AI Tools
- NEVER directly edit `.g.dart` or `.freezed.dart` files
- NEVER generate providers using `Provider()`, `StateNotifierProvider()`, or
  `FutureProvider()` directly — always use @riverpod annotation pattern
- After ANY change to annotated files, output this exact command:
  `dart run build_runner build --delete-conflicting-outputs`
- If you see a `.g.dart` file that looks wrong, tell the developer to run
  build_runner — do not attempt to fix it manually

### Cursor-Specific Rules
When using Cursor alongside this project:
- Set up a custom command: Ctrl+Shift+B → `dart run build_runner build --delete-conflicting-outputs`
- Do not accept autocomplete suggestions that import from `.g.dart` files by path
  (always import from the source file)

Flutter workflow with Claude Code (multi-session project):

For a feature that adds a new screen with data fetching:

# Task 1: Generate the domain layer
claude "Add a ProductReview domain entity and repository interface. Follow the 
existing User entity pattern in features/users/domain/. No implementation needed."

# Task 2: Generate the data layer  
claude "Implement the ProductReviewRepositoryImpl for the interface we just created. 
Use the UserRepositoryImpl pattern. Mock the remote data source for now."

# Task 3: Generate providers
claude "Add a Riverpod provider for ProductReview following the @riverpod annotation 
pattern in features/users/presentation/providers/. Add both a list provider and 
a single-item provider with the user ID parameter pattern."

# After Task 3, run build_runner:
dart run build_runner build --delete-conflicting-outputs

# Task 4: Generate the UI
claude "Create the ProductReviewListPage and ProductReviewCard widget. Use the 
AsyncValue.when pattern for the list provider. Follow UserListPage as reference."

Breaking tasks this way keeps each Claude Code session focused and prevents it from generating code across all four layers simultaneously — which tends to produce inconsistencies when it can’t see the build_runner output.

Cursor + Claude Code hybrid workflow for Flutter:

Cursor’s autocomplete is strong for Dart because it can see your entire project and learns your patterns. The effective split:

  • Cursor: Widget composition, completing boilerplate, in-file refactoring
  • Claude Code: Architecture decisions, cross-file refactoring, generating new features end-to-end, writing tests

The risk is that Cursor’s suggestions won’t know your CLAUDE.md rules. Add a .cursorrules file at project root that mirrors the critical rules:

# Flutter Project Rules — Cursor

## State Management
- Use ONLY Riverpod with @riverpod annotation (riverpod_generator)
- Never suggest setState for feature state
- Never suggest ChangeNotifier, ValueNotifier, GetX, or Bloc
- ref.watch() in build() — ref.read() in callbacks only

## Code Generation
- After modifying @freezed, @riverpod, @RestApi, or @JsonSerializable files,
  remind developer to run: dart run build_runner build --delete-conflicting-outputs
- Never edit .g.dart or .freezed.dart files directly

## Navigation
- Use go_router exclusively (context.go, context.push, context.pop)
- Never use Navigator.of(context).push directly

## Widget Patterns
- Prefer ConsumerWidget over StatelessWidget when Riverpod is involved
- const constructors everywhere possible
- SafeArea wraps all page root content

React Native

React Native’s AI tooling story is more complex than Flutter’s because the ecosystem is fractured: Expo vs. bare workflow, New Architecture vs. old architecture, and the ongoing migration from Bridge-based native modules to JSI TurboModules.

The context your AI tool needs:

## React Native Project Configuration

### Architecture
- React Native 0.74+ with New Architecture enabled (JSI, TurboModules, Fabric)
- Expo: [bare / managed] workflow
  (Delete whichever doesn't apply)
- JavaScript/TypeScript: TypeScript strict mode
- Navigation: React Navigation 7 (Stack, Tab, Drawer)
- State: [Zustand / Jotai / Redux Toolkit]
  (Delete whichever doesn't apply)
- Async: React Query (server state) + Zustand (client state)
- Styling: StyleSheet (or NativeWind if using Tailwind approach)
- Native modules: Expo Modules API (not the old NativeModules bridge)

### New Architecture Rules
- Never use the NativeModules bridge for new features — use TurboModules
- Do NOT use legacy NativeModules.* calls
- Use the Expo Modules API for custom native modules:
  https://docs.expo.dev/modules/module-api/
- If you see code using NativeModules.*, flag it as technical debt

### Platform-Specific Code
- Use Platform.OS === 'ios' and Platform.OS === 'android' (not 'ios' directly)
- Platform-specific files: ComponentName.ios.tsx / ComponentName.android.tsx
- Prefer cross-platform implementations; use platform splits only when necessary
- StyleSheet.create() — never inline styles in JSX

### Metro Bundler
- Do NOT use require() with dynamic expressions — Metro can't tree-shake them
- Avoid circular imports — Metro handles them poorly at scale
- Image assets: import statically, never with dynamic paths

React Navigation patterns — the pitfall-heavy area:

React Navigation 7 changed the typing significantly from v6. Without explicit rules, Claude Code often generates v6 patterns in a v7 project:

## Navigation Rules (React Navigation 7)

### Route Type Definitions
// Define types at the navigator level:
type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Settings: undefined;
};

// Type the navigation prop:
type ProfileScreenNavigationProp = NativeStackNavigationProp<
  RootStackParamList,
  'Profile'
>;

### Do NOT Use
- navigation.navigate('ScreenName') without TypeScript types
- useNavigation() without generic type parameter
- navigation.push() for tab navigators (use jumpTo())
- router.back() — use navigation.goBack()

### Nested Navigation
- Each nested navigator needs its own ParamList type
- Use CompositeNavigationProp for accessing parent navigator from child
- Do not pass navigation prop down through component props — use useNavigation()

Expo workflow specifics:

If your project uses Expo (managed or bare), add these rules to prevent common mistakes:

## Expo-Specific Rules

### EAS Build
- Expo config plugins, not native module files directly
- app.json / app.config.js — single source of truth for app config
- Do NOT manually edit android/ or ios/ directories in managed workflow
- New native dependencies require: npx expo install + EAS build

### SDK Version
- Always use packages compatible with the current Expo SDK version
- Check compatibility at: https://docs.expo.dev/versions/latest/
- Do NOT install packages from npm without checking Expo SDK compatibility

### Environment Variables
- Use .env with EXPO_PUBLIC_ prefix for client-side vars
- Server-only secrets: EAS Secrets (not in .env committed to git)
- Access: process.env.EXPO_PUBLIC_API_URL (not process.env.API_URL)

Aider for React Native:

Aider works well for React Native because of its git-native diff approach. The typical flow:

# Initialize Aider with your key files in context
aider --read CLAUDE.md src/navigation/types.ts src/stores/

# Generate a new screen
aider "Add a NotificationsScreen component. It should:
- Use the Zustand store pattern from src/stores/user.ts
- Fetch with React Query using the pattern in src/hooks/useProfile.ts
- Handle loading/error states like ProfileScreen.tsx"

# Review the diff before committing
git diff --staged

Native iOS (Swift / SwiftUI)

Swift/SwiftUI has the most dynamic AI tooling story of the four frameworks. Swift 6’s strict concurrency checking, SwiftData replacing Core Data for new projects, and the maturation of Swift Concurrency over Combine all mean that AI tools trained on older Swift codebases will suggest deprecated patterns.

The key version context your AI tool needs:

## iOS Project Configuration

### Swift Version and Concurrency
- Swift 6 with strict concurrency checking enabled
- Swift Concurrency (async/await, actors, structured concurrency) — primary model
- Combine: used ONLY in existing code, do NOT introduce new Combine usage
- All new async code uses async/await and actors

### Data Persistence
- New models: SwiftData (@Model, @Query, ModelContext)
- Legacy models: Core Data (existing only, do not add new entities)
- Do NOT introduce Core Data for new features — use SwiftData

### Architecture
- [MVVM / MV / TCA]
  (Delete whichever doesn't apply)
- ObservationFramework (@Observable) — not ObservableObject/@Published for new code
- Navigation: NavigationStack with NavigationPath

### Minimum Deployment Target
- iOS 17.0 (enables SwiftData, @Observable, NavigationStack)
- Do NOT use APIs introduced after iOS 17.0 without availability checks
- Always add @available(iOS X.0, *) for newer API usage

Swift 6 concurrency — where AI tools struggle most:

Swift 6’s strict concurrency produces compile errors that look unfamiliar to AI tools trained primarily on Swift 5 code. The most common mistakes:

## Swift 6 Concurrency Rules

### Actor Isolation
- @MainActor for all UI-related types (ViewModels, ObservableObjects)
- Non-UI services: use actors for shared mutable state
- Do NOT use DispatchQueue.main.async — use await MainActor.run { } or mark type @MainActor

### Sendable
- Types crossing actor boundaries must be Sendable
- Value types (struct, enum) are Sendable by default
- Classes require explicit Sendable conformance or @unchecked Sendable (with comment explaining why)
- Do NOT suppress concurrency warnings with @unchecked Sendable without a comment

### Task Management
- Store Task handles to cancel them (in deinit or onDisappear)
- Do NOT create detached Tasks without a reason — use structured concurrency
- withTaskGroup for parallel work, async let for a fixed number of parallel tasks
- Do NOT use Task.sleep in production code — use proper async sequencing

### Do NOT Use (Swift 5 patterns)
- DispatchQueue.main.async { } → use @MainActor or await MainActor.run
- completion handlers for new async work → use async/await
- NotificationCenter with #selector → use async streams or Observation
- ObservableObject / @Published for new ViewModels → use @Observable

SwiftData patterns:

## SwiftData Rules

### Model Definition
// Good:
@Model
final class Task {
    var title: String
    var isCompleted: Bool
    var createdAt: Date
    
    init(title: String) {
        self.title = title
        self.isCompleted = false
        self.createdAt = .now
    }
}

// Bad — do NOT use Core Data patterns for new models:
// NSManagedObject, @NSManaged, etc.

### Query Usage
// In SwiftUI views:
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]

// With predicate:
@Query(filter: #Predicate<Task> { !$0.isCompleted }) private var pendingTasks: [Task]

### ModelContext Operations
// Insert:
modelContext.insert(Task(title: "New task"))

// Delete:
modelContext.delete(task)

// Save is automatic — do NOT call modelContext.save() manually in most cases
// Exception: before background operations

### Do NOT
- Mix Core Data and SwiftData in the same model graph
- Use modelContext across actor boundaries without passing it explicitly
- Fetch directly from ModelContainer.mainContext in background code

Xcode context for Claude Code:

Claude Code doesn’t have native Xcode project file awareness, which means it won’t update .xcodeproj when adding new files. Include this in your CLAUDE.md:

## Xcode Project Rules

### Adding Files
When adding new Swift files, I will:
1. Create the file in the correct group directory
2. Remind you to drag it into Xcode's project navigator (or use Xcode's "Add Files")
3. Confirm which target(s) the file belongs to

If using Swift Package Manager (Package.swift instead of .xcodeproj), 
I will add files to the correct target in Package.swift automatically.

### Testing
- Unit tests: MyAppTests target
- UI tests: MyAppUITests target  
- Snapshot tests (if SnapshotTesting is installed): MyAppTests target, Snapshots/ group

Native Android (Kotlin / Compose)

Kotlin/Compose has become the default for new Android development, but the ecosystem still carries significant Gradle complexity and Jetpack patterns that AI tools frequently misconfigure.

The configuration context:

## Android Project Configuration

### Language and Version
- Kotlin 2.x with K2 compiler
- Jetpack Compose (UI layer — no XML layouts for new features)
- Minimum SDK: 24 / Target SDK: 35
- Compose BOM: [current version — check libs.versions.toml]

### Architecture
- MVVM + MVI (unidirectional data flow)
- ViewModel (Jetpack) + StateFlow + sealed class for UI state
- Repository pattern: ViewModel → Repository → DataSource
- Dependency injection: Hilt (not Koin, not manual DI)
- Navigation: Compose Navigation (not Fragment navigation)

### Async
- Coroutines + Flow (primary model)
- No RxJava for new code
- viewModelScope for ViewModel coroutines
- lifecycleScope in Activity/Fragment (rare — prefer ViewModel)

### Build System
- Gradle with version catalog (libs.versions.toml)
- KSP (not KAPT) for annotation processing
- Compose compiler plugin: [version in libs.versions.toml]

Compose recomposition — where AI tools create subtle bugs:

Compose’s declarative model requires understanding of recomposition to write efficient UIs. Claude Code often produces code that triggers unnecessary recomposition:

## Compose Performance Rules

### State Hoisting
- State lives in the ViewModel or the highest Composable that needs it
- Stateless Composables receive state and callbacks — do NOT read from ViewModel inside leaf Composables
- Pattern: Screen → ViewModel → UIState. Screen passes UIState down to components.

### Avoiding Unnecessary Recomposition
// Bad — reading from ViewModel directly in a component:
@Composable
fun UserName(viewModel: UserViewModel = hiltViewModel()) {
    Text(text = viewModel.userName) // recomposes on ANY ViewModel state change
}

// Good — receive only what's needed:
@Composable
fun UserName(name: String) {
    Text(text = name) // recomposes only when name changes
}

### Lambda Stability
// Bad — new lambda on every recomposition:
Button(onClick = { viewModel.onSubmit(input) }) { ... }

// Good — stable lambda reference:
val onSubmit = remember { { viewModel.onSubmit(input) } }
Button(onClick = onSubmit) { ... }

### Keys in LazyColumn
// Always provide stable keys:
LazyColumn {
    items(items = users, key = { it.id }) { user ->
        UserItem(user = user)
    }
}

### Derivation
- Use remember { derivedStateOf { ... } } for computed state — avoid computing in recomposition
- Do NOT perform expensive calculations directly in Composable scope

Hilt dependency injection — the annotation maze:

Hilt has more annotations than any other Android library, and AI tools frequently confuse them:

## Hilt Rules

### Component Annotations
- @HiltAndroidApp — Application class (one per project)
- @AndroidEntryPoint — Activity, Fragment, View, Service (NOT ViewModel)
- @HiltViewModel — ViewModel
- @Inject — constructor injection (preferred over field injection)
- @Provides — in @Module, for types you don't own
- @Binds — in @Module @InstallIn, for interface binding

### Do NOT
- Use @AndroidEntryPoint on ViewModel — use @HiltViewModel
- Use @Inject for field injection in ViewModel — use constructor injection
- Create @Module without @InstallIn
- Use @ActivityScoped when @ViewModelScoped is more appropriate

### Module Structure
@Module
@InstallIn(SingletonComponent::class) // or ActivityComponent, ViewModelComponent
abstract class UserModule {
    @Binds
    abstract fun bindUserRepository(
        impl: UserRepositoryImpl
    ): UserRepository
}

// For external types (Retrofit, Room):
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.API_BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
}

Room database — KSP and type converter patterns:

## Room Database Rules

### KSP (not KAPT)
- Use KSP processor (ksp("androidx.room:room-compiler:..."))
- Do NOT use kapt for Room — KSP is faster and officially preferred

### Entities
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    @ColumnInfo(name = "display_name") val displayName: String,
    val email: String,
    @ColumnInfo(name = "created_at") val createdAt: Long // Unix timestamp
)

### Type Converters
// Declare globally, not per-entity:
class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
    
    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time
}

// Register in @Database annotation:
@Database(entities = [UserEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

### DAOs
- suspend functions for one-shot operations
- Flow<T> return type for observable queries (automatically updates on data change)
- Do NOT use LiveData for new DAOs — use Flow
- Use @Transaction for operations spanning multiple tables

ViewModel + Flow pattern — the canonical Android 2026 pattern:

## ViewModel + Flow Pattern

### UI State
sealed interface UserDetailUiState {
    object Loading : UserDetailUiState
    data class Success(val user: User) : UserDetailUiState
    data class Error(val message: String) : UserDetailUiState
}

### ViewModel Structure
@HiltViewModel
class UserDetailViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository
) : ViewModel() {
    
    private val userId: String = checkNotNull(savedStateHandle["userId"])
    
    val uiState: StateFlow<UserDetailUiState> = userRepository
        .observeUser(userId)
        .map { user -> UserDetailUiState.Success(user) }
        .catch { e -> emit(UserDetailUiState.Error(e.message ?: "Unknown error")) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = UserDetailUiState.Loading
        )
    
    fun retry() {
        // Trigger via separate StateFlow or SharedFlow
    }
}

### Collecting in Composable
@Composable
fun UserDetailScreen(viewModel: UserDetailViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // ...
}

// Always use collectAsStateWithLifecycle (not collectAsState) — 
// it respects lifecycle and stops collection in background

CLAUDE.md / AGENTS.md Templates Per Framework

Ready-to-use templates that cover the configuration above in a copy-paste format.

Flutter — Minimal CLAUDE.md

# Flutter Project — AI Rules

## Stack
- Flutter 3.x / Dart 3.x / Null Safety
- State: Riverpod (riverpod_annotation, riverpod_generator)
- Architecture: Feature-first clean architecture
- Navigation: go_router
- HTTP: Dio + Retrofit (code generated)
- Models: Freezed + json_serializable
- Build: build_runner (KSP equivalent for Dart)

## Non-Negotiable Rules
1. State management: ONLY @riverpod annotation pattern. Never: setState (for feature state), ChangeNotifier, GetX, Bloc, Provider package
2. Navigation: ONLY go_router. Never: Navigator.of(context).push, MaterialPageRoute directly
3. After ANY change to @freezed, @riverpod, @RestApi, @JsonSerializable, @TypedGoRoute annotated files → output build_runner command
4. Never edit .g.dart or .freezed.dart files directly
5. ref.watch in build(), ref.read in callbacks — never swap these

## Build Runner Command
dart run build_runner build --delete-conflicting-outputs

## Project Structure
lib/
├── core/           # shared infrastructure
├── features/       # feature-first (data/domain/presentation per feature)
└── shared/         # truly cross-feature widgets

## Testing
- Widget tests: ProviderScope overrides for Riverpod, not mocking entire ViewModel
- Unit tests: test use cases, repositories, utilities
- Integration tests: integration_test/ package
- Never test: .g.dart files, generated code, Flutter framework internals

React Native — Minimal CLAUDE.md

# React Native Project — AI Rules

## Stack
- React Native 0.74+ / New Architecture (JSI enabled)
- Language: TypeScript (strict: true)
- Navigation: React Navigation 7
- State: Zustand (client) + React Query (server)
- Styling: StyleSheet (no inline styles)
- Native modules: Expo Modules API
- [Expo managed / Expo bare / Bare RN]

## Non-Negotiable Rules
1. Never use NativeModules bridge for new features — use TurboModules or Expo Modules API
2. All components in TypeScript with explicit prop types (no `any`)
3. Navigation with proper TypeScript paramList types — no untyped navigate()
4. Platform-specific code: Platform.OS checks or .ios.tsx / .android.tsx files
5. No inline styles — always StyleSheet.create()
6. No dynamic require() — Metro can't optimize it

## New Architecture
- JSI is default — assume New Architecture is enabled
- Avoid libraries that haven't been updated for New Architecture
- Fabric renderer — avoid refs that assumed the old renderer behavior

## Banned Patterns
- NativeModules.* (use TurboModules)
- React.FC generic (use explicit function signature)
- useState for server data (use React Query)
- useEffect for data fetching (use React Query)

Native iOS — Minimal CLAUDE.md

# iOS Project — AI Rules

## Stack
- Swift 6 / SwiftUI
- Minimum deployment: iOS 17.0
- Architecture: MVVM with @Observable
- Data: SwiftData
- Async: Swift Concurrency (async/await, actors)
- No Combine for new code
- No UIKit for new features

## Non-Negotiable Rules
1. Swift 6 strict concurrency — all Sendable and actor isolation errors must be fixed, not suppressed
2. @MainActor on all ViewModel types and UI-updating code
3. New persistence: SwiftData only. No new Core Data entities
4. New reactive code: @Observable. No new ObservableObject / @Published
5. New async code: async/await. No new completion handlers or DispatchQueue.main.async
6. NavigationStack with typed NavigationPath — no NavigationLink(destination:)

## Adding Files
Remind developer to add new .swift files to the Xcode project navigator.
Claude Code cannot update .xcodeproj directly.

## Testing
- Unit tests: XCTest (MyAppTests target)
- UI tests: XCUITest (MyAppUITests target)
- Snapshot tests: SnapshotTesting library (if installed)
- @MainActor for async test methods that interact with MainActor-isolated code

Native Android — Minimal CLAUDE.md

# Android Project — AI Rules

## Stack
- Kotlin 2.x / K2 compiler
- Compose UI (no XML layouts for new features)
- Architecture: MVVM + unidirectional data flow
- DI: Hilt
- Async: Coroutines + Flow (no RxJava)
- Navigation: Compose Navigation
- Database: Room with KSP (not KAPT)
- HTTP: Retrofit + OkHttp

## Non-Negotiable Rules
1. Hilt for all DI — no manual injection, no Koin
2. KSP for annotation processing (Room, Hilt) — NEVER kapt
3. UI state as sealed interface/class — not Boolean flags
4. collectAsStateWithLifecycle (not collectAsState) in Composables
5. StateFlow for ViewModel → UI, SharedFlow for one-shot events
6. No RxJava for new code
7. Composable functions: stateless components receive state + callbacks only

## Compose Performance
- State hoisting: state in ViewModel, not in leaf components
- Stable keys in LazyColumn/LazyRow items
- remember { derivedStateOf } for computed state
- rememberUpdatedState for callbacks in launched effects

## Gradle
- Version catalog: libs.versions.toml (single source of truth for versions)
- Never hardcode dependency versions in build.gradle.kts files
- KSP processor, not KAPT

Multi-Framework AGENTS.md (for AI agent workflows)

If you’re running AI agent tasks (Codex CLI, multi-agent Claude Code) across a codebase that contains multiple targets or frameworks:

# AGENTS.md

## Project Structure
This repository contains:
- /mobile/flutter/ — Flutter mobile app
- /mobile/rn/ — React Native web companion  
- /backend/ — Node.js API

## Scope Rules for Agents
- Mobile tasks are isolated: changes to /mobile/flutter/ must not touch /mobile/rn/
- Do not modify /backend/ unless the task explicitly includes API changes
- Cross-platform changes (shared types, API contracts): modify /shared/ only,
  then apply to each platform separately

## Task Decomposition
For tasks spanning multiple platforms, decompose into:
1. Shared contract changes (/shared/)
2. Flutter implementation
3. React Native implementation
Each step should be independently testable.

## Build Commands Per Platform
- Flutter: `cd mobile/flutter && flutter build apk --debug`
- React Native: `cd mobile/rn && npx react-native run-android`
- Backend: `cd backend && npm test`
- All: use the Makefile targets (make test-all, make build-all)

## Never
- Run platform-specific build commands from the wrong directory
- Mix imports across platforms
- Modify package.json from a Flutter task or pubspec.yaml from an RN task

Real-World Example Applications

These open-source apps demonstrate the patterns above in practice — useful for showing Claude Code what “correct” looks like in your codebase context.

Flutter

flutter_shopping_app (VGV) Very Good Ventures’ example app. Clean architecture, Riverpod, go_router. Production-grade patterns. Good reference when setting up CLAUDE.md rules for a new project — run Claude Code against it to see how it behaves before applying rules.

holobooth Official Flutter team example. Demonstrates camera/AR integration patterns and platform-specific code that requires careful AI tool rules around platform channel usage.

React Native

react-native-template-typescript The de facto starting point. Low-opinionated but shows the New Architecture setup correctly.

Rainbow Ethereum wallet app. Excellent example of complex navigation, native module integration (biometrics, camera), and TypeScript discipline. Shows how to structure CLAUDE.md rules for apps with significant native code.

Native iOS

TCA (The Composable Architecture) examples If you’re using TCA, these examples show the canonical patterns. Useful for establishing rules around TCA’s specific conventions (Reducers, Stores, ViewStores) — which AI tools frequently conflate with MVVM patterns.

Scrumdinger Apple’s official SwiftUI tutorial app. SwiftData and Swift Concurrency. Not production-scale, but correct patterns for iOS 17.

Native Android

Now in Android Google’s reference app. Hilt, Compose, KSP, Version Catalog — everything you want as a reference for setting up Android CLAUDE.md rules. Run claude "Review CLAUDE.md against the Now in Android codebase patterns" as a calibration step.

Jetpack Compose Samples JetSnack, Owl, Rally — each demonstrates different Compose UI patterns. Good source of concrete examples to paste into your CLAUDE.md as “patterns to follow.”

Pitfalls + FAQ

Why does Claude Code keep generating the wrong state management patterns?

Because state management has multiple valid approaches in every mobile framework, and without explicit prohibition, Claude uses whatever pattern fits the immediate context. The fix is always explicit rules:

## State Management Policy
This project uses ONLY [Riverpod / Zustand + React Query / @Observable / StateFlow].
NEVER introduce: [list of banned patterns].
If you think you need an exception, ask before implementing.

The prohibition list is as important as the positive rule.


Cursor generates code that violates my CLAUDE.md rules — why?

Cursor doesn’t read CLAUDE.md by default. You need either:

  1. A .cursorrules file at project root (Cursor reads this automatically)
  2. Rules added to Cursor’s project context via the Cursor settings panel

The .cursorrules file should mirror the most critical rules from CLAUDE.md — particularly state management, navigation, and code generation rules. It doesn’t need to duplicate everything.


How do I handle projects migrating from an old architecture to a new one?

Migration is one of the hardest scenarios for AI tools because the codebase contains both patterns. The rule structure changes:

## Migration Context

We are ACTIVELY MIGRATING from [old pattern] to [new pattern].

### New Code Rules (STRICT)
All new files: [new pattern rules]

### Legacy Code Rules
When touching existing files in [legacy paths], you may use existing patterns
to minimize diff size. Do NOT refactor legacy code unless the task specifically
asks for it.

### Migration Progress
- Completed: [list of migrated modules]
- In progress: [current module]
- Not yet started: [remaining modules]

### Never
- Mix patterns within a single file
- Introduce the old pattern in new files even if a nearby file uses it

Codex CLI for mobile — what’s it actually good for?

Codex CLI’s strength in mobile is scripted, repeatable generation — not architecture. The use cases where it adds value:

  • Generating CRUD screens from a model definition (when you have a template the CLI can follow)
  • Running analysis tasks (“find all places where we use NativeModules bridge and list them”)
  • CI/CD integration: codex "Update all version strings from 1.2.3 to 1.3.0" in a deploy pipeline

For architecture-level work, real refactors, and anything requiring understanding of how multiple files interact, Claude Code’s context window and reasoning are significantly better.


Why does Swift 6 concurrency break so many AI suggestions?

Swift 6’s strict concurrency checking catches data races at compile time. AI tools trained on pre-Swift 6 code generate patterns that worked before but now fail — primarily:

  1. DispatchQueue.main.async instead of @MainActor
  2. Sending non-Sendable types across actor boundaries
  3. Calling async functions from synchronous contexts without proper Task wrapping
  4. Mutating shared state from multiple contexts

The solution: include the Swift 6 concurrency rules section from this guide in your CLAUDE.md, and add a specific rule: “All concurrency warnings are errors. Do not suppress them with @unchecked Sendable or nonisolated(unsafe) without a comment explaining exactly why it’s safe.”


What’s the right level of detail for mobile CLAUDE.md files?

There’s a real cost to over-specification: very long CLAUDE.md files slow Claude Code down and make it harder to update rules when patterns evolve.

The pragmatic guideline:

  • Architecture and state management: Always specify explicitly
  • Naming conventions: Specify the non-obvious ones (Dart snake_case is obvious; _page.dart suffix convention is not)
  • Code generation workflows: Always specify (build_runner, KSP, Hilt processors)
  • Banned patterns: Always list explicitly — positive rules alone aren’t enough
  • Widget/Composable decomposition thresholds: Specify if your team debates this
  • Testing levels: Specify which types of tests belong where

Start with the minimal templates above, add rules when you see Claude Code make a specific wrong decision, and trim rules that never get triggered.


Aider vs. Claude Code for mobile — when does Aider win?

Aider’s git-native workflow has two advantages over Claude Code for mobile:

  1. Diff review before apply: Aider shows you the full diff before committing. Claude Code applies changes directly. For iOS Swift where Xcode project file changes can’t be tracked automatically, Aider’s review step is a useful safety check.

  2. Multi-file edits without session memory: Aider can edit multiple files in a single command and the changes are immediately git-tracked. Useful for rename refactors that touch many files.

Claude Code wins on reasoning quality and context size. For a migration task touching 50+ files, Claude Code’s 1M context window and ability to understand cross-file implications is clearly better than Aider’s approach.


The framework-specific patterns in this guide change more slowly than the tool versions themselves. Revisit the tool compatibility matrix section when major framework versions ship — particularly when React Native’s New Architecture stabilizes, when Compose multiplatform matures, and when Swift 6 adoption becomes universal. The CLAUDE.md templates are stable; the tool recommendations are a 2026 snapshot.

Browse framework-specific rules from real projects in our gallery, or explore the Flutter guide for a deeper dive into the Dart/Riverpod stack specifically.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules