LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

DroidKaigi 2026 にブース出展 with コードレビュー企画

みなさん初めまして、2026年に新卒で入社したモバイルエンジニアの銭と申します。2026年9月1日(火)〜9月3日(木)の3日間にわたって開催されたDroidKaigi 2026に、LINEヤフー株式会社はゴールドスポンサーとして参加しました。

イベント期間中は、たくさんの方にLINEヤフーのブースにお越しいただき、誠にありがとうございました。また、弊社所属の森が登壇したセッション「デバイス操作はAIエージェントの時代へ。mobile-mcpを活用したAndroid UI/E2Eテストの挑戦」をお聞きいただいた皆さんにも、この場を借りてお礼申し上げます。

本ブログでは、9月2日〜3日のブース出展の様子と、参加型企画「Code Review Challenge」の概要をご紹介します。

会場の様子

今年は渋谷のベルサール渋谷ガーデンでの開催となりました。

DroidKaigi 2026の会場案内

会場では、弊社を含め、モバイルアプリ開発に携わる人たちが集まり、AIを業務や個人開発でどう活用しているかについて話していました。一年前と比べてCodexやClaude Codeは大きく進化しており、みんな戸惑いながらも暗中模索しているようでした。各社のカルチャーによってAIの使い方も異なり、参考になりました。参加者の方々との会話を通じて、私はLINEヤフーにはAIを業務で活用しやすい環境があると改めて感じました。その環境づくりに関わった方々に感謝しています。

学生の方も何人か会場にいらっしゃいました。学生の方々から刺激を受け、私も学び続けたいと思いました。学生は参加費も安いため、就職活動の参考に参加するのもおすすめです!

DroidKaigiのキャラクターたち

余談その1

かわいい。

DroidKaigi 2026で行われたマグロ解体ショー

余談その2

マグロ解体ショーは毎年の恒例だそうです。ITイベントらしからぬ企画に、思わず笑ってしまいました。

Code Review Challenge

昨年に続き、恒例のCode Review Challengeを開催しました。この一年でAIによるレビューが広く使われるようになった中、古き良き(?)人によるレビュー企画は大盛況でした。参加者の方々にはコードに付箋を貼っていただき、問題のある箇所をバシバシ指摘していただきました。みなさんから「まだまだ人間もやれるぞ」という底力や意地がメラメラと感じられます。急に無人島に行ってもコードレビューができるのは人間だけ。私はまだ人類の力を諦めません。

参加者とLINEヤフー社員がCode Review Challengeについて話し合う様子

こちらでは、作問者が問題を解説しています。大量の付箋には想定外の回答もあり、私たち社員にとっても大きな学びになっています。英語話者の方も参加され、グローバルな議論も繰り広げられました。

問題例: When the Flag Flies

セッションでも登壇された森の作問です。AndroidのUIではなく、APIから会話のメッセージを取得し、キャッシュに保存する機能(Repository)について、問題のある実装を見つけましょう。

  • loadConversation:画面を開いたときに会話を読み込みます。既読処理と期限切れ下書きの削除を行い、既読をつけたことをログに取ります。
  • refreshConversation:会話を再取得します。既読処理と下書きの削除は行いません。
  • clearMessages:指定した会話のメッセージをキャッシュから削除します。
  • updateConversation:会話の種類に応じてメッセージを取得・保存し、必要に応じてメンバー取得、既読処理、下書きの削除を行って Conversation オブジェクトを返します。
/**
 * Repository for loading, refreshing, and managing conversation messages.
 */
class MessageRepository(
    private val api: MessageApi,
    private val messageCache: MessageCache,
    private val groupMessageCache: GroupMessageCache,
    private val analytics: Analytics,
) {
    /**
    * Called from ConversationActivity when the user opens the screen.
     */
    suspend fun loadConversation(
        conversationId: String,
        isGroup: Boolean,
    ): Conversation {
        val conversation = updateConversation(
            conversationId = conversationId,
            isGroup = isGroup,
            markAsRead = true,
            deleteExpiredDrafts = true,
        )

        analytics.trackConversationOpened(conversationId)
        return conversation
    }

    /**
    * Called only from pull-to-refresh.
     *
    * Unlike [loadConversation], this does not mark messages as read
    * or delete expired drafts.
     */
    suspend fun refreshConversation(
        conversationId: String,
        isGroup: Boolean,
    ): Conversation {
        return updateConversation(
            conversationId = conversationId,
            isGroup = isGroup,
            markAsRead = false,
            deleteExpiredDrafts = false,
        )
    }

    suspend fun clearMessages(conversationId: String) {
        messageCache.deleteMessages(conversationId)
    }

    private suspend fun updateConversation(
        conversationId: String,
        isGroup: Boolean,
        markAsRead: Boolean,
        deleteExpiredDrafts: Boolean,
    ): Conversation {
        val messages = if (isGroup) {
            api.getGroupMessages(conversationId).also {
                groupMessageCache.saveMessages(conversationId, it)
            }
        } else {
            api.getSingleMessages(conversationId).also {
                messageCache.saveMessages(conversationId, it)
            }
        }

        // This property was previously named participants.
        val members = if (isGroup) {
            api.getGroupMembers(conversationId)
        } else {
            emptyList()
        }

        if (markAsRead) {
            if (isGroup) {
                api.markGroupAsRead(conversationId)
            } else {
                api.markSingleAsRead(conversationId)
            }
        }

        if (deleteExpiredDrafts) {
            messageCache.deleteExpiredDrafts()
        }

        return Conversation(
            messages = messages,
            members = members,
        )
    }
}

