AIが書いたKotlinコードをベテランエンジニアがレビューした結果
きっかけ
「AIが書いたコードって実際どうなの?」
この疑問に答えるため、Android開発歴12年の知人エンジニア(以下「おじさん」)に、
Claude Codeが生成したKotlinコードを何も言わずにレビューしてもらいました。
「人間が書いたと思って、普通にレビューしてください」
その結果がこちらです。
対象コード:習慣トラッカーアプリ
Claude Codeへの指示は一行だけ:
「Kotlin + Jetpack Compose + Room + MVVMでシンプルな習慣トラッカーを作ってください」
生成時間:47秒
レビュー対象1:Room Entity
AIが生成したコード
@Entity(tableName = "habits")
data class Habit(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val description: String = "",
val targetDaysPerWeek: Int = 7,
val createdAt: Long = System.currentTimeMillis(),
val currentStreak: Int = 0,
val longestStreak: Int = 0,
val isActive: Boolean = true
)
おじさんのコメント
id: Int = 0はAuto-incrementのデフォルト値として正しい。
createdAtをLongで持つのも標準的。
ただcurrentStreakをEntityに持たせるのは議論の余地がある。
計算値はRepositoryで導出するべきという考え方もある。
全体的には問題ない。新卒の書いたコードより整理されてる。
評価:★★★★☆(4/5)
レビュー対象2:DAO
AIが生成したコード
@Dao
interface HabitDao {
@Query("SELECT * FROM habits WHERE isActive = 1 ORDER BY name ASC")
fun getAllHabits(): Flow<List<Habit>>
@Query("SELECT * FROM habits WHERE id = :habitId")
suspend fun getHabitById(habitId: Int): Habit?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertHabit(habit: Habit): Long
@Update
suspend fun updateHabit(habit: Habit)
@Query("UPDATE habits SET isActive = 0 WHERE id = :habitId")
suspend fun softDeleteHabit(habitId: Int)
@Query("SELECT * FROM habits WHERE isActive = 1 AND name LIKE '%' || :query || '%'")
fun searchHabits(query: String): Flow<List<Habit>>
}
おじさんのコメント
Flow<List<Habit>>を返しているのが正しい。コルーチンとRoomの組み合わせを理解してる。
ソフトデリートを実装してるのは丁寧。isActive = 0にするだけで物理削除しない。
OnConflictStrategy.REPLACEは用途によってIGNOREの方が良い場面もあるが、habitトラッカーなら許容範囲。
SQLのLIKE検索、|| :query ||のバインドの書き方も正しい。
普通に使える。
評価:★★★★★(5/5)
レビュー対象3:Repository
AIが生成したコード
class HabitRepository(private val habitDao: HabitDao) {
val allHabits: Flow<List<Habit>> = habitDao.getAllHabits()
suspend fun insertHabit(habit: Habit): Long {
return habitDao.insertHabit(habit)
}
suspend fun updateHabit(habit: Habit) {
habitDao.updateHabit(habit)
}
suspend fun deleteHabit(habitId: Int) {
habitDao.softDeleteHabit(habitId)
}
fun searchHabits(query: String): Flow<List<Habit>> {
return habitDao.searchHabits(query)
}
suspend fun updateStreak(habitId: Int, newStreak: Int, longestStreak: Int) {
val habit = habitDao.getHabitById(habitId) ?: return
habitDao.updateHabit(
habit.copy(
currentStreak = newStreak,
longestStreak = maxOf(longestStreak, newStreak)
)
)
}
}
おじさんのコメント
Repositoryパターンとして綺麗に分離できてる。ViewModelからDAOを直接触らせていない。
updateStreakはgetHabitById→updateHabitの2クエリになってるが、
トランザクション管理が必要な場合は@Transactionアノテーションを付けた方が安全。
でも習慣トラッカー程度なら過剰設計になる可能性もある。バランス感覚はある。
合格ライン余裕で超えてる。
評価:★★★★☆(4/5)
レビュー対象4:ViewModel
AIが生成したコード
@HiltViewModel
class HabitViewModel @Inject constructor(
private val repository: HabitRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
val habits: StateFlow<List<Habit>> = searchQuery
.flatMapLatest { query ->
if (query.isEmpty()) repository.allHabits
else repository.searchHabits(query)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onSearchQueryChange(query: String) {
_searchQuery.value = query
}
fun addHabit(name: String, description: String, targetDays: Int) {
viewModelScope.launch {
repository.insertHabit(
Habit(
name = name,
description = description,
targetDaysPerWeek = targetDays
)
)
}
}
fun deleteHabit(habitId: Int) {
viewModelScope.launch {
repository.deleteHabit(habitId)
}
}
}
おじさんのコメント
flatMapLatestを使った検索の実装、完璧。
これ、ちゃんとわかってないとflatMapやswitchMapを間違えて使うやつが多い。
SharingStarted.WhileSubscribed(5000)も適切。5秒後にUpstreamを止める。
Hilt対応もできてる。
これ、ジュニアに書かせたら詰まる部分を全部正しく実装してる。
誰が書いたの?
評価:★★★★★(5/5)
おじさんの総評
全コードを見終わったあと、「AI生成だ」と告げました。
沈黙が3秒ありました。
「…マジで?」
「アーキテクチャとして一貫してる。設計の意図が伝わってくる。
flatMapLatestとか、正直ちゃんとわかってる人間でも間違えるやつをAIが正しく使ってる。
実務に出してもレビュー通るレベル。」
「ただ、エラーハンドリングが甘い。
try-catchがない箇所がある。
RepositoryのupdateStreakが途中でクラッシュしたときに無言で失敗する。
本番コードならResult<T>型で包んでUIに伝えるべき。」
「でもベースとしては使える。テンプレートとして渡してもいい。」
改善が必要だった箇所
おじさんが指摘したエラーハンドリングを追加すると:
// Repositoryに追加
suspend fun updateStreak(habitId: Int, newStreak: Int, longestStreak: Int): Result<Unit> {
return try {
val habit = habitDao.getHabitById(habitId)
?: return Result.failure(IllegalArgumentException("Habit not found: $habitId"))
habitDao.updateHabit(
habit.copy(
currentStreak = newStreak,
longestStreak = maxOf(longestStreak, newStreak)
)
)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
これもClaudeに「Result<T>で包んでエラーをUIに伝えるように修正して」と指示すれば30秒で直ります。
結論:AIコードの実力と限界
| 項目 | 評価 |
|---|---|
| アーキテクチャ設計 | ★★★★★ |
| Kotlin idiom | ★★★★★ |
| Compose UI | ★★★★☆ |
| エラーハンドリング | ★★★☆☆ |
| テストコード | ★★★☆☆(生成されるが薄い) |
AIが得意なのは「正しいパターンを正しく組み合わせること」。
苦手なのは「何が起きるかわからない状況のケアをすること」。
エラーハンドリングとエッジケースは人間がレビューする必要があります。
ただそれも、「ここを改善して」と指示すれば即座に直ります。
テンプレートとして使いたい人へ
今回レビューしたコードをベースにした8種類のAndroidアプリテンプレートをGumroadで公開しています。
- 習慣トラッカー(今回のコード)
- 支出メモ
- 予算管理
- タスクマネージャー
- ミーティングタイマー
- カウントダウン
- 単位変換
- ワークアウトログ
全てKotlin + Jetpack Compose + Material3 + Room。
おじさんがレビューしてもOKと言ったレベルのコードです。
AIのコードレビューをやったことある方、他の印象も聞かせてください。
関連記事
- Claude Codeに「Androidアプリ作って」と言ったら47秒で完成した話
- Claude Code vs Cursor vs GitHub Copilot:比較
- コーディング経験ゼロからAndroidアプリを11分で動かす
- AIが書いたKotlinコードをベテランエンジニアがレビューした結果
8種類のAndroidアプリテンプレート → Gumroad