惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 聂微东
MyScale Blog
MyScale Blog
H
Help Net Security
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Proofpoint News Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
CLAUDE.md for Android and Jetpack Compose: 13 Rules That ...
Olivia Craft · 2026-05-20 · via DEV Community

CLAUDE.md for Android and Jetpack Compose: 13 Rules That Make AI Write Modern, Production-Ready Android Code

Android development has two eras of AI output quality.

The first era: AI that reaches for XML layouts, AsyncTask, SharedPreferences, and findViewById. Code that compiles on Android 14 but was written for Android 4. Code that passes lint but violates every modern architecture guideline.

The second era: AI that defaults to Compose, coroutines, ViewModel, StateFlow, Hilt, and the patterns from the Now in Android reference project.

The difference between these outcomes is a CLAUDE.md that specifies which Android you're building.

These 13 rules cover the patterns that matter most — the ones where AI drifts to legacy APIs without explicit instruction.


Rule 1: Jetpack Compose only — no XML layouts

UI: Jetpack Compose exclusively. No XML layouts, no View system.
Forbidden: ConstraintLayout XML, LinearLayout, RelativeLayout, findViewById.
Composables: stateless where possible. State hoisted to ViewModel.
Preview: @Preview annotation on every composable component.
Material3 only — not Material2.

Enter fullscreen mode Exit fullscreen mode

AI has seen enormous volumes of XML-based Android code. Without a rule, it will generate setContentView(R.layout.activity_main) and ViewBinding for new screens in a Compose project.

// Banned — XML/View system
class UserFragment : Fragment() {
    private lateinit var binding: FragmentUserBinding
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.nameText.text = viewModel.userName
    }
}

// Required — Jetpack Compose
@Composable
fun UserScreen(
    uiState: UserUiState,
    onAction: (UserAction) -> Unit
) {
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text(text = uiState.userName, style = MaterialTheme.typography.headlineMedium)
    }
}

Enter fullscreen mode Exit fullscreen mode


Rule 2: ViewModel + UiState — no logic in Composables

Architecture:
- ViewModel: holds UI state, handles user actions, calls use cases.
- UiState: sealed class or data class representing screen state.
- Composables: render UiState. No business logic. No direct repository calls.
- Events: one-time events (navigation, toasts) via Channel<UiEvent>.
- StateFlow<UiState> exposed from ViewModel. Collected with collectAsStateWithLifecycle().

Enter fullscreen mode Exit fullscreen mode

AI generates Composables that call repositories directly, fetch data in LaunchedEffect without a ViewModel, and hold mutable state locally for things that should survive rotation.

// Wrong — business logic and state in Composable
@Composable
fun UserScreen(repository: UserRepository) {
    var userName by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        userName = repository.getUser().name  // Direct repo call, no ViewModel
    }
    Text(text = userName)
}

// Right — ViewModel owns state
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    init { loadUser() }

    private fun loadUser() = viewModelScope.launch {
        _uiState.value = getUserUseCase().fold(
            onSuccess = { UserUiState.Content(it) },
            onFailure = { UserUiState.Error(it.message) }
        )
    }
}

Enter fullscreen mode Exit fullscreen mode


Rule 3: Coroutines and Flow — no callbacks, no RxJava

Async: Kotlin coroutines everywhere. No callbacks. No RxJava. No AsyncTask.
Repositories: return Flow<T> for streams, suspend fun for one-shot operations.
Dispatchers: IO for network/disk, Default for CPU-intensive. Never Main for background work.
viewModelScope: use for ViewModel coroutines. Never GlobalScope.
Cancellation: coroutines are cooperative — check cancellation in long loops.

Enter fullscreen mode Exit fullscreen mode

AI generates callback-style code and sometimes RxJava (from older training data). Both are banned in modern Android.

// Banned — callback style
fun getUser(id: String, callback: (User) -> Unit) {
    thread { callback(api.getUser(id)) }  // No cancellation, no error handling
}

// Required — coroutines
suspend fun getUser(id: String): Result<User> = withContext(Dispatchers.IO) {
    runCatching { api.getUser(id) }
}

