みなさん初めまして、2026年に新卒で入社したモバイルエンジニアの銭と申します。2026年9月1日(火)〜9月3日(木)の3日間にわたって開催されたDroidKaigi 2026に、LINEヤフー株式会社はゴ ールドスポンサーとして参加しました。
イベント期間中は、たくさんの方にLINEヤフーのブースにお越しいただき、誠にありがとうございました。また、弊社所属の森が登壇したセッション「デバイス操作はAIエージェントの時代へ。mobile-mcpを活用したAndroid UI/E2Eテストの挑戦」をお聞きいただいた皆さんにも、この場を借りてお礼申し上げます。
本ブログでは、9月2日〜3日のブース出展の様子と、参加型企画「Code Review Challenge」の概要をご紹介します。
会場の様子
今年は渋谷のベルサール渋谷ガーデンでの開催となりました。

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

余談その1
かわいい。

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

こちらでは、作問者が問題を解説しています。大量の付箋には想定外の回答もあり、私たち社員にとっても大きな学びになっています。英語話者の方も参加され、グローバルな議論も繰り広げられました。
問題例: 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,
)
}
}
人によっては危なくて、読み進められない気持ちになったかと思います。この問題例では多くのミスを一度に扱っています。より複雑なシステムでは、同様の見落としが起こる可能性もあると考えています。
<=====この先にスクロールすると想定回答が表示されます。=====>

写真は、会場で参加者の皆さんがコードのパネルを囲んでレビューに取り組んでいる様子です。びっくりするくらい大盛況でした!
想定回答は以下の通りです。
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の様子をご紹介しました。参加者の皆さんとの議論は、私たちにとっても学びの多い機会となりました。ブースにお越しいただいた皆さん、ありがとうございました。