人によっては危なくて、読み進められない気持ちになったかと思います。この問題例では多くのミスを一度に扱っています。より複雑なシステムでは、同様の見落としが起こる可能性もあると考えています。

<=====この先にスクロールすると想定回答が表示されます。=====>

付箋が貼られたコードのパネルを囲み、Code Review Challengeに参加する来場者たち

写真は、会場で参加者の皆さんがコードのパネルを囲んでレビューに取り組んでいる様子です。びっくりするくらい大盛況でした!


想定回答は以下の通りです。

Major:重大な問題

  • グループ会話のキャッシュが削除されない: clearMessages は messageCache.deleteMessages(conversationId) だけを呼び出し、groupMessageCache を削除しません。会話タイプに応じたキャッシュ削除を行うか、単一・グループでキャッシュの管理構造やインターフェースを統一します。

  • 取得処理に状態変更が含まれる(CQS違反): loadConversation はデータ取得(Query)と同時に、既読化や期限切れ下書きの削除(Command)も行います。副作用を把握しやすくするため、これらの操作を取得処理から切り離し、UseCaseやViewModel側で明示的に実行します。

  • isGroupによる条件分岐が各所に散らばっている: if (isGroup) ... else ... がコード全体で使われており、会話種別が増えると各処理の修正が必要になって、修正漏れや影響範囲の拡大につながります。SingleConversationRepository と GroupConversationRepository に分けるか、ポリモーフィズムで種別ごとの処理をまとめます。

  • 会話IDと会話タイプを別々に渡している: conversationId: String と isGroup: Boolean の組み合わせが不正でも、コンパイル時に検出できません。ConversationTarget(id, type) のような型でまとめるか、GroupId と SingleConversationId のようにIDの型を分けて表現できます。

  • フラグ引数で関数の振る舞いを切り替えている: updateConversation は markAsRead や deleteExpiredDrafts のBoolean引数によって内部処理が変わります。複数のフラグで制御するのをやめ、目的ごとに単一の役割を持つ関数へ分割します。

Medium:設計と失敗時の動作

  • isGroup = falseの意味が暗黙的: falseが「1対1(Single)」を表すことは引数から読み取りにくく、会話タイプの追加にも対応しづらい状態です。Booleanをやめ、ConversationType のenumやsealed classで状態を明示します。

  • グループ会話のAPI通信が直列になっている: updateConversation は api.getGroupMessages と api.getGroupMembers を順番に実行しています。互いに依存しないリクエストであれば、coroutineScope と async を使って並列に取得できます。

  • RepositoryにAnalyticsの責務が混在している: analytics.trackConversationOpened がRepositoryにあるため、データ取得と画面表示の計測が結びついています。RepositoryからAnalyticsの依存を外し、呼び出し元のViewModelやUseCaseでイベントを送信します。

  • 失敗時の処理とキャッシュ優先の取得がない: 常にAPIから取得し、失敗時に既存キャッシュを返す処理やエラーハンドリングがありません。既読APIだけが失敗した場合も取得処理全体が失敗します。ネットワークエラーを処理し、キャッシュ優先(Offline-first)の取得または Result 型などによるエラー返却を導入します。

Minor:コメントの整理

  • KDocが特定のActivityに依存している: Called from ConversationActivity... と呼び出し元を記載しており、Repositoryの説明がUI実装の詳細に結びついています。特定の画面名への言及を避け、メソッドが何を行うかを説明します。

  • 不要な過去ログコメントが残っている: // This property was previously named participants. はGitの履歴で確認できる過去の変更理由で、コード内ではノイズになります。不要であれば削除します。

まとめ

DroidKaigi 2026の会場と、Code Review Challengeの様子をご紹介しました。参加者の皆さんとの議論は、私たちにとっても学びの多い機会となりました。ブースにお越しいただいた皆さん、ありがとうございました。