// Repository stream
fun observeMessages(): Flow<List<Message>> = messageDao.observeAll()
    .flowOn(Dispatchers.IO)
    .catch { emit(emptyList()) }

Enter fullscreen mode Exit fullscreen mode


Rule 4: Hilt for dependency injection — no manual DI, no Koin

DI: Hilt exclusively.
Modules: @InstallIn with appropriate component scope.
ViewModel: @HiltViewModel + @Inject constructor.
Application: @HiltAndroidApp on Application class.
No: manual object graphs, service locators, companion object getInstance().

Enter fullscreen mode Exit fullscreen mode

AI generates singleton patterns and manual DI because they're simpler to write inline. Hilt makes dependencies explicit, scoped, and testable.

// Banned — manual singleton
object NetworkClient {
    val api: ApiService by lazy { Retrofit.Builder()... }
}

// Required — Hilt module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides @Singleton
    fun provideApiService(client: OkHttpClient): ApiService =
        Retrofit.Builder().client(client).baseUrl(BASE_URL).build()
            .create(ApiService::class.java)
}

Enter fullscreen mode Exit fullscreen mode


Rule 5: Repository pattern — data layer is abstract

Data layer:
- Repository interface in domain layer. Implementation in data layer.
- Repositories return domain models — never network DTOs or Room entities.
- Mapping: explicit mappers between layers (DTO → Domain, Entity → Domain).
- Local-first: Room as source of truth. Network syncs to Room. UI observes Room.
- No direct Retrofit calls from ViewModel or use cases.

Enter fullscreen mode Exit fullscreen mode

// Domain layer — interface
interface UserRepository {
    fun observeUser(id: String): Flow<User>
    suspend fun syncUser(id: String): Result<Unit>
}

// Data layer — implementation
class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao,
    private val mapper: UserMapper
) : UserRepository {
    override fun observeUser(id: String): Flow<User> =
        dao.observeById(id).map(mapper::toDomain)

    override suspend fun syncUser(id: String): Result<Unit> = runCatching {
        val dto = api.getUser(id)
        dao.upsert(mapper.toEntity(dto))
    }
}

Enter fullscreen mode Exit fullscreen mode


Rule 6: Room for local persistence — no SharedPreferences for structured data

Local data:
- Room for all structured data. No raw SQLite.
- SharedPreferences / DataStore: only for simple key-value preferences (theme, onboarding flag).
- DataStore (Proto or Preferences): replace SharedPreferences for new code.
- Room DAOs: return Flow<T> for observed queries, suspend fun for mutations.
- Migrations: explicit RoomDatabase.Migration — never destructive migration in production.

Enter fullscreen mode Exit fullscreen mode

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE id = :id")
    fun observeById(id: String): Flow<UserEntity?>

    @Upsert
    suspend fun upsert(user: UserEntity)

    @Query("DELETE FROM users WHERE id = :id")
    suspend fun deleteById(id: String)
}

Enter fullscreen mode Exit fullscreen mode


Rule 7: Navigation — Jetpack Navigation Compose with typed routes

Navigation:
- Jetpack Navigation Compose with type-safe routes (Navigation 2.8+).
- Routes: sealed class or @Serializable data class — no string literals.
- Single NavHost per feature graph. Nested graphs for feature modules.
- Navigate from ViewModel via UiEvent channel — not directly from Composable.
- Deep links: declared in NavGraph, not hardcoded in AndroidManifest.

Enter fullscreen mode Exit fullscreen mode


Rule 8: State management — immutable UiState, unidirectional data flow

State rules:
- UiState: immutable data class. Use copy() for updates.
- MutableStateFlow inside ViewModel, exposed as StateFlow (read-only).
- No mutableStateOf in ViewModel — use MutableStateFlow.
- Composables: collect with collectAsStateWithLifecycle() (lifecycle-aware).
- No shared mutable state between ViewModels.

Enter fullscreen mode Exit fullscreen mode

