Mobile development is where AI tools face their toughest test. Android has complex APIs, Jetpack Compose changes fast, and platform-specific patterns do not always match what AI learned from web development tutorials.
In this article, you will build a complete notes app with Kotlin and Jetpack Compose. You will use Claude Code as the primary tool (running alongside Android Studio) and Copilot for inline completions. Every prompt, every AI mistake, and every manual fix is shown.
The project: QuickNotes — a simple notes app with offline storage using Room database, search, and categories. Small enough to build in one session, complex enough to expose real mobile-specific AI issues.
Step 1: Describe the App Architecture
Start with architecture, not screens. Mobile apps have lifecycle concerns, dependency injection, and platform-specific patterns that AI needs to understand upfront.
Prompt to Claude Code:
I want to build an Android notes app called "QuickNotes"
using Kotlin and Jetpack Compose.
Architecture:
- MVVM with clean architecture layers
- UI layer: Jetpack Compose screens + ViewModels
- Domain layer: use cases (optional for this size)
- Data layer: Room database + Repository pattern
- Dependency injection: Hilt
Screens:
1. Notes list — shows all notes, search bar at top,
FAB to add new note
2. Note editor — title, content, category picker,
save and delete buttons
3. Categories — simple list to add/rename/delete categories
Features:
- Offline-first (Room database, no network)
- Search notes by title and content
- Filter notes by category
- Sort by date created or date modified
- Swipe to delete with undo snackbar
Tech stack:
- Kotlin 2.1
- Jetpack Compose with Material 3
- Room for local storage
- Hilt for dependency injection
- Compose Navigation
- Kotlin Coroutines + Flow
Write the architecture document with database schema,
screen layouts, and navigation graph. Do not write code yet.
Claude Code generated a solid architecture document. The database schema, navigation graph, and layer separation were all correct.
What I changed:
- Removed the domain layer. For an app this size, use cases add unnecessary abstraction. The ViewModels call the repository directly.
- Changed the category from a separate table to a simple string field on the Note entity. A full Category table with foreign keys is overkill for a notes app with 5-10 categories.
Step 2: Scaffold the Project
Prompt:
Read the architecture document. Create the project structure.
Set up:
- Hilt application class and module
- Room database with Note entity and NoteDao
- Repository interface and implementation
- ViewModels for notes list and note editor
- Empty Composable screens with navigation
- Build configuration (libs.toml for dependencies)
Use the latest stable versions:
- Compose BOM 2026.01.00
- Room 2.7.0
- Hilt 2.54
- Navigation Compose 2.9.0
Make sure the project compiles. Do not add UI yet.
Claude Code generated the project files. I copied them into the Android Studio project.
First compile attempt: 3 errors.
Hilt version mismatch. Claude Code used Hilt 2.54 but specified an older KSP version that was incompatible. AI tools frequently get Android dependency versions wrong because the compatibility matrix changes every few months. I updated to the correct KSP version from the Hilt release notes.
Room annotation processor. Claude Code used
kaptinstead ofkspfor Room. KSP is faster and is the recommended approach since 2024. I switched to KSP.Missing
@HiltViewModelannotation. One of the two ViewModels was missing the annotation. Simple fix.
Lesson: Always run ./gradlew assembleDebug after scaffolding. Do not assume AI-generated Android code compiles on the first try.
Step 3: Build the UI with Compose
This is where AI needs the most guidance. Compose UI code is where AI makes the most mistakes.
Prompt for the notes list screen:
Build the notes list screen.
Requirements:
- Top app bar with "QuickNotes" title and search icon
- Search bar that expands when the icon is tapped
- LazyColumn showing note cards
- Each card shows: title (bold), first 2 lines of content,
category chip, and date
- FAB (floating action button) to add a new note
- Swipe to delete with undo Snackbar
- Empty state: centered icon and "No notes yet" text
- Filter chips row below the search bar for categories
Use Material 3 components.
Pass a NoteListUiState from the ViewModel.
What AI generated correctly:
- The overall screen structure with
Scaffold,TopAppBar,LazyColumn, andFloatingActionButton - The note card layout with title, preview, and date
- The empty state composable
- The search bar expand/collapse animation
Common Compose mistakes AI made (and fixes):
Wrong modifier chains. AI wrote
Modifier.padding(16.dp).fillMaxWidth()instead ofModifier.fillMaxWidth().padding(16.dp). Order matters in Compose — padding before fillMaxWidth adds padding outside the full width, which is usually wrong. Always review modifier chains.Deprecated API usage. Claude Code used
rememberSwipeToDismissStatewhich was renamed in recent Compose versions. The API was correct in concept but used an older name. I updated to the current API.Incorrect state hoisting. The search query state was managed inside the composable instead of in the ViewModel. This meant the search text was lost on configuration changes. I moved the state to the ViewModel.
Missing
rememberfor derived state. The filtered notes list was recalculated on every recomposition. I wrapped it inrememberwith the correct keys.Lifecycle issues. The ViewModel’s
Flowwas collected usingcollectAsState()without lifecycle awareness. I changed it tocollectAsStateWithLifecycle()from thelifecycle-runtime-composelibrary. This is an Android-specific pattern that AI consistently misses.
Step 4: Add Room Database
Prompt:
Implement the Room database layer.
Note entity:
- id: Long (auto-generated primary key)
- title: String
- content: String
- category: String (default "General")
- createdAt: Long (timestamp)
- updatedAt: Long (timestamp)
NoteDao:
- getAllNotes(): Flow<List<Note>> — sorted by updatedAt desc
- getNoteById(id: Long): Flow<Note?>
- searchNotes(query: String): Flow<List<Note>> — search
title and content
- getNotesByCategory(category: String): Flow<List<Note>>
- insertNote(note: Note): Long
- updateNote(note: Note)
- deleteNote(note: Note)
- getCategories(): Flow<List<String>> — distinct categories
NoteDatabase:
- Version 1
- Export schema for migration testing
Repository:
- Wraps the DAO
- All operations go through Dispatchers.IO
Claude Code generated clean Room code. Room is well-documented and widely used, so AI handles it well.
One issue: The searchNotes query used LIKE '%' || :query || '%' which is correct, but Claude Code did not make it case-insensitive. I added COLLATE NOCASE to the query.
What AI got right that impressed me:
- The Flow return types for reactive queries
- The
@Transactionannotation on the insert-and-return-id pattern - The schema export configuration in the build file
- Proper use of
Dispatchers.IOin the repository
Step 5: AI-Generated ViewModel Tests
Prompt:
Write unit tests for NoteListViewModel.
Test cases:
- Initial state shows all notes
- Search filters notes by title
- Search filters notes by content
- Category filter shows only matching notes
- Delete note removes it from the list
- Delete note shows undo snackbar state
- Undo restores the deleted note
- Empty search shows all notes
Use:
- JUnit 5
- Kotlin coroutines test library
- Turbine for Flow testing
- Fake repository (not Mockito — use a real
in-memory implementation)
Create FakeNoteRepository that implements the
repository interface with an in-memory list.
Claude Code generated 8 tests and a FakeNoteRepository.
Test results: 6 passed, 2 failed.
Undo test failed. The test checked the state immediately after calling
undoDelete(), but the repository update was asynchronous. I addedadvanceUntilIdle()from the coroutines test library.Search test failed. The FakeNoteRepository’s search did not match the Room DAO’s case-insensitive behavior. I updated the fake to use
contains(query, ignoreCase = true).
What I added manually:
- A test for rapid search input (debouncing). The ViewModel should not query the database on every keystroke.
- A test for concurrent delete-and-undo operations.
Android-Specific AI Issues to Watch For
After building this app and several others, here are the most common mobile-specific issues with AI-generated code:
Compose issues:
- Wrong modifier order (padding before size constraints)
- Missing
rememberfor expensive computations - Using
collectAsState()instead ofcollectAsStateWithLifecycle() - Generating Material 2 code instead of Material 3
- Incorrect navigation argument passing (using strings for complex objects)
Android platform issues:
- Outdated dependency versions (especially Hilt, Room, and Compose BOM)
- Using
kaptinstead ofkspfor annotation processors - Missing ProGuard rules for libraries that need them
- Incorrect permission declarations in AndroidManifest.xml
- Wrong Gradle configuration syntax (Groovy vs Kotlin DSL confusion)
Lifecycle issues:
- Not handling configuration changes (state lost on rotation)
- Collecting flows without lifecycle awareness
- Starting coroutines in composables without
LaunchedEffect - Not canceling work when the screen is disposed
Android Studio’s Gemini integration can help catch some of these issues. It understands Android-specific patterns better than general-purpose AI tools. Use it alongside Claude Code for a second opinion on Android-specific code.
Tips for Mobile Development with AI
After building multiple Android apps with AI assistance, here are the patterns that consistently save time.
Keep a version reference file. Create a versions.md in your project root with the exact versions of every dependency. Include it in your CLAUDE.md. This prevents AI from generating code with outdated library versions.
## Current Versions
- Compose BOM: 2026.01.00
- Room: 2.7.0
- Hilt: 2.54
- KSP: 2.1.21-2.0.5
- Navigation Compose: 2.9.0
- Lifecycle: 2.9.0
Use AI for boilerplate, review for logic. Let AI generate the DAO interfaces, entity classes, and repository wrappers — these are highly repetitive and AI handles them well. Spend your review time on ViewModel logic, state management, and navigation — these are where bugs hide.
Test on a real device early. Emulators miss performance issues that show up on real hardware. AI-generated Compose code sometimes creates unnecessary recompositions that slow the UI on real devices. Use the Layout Inspector and recomposition counter in Android Studio to catch these.
Build in layers. Compile after each layer: data layer first, then ViewModels, then UI. This catches dependency and type errors early instead of facing 30 compiler errors after AI generates everything.
Time Tracking
| Step | Time | AI vs Manual |
|---|---|---|
| Architecture doc | 8 min | AI: 5 min, Review: 3 min |
| Scaffold + compile | 18 min | AI: 5 min, Fix: 13 min |
| UI (3 screens) | 35 min | AI: 15 min, Fix: 20 min |
| Room database | 10 min | AI: 6 min, Fix: 4 min |
| Tests | 18 min | AI: 8 min, Fix: 10 min |
| Total | 89 min | AI: 39 min, Human: 50 min |
The human-to-AI time ratio is higher for mobile than for web development. Android’s complex build system, platform-specific APIs, and rapidly changing libraries mean more fixing and less accepting.
Key Takeaways
- AI struggles more with mobile than web. Compose APIs change frequently, and AI training data lags behind. Always verify API names against official documentation.
- Compile early and often. Do not let AI generate 10 files before checking if the project builds. Scaffold, compile, then add features one at a time.
- Watch the modifier chain. This is the most common Compose mistake AI makes. Review every modifier chain.
- Use lifecycle-aware collection.
collectAsStateWithLifecycle()is the correct way to collect flows in Compose. AI almost always gets this wrong. - Dependency versions are a constant battle. Keep a reference to the official version catalogs and update what AI generates.
What’s Next?
In the next article, you will build a browser extension with AI. Chrome extensions are the perfect vibe coding project — small scope, fast feedback loop, and a clear structure that AI handles well.
Part 18 of the Vibe Coding series.