3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが書いたKotlinコードをベテランエンジニアがレビューした結果

3
Last updated at Posted at 2026-03-01

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を直接触らせていない。
updateStreakgetHabitByIdupdateHabit の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 を使った検索の実装、完璧。
これ、ちゃんとわかってないと flatMapswitchMap を間違えて使うやつが多い。
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のコードレビューをやったことある方、他の印象も聞かせてください。


関連記事

8種類のAndroidアプリテンプレート → Gumroad


関連記事

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?