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.

  1. 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.

  2. Room annotation processor. Claude Code used kapt instead of ksp for Room. KSP is faster and is the recommended approach since 2024. I switched to KSP.

  3. Missing @HiltViewModel annotation. 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, and FloatingActionButton
  • 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):

  1. Wrong modifier chains. AI wrote Modifier.padding(16.dp).fillMaxWidth() instead of Modifier.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.

  2. Deprecated API usage. Claude Code used rememberSwipeToDismissState which was renamed in recent Compose versions. The API was correct in concept but used an older name. I updated to the current API.

  3. 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.

  4. Missing remember for derived state. The filtered notes list was recalculated on every recomposition. I wrapped it in remember with the correct keys.

  5. Lifecycle issues. The ViewModel’s Flow was collected using collectAsState() without lifecycle awareness. I changed it to collectAsStateWithLifecycle() from the lifecycle-runtime-compose library. 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 @Transaction annotation on the insert-and-return-id pattern
  • The schema export configuration in the build file
  • Proper use of Dispatchers.IO in 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.

  1. Undo test failed. The test checked the state immediately after calling undoDelete(), but the repository update was asynchronous. I added advanceUntilIdle() from the coroutines test library.

  2. 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 remember for expensive computations
  • Using collectAsState() instead of collectAsStateWithLifecycle()
  • 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 kapt instead of ksp for 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

StepTimeAI vs Manual
Architecture doc8 minAI: 5 min, Review: 3 min
Scaffold + compile18 minAI: 5 min, Fix: 13 min
UI (3 screens)35 minAI: 15 min, Fix: 20 min
Room database10 minAI: 6 min, Fix: 4 min
Tests18 minAI: 8 min, Fix: 10 min
Total89 minAI: 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.