data class SearchUiState(
    val query: String = "",
    val results: List<SearchResult> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// In ViewModel
_uiState.update { it.copy(isLoading = true) }

Enter fullscreen mode Exit fullscreen mode


Rule 9: Testing — ViewModel unit tests, Composable screenshot tests

Testing:
- ViewModel: JUnit 5 + MockK + Turbine (for Flow testing). No Android dependencies.
- Repository: JUnit 5 + MockK for unit tests. Room in-memory DB for integration.
- Composables: Compose UI test (composeTestRule) or Paparazzi for screenshot tests.
- No Robolectric for new tests — prefer pure JVM or instrumented tests.
- coroutinesTestRule: StandardTestDispatcher for deterministic coroutine tests.

Enter fullscreen mode Exit fullscreen mode

@Test
fun `search query updates state`() = runTest {
    val viewModel = SearchViewModel(mockRepository)
    viewModel.uiState.test {
        assertThat(awaitItem().query).isEmpty()
        viewModel.onQueryChanged("kotlin")
        assertThat(awaitItem().query).isEqualTo("kotlin")
    }
}

Enter fullscreen mode Exit fullscreen mode


Rule 10: Permissions — request at point of use, handle denial gracefully

Permissions:
- Request via rememberLauncherForActivityResult — never startActivityForResult.
- Check permission before use: ContextCompat.checkSelfPermission.
- Handle denial: show rationale UI, offer settings redirect after permanent denial.
- Never crash or silently fail on permission denial.
- Dangerous permissions: request only when the user triggers the action.

Enter fullscreen mode Exit fullscreen mode


Rule 11: API communication — Retrofit + OkHttp + kotlinx.serialization

Network:
- Retrofit for REST. OkHttp with logging interceptor (debug builds only).
- Serialization: kotlinx.serialization. No Gson, no Moshi.
- Response wrapper: Result<T> or sealed class — never nullable responses.
- Timeouts: explicit connect (10s), read (30s), write (30s) timeouts.
- Certificate pinning for production builds.

Enter fullscreen mode Exit fullscreen mode


Rule 12: Build config — Gradle Kotlin DSL, version catalog

Build:
- Gradle Kotlin DSL (.kts) only — no Groovy.
- Version catalog (libs.versions.toml) for all dependencies.
- Build variants: debug (logging, mock data) / release (ProGuard, no logging).
- No hardcoded API keys or secrets in BuildConfig — use local.properties + secrets-gradle-plugin.
- Baseline profiles for startup performance.

Enter fullscreen mode Exit fullscreen mode


Rule 13: The CLAUDE.md block for Android

## Android Standards

**Min SDK:** 26 | **Target SDK:** 35 | **Language:** Kotlin 2.x
**UI:** Jetpack Compose + Material3 only (no XML layouts)
**Architecture:** MVVM + Clean Architecture (presentation / domain / data)

### UI Layer
- Composables: stateless. State hoisted to ViewModel.
- UiState: immutable data class, exposed as StateFlow<UiState>
- Collect with collectAsStateWithLifecycle() — not collectAsState()
- One-time events: Channel<UiEvent> in ViewModel

### Data Layer
- Repository pattern: interface in domain, impl in data
- Room as source of truth. Network syncs to Room.
- Coroutines: suspend fun for one-shot, Flow for streams
- No callbacks, no RxJava, no AsyncTask

### DI
- Hilt exclusively. @HiltViewModel on all ViewModels.
- No manual DI, no Koin, no service locators.

### Testing
- ViewModel: JUnit 5 + MockK + Turbine
- No Robolectric. Pure JVM unit tests preferred.

### Build
- Gradle Kotlin DSL + version catalog (libs.versions.toml)
- No secrets in source code or BuildConfig

Enter fullscreen mode Exit fullscreen mode


Why Android specifically needs this

Android's API surface has been evolving continuously for 15 years. AI models have absorbed code from every era — from AsyncTask through RxJava to coroutines, from XML inflation through DataBinding to Compose.

Without explicit rules, the model defaults to the mean of that training distribution. That mean includes a lot of code that works on current Android but violates current best practices, creates maintenance debt, and will break when APIs are deprecated.

The rules above aren't a style guide. They're the line between code that a senior Android engineer would merge and code that gets sent back for a full rewrite.

The CLAUDE.md block at the end is what you add to your repo. The AI reads it at the start of every session, and every Composable, ViewModel, and repository it generates starts from those constraints rather than from the average of all Android code ever written.