
Android
イベント
マガジン
技術ブログ
こんにちは、LIFULL HOME'Sのネイティブアプリケーション開発チームでエンジニアリングマネージャーをしている佐々木です。 前回の記事 では、チームの業務知識をAIに構造化して渡すことで調査工数を80〜90%削減した話を書きました。その最後に「コンテキストレイヤ」という概念に触れました。企業のデータを束ね、ビジネスロジックを理解できる層をAIエージェントに供給する基盤のことです。 今回は、そのパッケージの技術設計について掘り下げます。作ったものをあらためてコンテキストレイヤの視点で整理してみたら、5つのレイヤにきれいに分解できたので、その構造に沿って説明していきたいと思います。 パッケージの全体像 レイヤ①:セマンティック — ドメイン知識の構造化 レイヤ②:メタデータ — 30リポジトリを25ファイルで表現する レイヤ③:リネージ — 画面名からコードへの追跡可能性 レイヤ④:ポリシー — 行動ルールと委譲構造 絶対ルール:メインはコードを読まない サブエージェントへのスキル情報の橋渡し 7体のサブエージェント 行動定義 > ロール設定 レイヤ⑤:トライバルナレッジ — 45個のスキル Progressive Disclosure(段階的開示) 目的単位での切り出し 複合スキルと依存関係 レイヤを横断するしくみ:ナレッジキャッシュ 具体例:PdMが4リポジトリを横断して障害原因を特定した話 まとめ パッケージの全体像 まず構成要素を示します。 .kiro/ ├── steering/ … メインエージェントの行動ルール(5ファイル) ├── agents/ … サブエージェント定義(7体) └── skills/ … 質問パターンごとの手順定義(45個) docs/ … API仕様・画面マッピング・ドメイン知識 repos/ … ソースコード(6リポジトリ) メインエージェント(オーケストレーター)が質問を受け取り、スキルを参照して適切なサブエージェントに調査を委譲する。サブエージェントは独立したコンテキストでコードを読み、結果の要約だけをメインに返す。この構造を支えているのが「5層のコンテキストレイヤ」です。 レイヤ パッケージでの実装 役割 セマンティック docs/domain-knowledge.md 業務用語・分類体系の定義 メタデータ docs/bff/ , docs/backend-api/ API仕様の構造化 リネージ docs/screen-mapping.md 画面→コードの追跡可能性 ポリシー steering/delegation.md 行動ルール・制約・委譲手順 トライバルナレッジ skills/ (45個) 調査手順・暗黙のノウハウ 以下、各レイヤの設計を詳しく見ていきます。 レイヤ①:セマンティック — ドメイン知識の構造化 docs/domain-knowledge.md には、LIFULL HOME'Sアプリケーション固有の業務用語と分類体系が定義されています。 たとえば「新築」には2種類あります。 新築分譲 : デベロッパーが直接販売。DB仕様書Aで管理 新築仲介 : 築1年以内・未入居だが仲介扱いの流通物件。DB仕様書Bで管理 この区別を知らないAIは「新築の物件詳細を教えて」と聞かれたとき、どちらのDBを見ればよいかわかりません。ドメイン知識がなければ調査が始められないのです。 物件種別コード(bukken_type)の分類体系、現行BFFと新BFF(未移行)の関係など、エンジニアの頭の中にしかなかった知識をMarkdownとして書き出しています。このレイヤがあることで、AIは質問の文脈を正しく解釈できます。 レイヤ②:メタデータ — 30リポジトリを25ファイルで表現する LIFULL HOME'SアプリケーションのBFFはGitHub上に30のマイクロサービスリポジトリとして存在します。バックエンドAPIも39エンドポイントを持っています。これらをすべてパッケージに含めるとサイズが膨大になる上、AIのコンテキストも溢れます。 設計方針は「 アプリケーションが実際に利用しているエンドポイントの仕様だけを、Markdownとして抽出する 」です。 docs/ ├── bff.md … 全24エンドポイントのアーキテクチャ概要 ├── bff/ … 利用エンドポイントの詳細仕様(19ファイル) │ ├── search-common.md … 物件検索共通パラメータ・変換仕様 │ ├── detail-rent.md … 賃貸物件詳細のレスポンス構造 │ ├── detail-sale.md … 売買物件詳細 │ └── ... ├── backend-api.md … 全39エンドポイントの一覧 └── backend-api/ … 利用エンドポイントの詳細仕様(6ファイル) 30リポジトリのコードを丸ごと入れる代わりに、各エンドポイントの入力パラメータ・レスポンス構造・変換ロジックをドキュメント化しています。steeringの references.md にはこの docs/ へのルーティングだけを記述しています。「物件詳細のAPI仕様を聞かれたら docs/bff/detail-*.md を参照」という形でサブエージェントに渡します。 ソースコードを丸ごと含めるのは、UI実装を持つiOS / Android、Kotlin Multiplatform (KMP) による共通ロジック層、通知配信、DB仕様書2つの計6リポジトリだけ。この設計で190MBのzipに収まっています。 レイヤ③:リネージ — 画面名からコードへの追跡可能性 エンジニアに「物件詳細画面のコードどこ?」と聞けば即答できます。でもこの知識はエンジニアの頭の中にしかありませんでした。 これを screen-mapping.md として構造化しました。 | 画面名 | iOS | Android | |---|---|---| | 物件詳細 | (iOSの物件詳細ディレクトリ) | (Androidの物件詳細ディレクトリ) | | お気に入り一覧 | ... | ... | | 問い合わせ | ... | ... | | ... | ... | ... | 34画面について、各プラットフォームのディレクトリパスを対応付けています。iOS/Android間で共通化されたロジック層がある場合はその所在も記載し、サブエージェントが無駄に探索しないようにしています。 AIはこのマッピングを 調査の起点 として使います。「物件詳細画面の表示項目を教えて」と聞かれたら、まずここからパスを引き、それをサブエージェントへの指示に含めて並列起動する。マッピングがなければリポジトリ全体をgrepで探索することになり、検索に時間がかかる上、大量のファイルパス候補がコンテキストに載って肥大化します。マッピングを用意したことで、検索速度の向上とコンテキスト肥大化の抑制を同時に実現しています。 このマッピングは「完全な一覧」ではなく「調査の起点」と位置付けています。載っていない画面はリポジトリ内を検索して探しますし、それでも見つからなければ「見つかりませんでした」と正直に返します。 レイヤ④:ポリシー — 行動ルールと委譲構造 steering/delegation.md はパッケージの中で最も重要なファイルです。メインエージェントの行動を制御するポリシーレイヤとして機能します。 絶対ルール:メインはコードを読まない 自分自身で readFile、readCode、grepSearch、readMultipleFiles を 使ってリポジトリ内のコードを読んではいけない。 このルールに例外はない。「ちょっと確認するだけ」「1ファイルだけ」も禁止。 データ取得(コード調査)→ 分析 → 回答生成を1つのエージェントのコンテキスト内で全部走らせると、取得したコードが積もった時点でコンテキストが枯渇し、分析・回答生成の品質が落ちます。調査はサブエージェントの独立コンテキストで行い、結果の要約だけをメインへ返す構造にすることで、メインのコンテキストをクリーンに保っています。 サブエージェントへのスキル情報の橋渡し Kiro IDE のサブエージェントには、スキルを読み込む手段がないという制約があります。steeringファイルはサブエージェントにも自動で注入されますが、スキル定義についてはCLI版のような resources フィールドが存在せず、サブエージェントに設定できません。つまり、サブエージェントはスキルの内容を直接参照できません。 重要 : サブエージェントはスキルを直接参照できない。スキルに記載された調査のヒント(具体的なファイルパス、定義箇所、調査観点など)は、サブエージェントへの指示(query)に必ず含めること。 この制約を解決するために、delegation.mdは委譲手順を5ステップで定義しています。 質問がどのリポジトリに関係するか判断する 質問の種類に応じたスキルを参照し、調査に必要な情報を把握する スキルから得た情報をサブエージェントへの指示に含めて 並列起動する 各サブエージェントの結果を統合して回答する 結果が不十分な場合は追加のサブエージェントを起動する メインエージェントはスキルの中身(ファイルパス・調査観点・注意事項)を読み取り、それをサブエージェントへの指示にそのまま含めて渡す。この「橋渡し」がdelegation.mdの核です。 7体のサブエージェント エージェント 役割 コードを読む? ios-investigator iOSアプリケーション調査 ✅ Android-investigator Androidアプリケーション調査 ✅ kmp-investigator KMP(iOS/Android共通ロジック)調査 ✅ db-investigator DBテーブル仕様調査 ✅ backend-investigator 通知配信ロジック調査 ✅ product-analyst 数値の分析・解釈 ❌ bq-data-fetcher BigQueryクエリ設計・実行 ❌ product-analyst と bq-data-fetcher は意図的に分離しています。 bq-data-fetcher は「クエリを書いて実行してデータを返す」だけの役割です。そのデータに意味を見出す(So What?を言う)のは product-analyst の仕事です。取得と分析を同一コンテキストに入れないことで、それぞれの品質を保っています。 行動定義 > ロール設定 各エージェント定義において、「あなたは最高峰のiOSエンジニアです」のようなロール設定は行っていません。LLMに対するペルソナ設定は性能向上に寄与しない( むしろ最大26.2%の劣化が報告されている )ことが研究で示されています。 代わりに、各エージェントには具体的な行動を定義しています。 何をするか : 対象リポジトリのsteering配下のファイルをすべて読み、最新のルールを把握する 何をしないか : コードの編集は一切行わない。書き込み系MCPツール使用禁止 完了条件 : 根拠となったファイルパスを必ず明示する あいまいなロール設定より、具体的な行動指示の方がAIは正確に従ってくれます。 レイヤ⑤:トライバルナレッジ — 45個のスキル skills/ ディレクトリには45個のスキルが格納されています。PdMから「こういうことを知りたい」とリクエストがあったものと、「この人ならこういうことができたら喜ぶだろうな」と先回りして追加したものの積み重ねです。 カテゴリ 数 例 仕様調査 17 画面の表示項目、ビジネスロジック、API仕様、画面遷移、計測イベント デザイナー支援 6 アクセシビリティチェック、実装可能性判定、アセット命名提案 データ分析 8 CVR・ファネル分析、レビュー傾向、A/Bテスト結果 統合分析 6 UX課題発見、KPI下落診断、競合比較、デザインと実装の差分 評価・意思決定支援 2 施策比較、優先順位付け ドキュメント・タスク管理 6 仕様書ドラフト、Jiraチケット起票、ナレッジ保存 Progressive Disclosure(段階的開示) Kiro IDEの スキル機能 は Agent Skills仕様 に基づいています。重要な特徴が段階的開示です。 セッション起動時にはスキルの フロントマター (名前・説明・トリガキーワード)だけが読み込まれる ユーザーの質問がトリガへマッチしたときに初めて 全文が展開 される 45個のスキルの全文を常時コンテキストに載せると、それだけでコンテキストの大部分を消費します。Progressive Disclosureにより、必要なスキルだけが必要なときに展開され、コンテキストが節約されます。 目的単位での切り出し スキルは「ステップ順」ではなく 「何を明らかにするか(目的)」 単位で切っています。 ❌ ステップ順: search-file → read-code → format-output ✅ 目的単位: comparing-screens / explaining-business-logic / diagnosing-kpi-drop 目的単位で切ると、再利用可能になり、各スキルに品質基準・完了条件・出力フォーマットを定義でき、複合スキルがほかのスキルを「呼び出す」構造を作れます。 複合スキルと依存関係 45個のスキルは独立して動くものと、ほかのスキルを内部で呼び出す「複合スキル」があります。delegation.mdに依存関係グラフが明記されています。 summarizing-screen ⊃ {comparing-screens, tracing-navigation, investigating-api, investigating-tealium, explaining-business-logic} drafting-specification ⊃ {comparing-screens, investigating-api, analyzing-change-impact, investigating-tealium} + investigating-web-site(任意) 「物件詳細画面の全体像を教えて」と聞くだけで、 summarizing-screen が5つの基本スキルを順に呼び出し、表示項目・画面遷移・API仕様・計測イベント・ビジネスロジックをまとめて返します。 レイヤを横断するしくみ:ナレッジキャッシュ 5つのレイヤは調査のたびに参照されますが、頻出する質問のたびに毎回サブエージェントを起動してコードを読むのは非効率です。そこで調査結果をConfluenceに蓄積し、2回目以降はキャッシュから即答するしくみを入れています。 フロー: 1. 調査系スキルの起動前に、Confluenceで [KB] 接頭辞のページを検索 2. ヒット → 鮮度チェック(キャッシュの最終更新日 ≥ パッケージの更新日時なら有効) 3. 有効ならサブエージェント起動なしで即答 4. 重い調査(サブエージェント3つ以上起動したもの)の完了後 → 保存を提案 リアルタイム数値を扱うデータ分析系スキルはキャッシュ対象外にしています。昨日のCVRと今日のCVRは違うからです。 具体例:PdMが4リポジトリを横断して障害原因を特定した話 5つのレイヤが連携して機能した実例を紹介します。 ある日、「お気に入りに追加できない」というユーザー報告がありました。PdMがこのパッケージを使って調査した結果は以下でした。 ポリシー層 (delegation.md)が explaining-business-logic スキルを選択 トライバルナレッジ層 (スキル)が「お気に入り機能なら、共通ロジック層+iOS/Androidのキーチェイン/キーストア周りを見るべき」と指示 リネージ層 (screen-mapping)からお気に入り関連のパスを特定 3つのサブエージェントが並列起動(ios/Android/kmp-investigator) セマンティック層 のドメイン知識を踏まえて共通ロジック層内の該当箇所で kSecAttrAccessible 未設定を特定 原因判明:バックグラウンド復帰時にKeychainアクセスが失敗 → 毎日16,500件のエラーが発生していた エンジニアに依頼していたら、割り込み待ちも含めて数日かかってもおかしくない調査です。それが数分で完了しています。4リポジトリを横断して1行のコードまで特定できたのは、5層のコンテキストレイヤが連携した結果です。 まとめ コードベースパッケージの技術設計は、突き詰めると「 AIが正しく答えるために必要なコンテキストを、いかに効率的に供給するか 」の設計です。 レイヤ 解決する問題 セマンティック AIが業務用語を誤解する メタデータ 30リポジトリを丸ごと含められない リネージ AIがどのファイルを見ればよいかわからない ポリシー AIが何をすべきか/すべきでないかを制御する トライバルナレッジ エンジニアの調査ノウハウを再現する 前回の記事で「コンテキストレイヤ」に言及したとき、それは振り返って気付いた概念でした。今回この記事を書くにあたり、あらためて自分のパッケージを5層で整理してみると、各レイヤが明確な役割を持って連携していることがわかります。 この構造は特定のツールや言語に依存していません。実際に、同じ設計をベースに賃貸・流通チーム向けのパッケージ(18リポジトリ×13エージェント)も稼働しています。自分のチームでコンテキストレイヤを構築する際の参考になれば幸いです。 次回は、このしくみをエンジニアのいないチームに応用して、業務効率化に取り組んだ話を書く予定です。 最後に、LIFULLではともに挑戦していける仲間を募集しています。ご興味をお持ちいただけましたら、ぜひ以下のページもご覧ください。 https://hrmos.co/pages/LIFULL/jobs/010 https://hrmos.co/pages/LIFULL/jobs/010-9998
こんにちは。LINEアプリ開発SBU AIディベロッパーエクスペリエンスチームの onevcat(王 巍)です。最近は、AI エージェントを開発・検証のループに組み込むためのツールづくりに取り組んでい...
はじめに こんにちは、ZOZOTOWN開発本部ZOZOTOWN開発2部Androidブロックのにしみーです。 本記事では、レガシーなFragmentをKotlin + Jetpack Compose + MVVMベースの構造へリファクタリングした取り組みを紹介します。 対象画面のFragmentはJava製の共通基底クラスを継承しており、ロジック・状態管理・データ取得が基底クラスとFragmentに混在した構造でした。この構造を解体しながらリファクタリングを進めるにあたり、私たちは2つのアプローチを採用しました。1つは、実装前にAIと協働で「仕様調査 → 設計書 → タスク分解」のドキュメントを書き起こす、設計書から書き始める進め方です。もう1つは、ViewModelやRepositoryを後回しにしUIを先にComposeへ移行し、動かしながら段階的に進める工程設計です。 レガシーなAndroid実装をモダンな構造に置き換えている現場、肥大化した基底クラスと向き合っている方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 解体したい対象 辛かった点 ── レガシーの実像 共通基底クラスへの強い依存 Object型のリストに見出しとアイテムが混在 イベントバスとRxJava 2による状態伝播 Fragment内でのDB直接アクセス 解決の取り組み 設計書から書き始める ── 実装より先にドキュメントを書ききる なぜ実装より先に設計書を書ききる必要があったか ドキュメントの中身 AIを雛形作成に活用する UIから先に組み立て、動かしながら段階的に進める ── ハードコードで動く画面を先に作る なぜUIから先に組み立てるのか フェーズ分割と段階的接続 リファクタリング後のアーキテクチャ sealed interfaceで画面状態を表現する sealed interfaceでリストアイテムを型安全に表現する AI活用Tips Tips 1:タスク分解書を「AIへの依頼の共通言語」にする Tips 2:設計ドキュメント自体もGit管理する Tips 3:レビューで得た知見を「AI参照用」のドキュメントに蓄える Tips 4:AIが苦手なところを人が補う まとめ 背景・課題 解体したい対象 今回対象にしたのは、ZOZOTOWN Androidアプリの「公式ショップから探す」画面です。この画面では、キーワード検索やソート条件をもとにショップ一覧を絞り込めます。検索やソートのロジックをアプリ内で保持していたため、実装が複雑になっていました。 また、この画面のFragmentはJava製の抽象クラスを共通基底として継承する形で実装されていました。基底クラス側に多くの責務がまとめられており、たとえば画面表示・状態管理・データ取得・アニメーション制御まで、多くの実装が集約されていました。サブクラスとなる画面側のFragmentは、基底クラスが定義する抽象メソッドを実装することで成り立っていました。 このような「基底クラスありき」の構成は、複数の画面に対する共通化を素早く実現する手段としては有効です。しかし長年運用するうちに、この土台に責務が積み重なり1000行を超える規模に膨らんでいきました。画面ごとの個別事情も吸収しきれず、気づけば画面1つの挙動を読み解くにはまず基底クラスを読み込まなければならない状態になっていたのです。 今回のリファクタリング対象は、その共通基底クラスを継承した画面のうちの1つです。共通基底クラスごと解体するわけではなく、対象画面のFragmentをKotlin + Jetpack Compose + MVVMベースの構造に作り直すのがスコープでした。Fragmentに集中していたロジックは、Composable・ViewModel・Repositoryへ分散させていきます。ただし、対象画面を作り直すには、基底クラスが担っていた責務をどこに引き取らせるかを先に決めなくてはなりません。そのため、土台ごと向き合いながら作り直す必要がありました。 辛かった点 ── レガシーの実像 リファクタリング着手前に、私たちが向き合わなければならなかった課題を共有しておきます。 共通基底クラスへの強い依存 共通基底クラスによってサブクラスでは多数の抽象メソッドの実装が必要になっていました。サブクラスは基底クラスが定めた枠組みに乗るだけで画面が成立するため、共通機能の修正は基底クラス1箇所に閉じるという保守性の利点もありました。 ただし、抽象メソッドの多くは「Viewを返すだけ」のgetterで、protectedな変数を介して基底クラスとサブクラス間で状態を共有していました。構造を抽象化して示すと次のとおりです。 abstract class BaseListFragment extends Fragment { protected List<Object> displayData = new ArrayList<>(); protected String searchText = "" ; abstract protected RecyclerView getRecyclerView(); abstract protected EditText getSearchEditText(); // ...続く } サブクラスから見れば、protected変数はいつ・どこから書き換えられているかが追跡しづらく、画面単体での挙動を読み解くのは困難でした。 Object型のリストに見出しとアイテムが混在 リスト表示にはObject型のリスト( List<Object> )が使われており、A〜Zなどの見出し行とその配下のアイテムが同じリストに混在していました。この構造は1つのRecyclerViewで「見出し + アイテム」を扱えるため、表示順序や挿入・削除を単一のリスト操作で表現できる利点がありました。 ただ、型情報は失われており、RecyclerViewのAdapter側ではキャストして表示を切り替えていました。コンパイル時の型チェックが効かず、リスト操作のたびにキャストが必要でコード補完やリファクタリングツールの支援も受けにくい状態でした。 イベントバスとRxJava 2による状態伝播 性別の変更やアイテム選択などのイベントはOttoによる静的なイベントバスで伝播され、購読側は @Subscribe で受け取っていました。データ取得はRxJava 2の Observable で書かれており、 subscribeOn / observeOn によるスレッド切り替えや map / flatMap での非同期処理を宣言的に書けました。これらは当時のAndroidで広く採用されていた、Fragment間通信と非同期処理の主流パターンでした。 一方で、イベントがコードを横断して飛び交うため、状態の流れを追うのに時間がかかりました。Disposableの解放管理もFragment側で行う必要があり、リソースリークの温床にもなりがちでした。 Fragment内でのDB直接アクセス Repository層は実質的に存在せず、FragmentからRoomのDAOを直接呼び出していました。レイヤーが少ない分、データ取得の実装はシンプルで見通しが良いため過剰な抽象化を避ける観点では合理的にも見えます。 しかし、検索ロジックもFragment内に実装されていたため、ユニットテストで切り出すにはFragment全体を起動する必要がありました。ビジネスロジックがプレゼンテーション層に貼り付いている状態は、テスタビリティを大きく損ねていました。 これらが組み合わさることで、画面1つの挙動を変えるだけでも基底クラスを読み解き、状態の流れを追い、影響範囲を見積もる作業が必要でした。Compose化や機能改修を進めるうえで、これは大きな足かせになっていました。 解決の取り組み 設計書から書き始める ── 実装より先にドキュメントを書ききる 通常、画面のリファクタリングは「コードを読みながら少しずつ手を入れていく」というアプローチで進められることが多いと思います。今回もそうしたかったのですが、共通基底クラスごと向き合う必要があったためそれは難しい判断でした。 なぜ実装より先に設計書を書ききる必要があったか 基底クラスは対象の画面以外にも継承されていました。場当たり的に対象画面側だけを書き換えていくと、他画面への影響や基底クラスから引き剥がした責務の置き場所が曖昧になっていきます。途中まで進めてから「結局この責務はどこに置けばよかったのか」と立ち止まると、それまでの作業がやり直しになるリスクもあります。 加えて、私たちはこのリファクタリングをAIエージェントと分担して進めることを前提にしていました。AIに任せる範囲を明確にするには、私たちとAIが共通言語として参照できるドキュメントが必要です。コードベースだけを見せて「いい感じにリファクタリングして」と頼んでも、設計判断はぶれてしまいます。 そこで私たちは、実装前に「仕様調査」「リファクタリング設計書」「タスク分解」の3点をドキュメントとして書ききることにしました。 ドキュメントの中身 それぞれのドキュメントは次の目的で書きました。 仕様調査は対象画面の現状の挙動を網羅的に書き起こすドキュメントです。UI構成、状態管理、データフロー、依存関係、アナリティクス、現状の問題点までを1つの「現状把握ドキュメント」としてまとめました。 リファクタリング設計書は、移行後のアーキテクチャを書き起こすドキュメントです。Repository/ViewModel/UIの責務分担、データ構造、フェーズ分割、各フェーズでの動作確認ポイントまでを記述しました。基底クラスが担っていた責務の配置先をこの段階で決定しました。 タスク分解は、設計書に沿って実装タスクを「画面表示」「フィルタ切り替え」「ソート切り替え」「検索」など、機能単位の細かい粒度に分解したドキュメントです。各タスク間の依存関係を明示し、並行して作業できる範囲やクリティカルパスも明記しました。 AIを雛形作成に活用する これらのドキュメントは、すべてを人が一から書いたわけではありません。私たちは主にClaude Codeを活用し、既存コードを読ませて雛形を出力させました。その雛形をもとに、人が事実関係をチェックしながら整えていく進め方を採用しました。AIが正しく拾えなかった箇所は人が補い、AIの捉え方が良かった部分は積極的に採用します。 ドキュメントを「AIと人の両方が読めるもの」として整えておくことは、実装フェーズで特に効果的でした。タスク分解書ではレビューしやすい単位を1つのタスクとして切り出しており、おおむね「1タスク = 1PR」の粒度になっています。実装フェーズではこのタスクごとにIssueを作成し、Issueの内容を参照してAIへ実装を進めてもらう運用としました。Issueには対応するタスクの記述を書き、必要に応じて設計書の該当箇所も転記しておくことで、AIは「どこから手をつけるべきか」「どこまでがスコープか」を迷わず進められるようになりました。 設計書を書く工数自体は決して小さくありませんが、実装フェーズに入ってからの迷いを減らすことができました。 UIから先に組み立て、動かしながら段階的に進める ── ハードコードで動く画面を先に作る 設計書を書ききった後、いよいよ実装フェーズに入ります。ここでは動作確認がしやすいように工夫して進めました。 なぜUIから先に組み立てるのか レガシーなFragmentをモダンな構造に置き換えるとき、素直にやろうとすると「UIも、状態管理も、データ取得も、イベント処理も全部一度に入れ替える」ことになります。これは変更差分が大きく、レビューと動作確認の両面で辛い状態を生みます。途中でバグが出ても、UI側のミスかロジック側のミスかが切り分けづらくなります。 そこで私たちは、まずUI部分だけをComposableとして組み立てるところから始めました。ViewModelやRepositoryの繋ぎ込みは後回しにして、ハードコードした仮のデータを渡して動く画面を先に作る進め方です。Composable関数は引数に渡されるデータの型さえ揃っていれば描画できるため、ロジックが未実装でもUIレイアウトやスクロール挙動などはすべて確認できます。 フェーズ分割と段階的接続 タスク分解書では実装フェーズを段階的に分けていました。順序はおおむね次のとおりです。 フェーズ1 :空のFragmentとComposable Screenの土台を作る フェーズ2–4 :UIを仮データで組み立てる フェーズ5 :ViewData/UiState/Repository/ViewModelの骨格を作る フェーズ6 :機能ごとに「ロジック実装 + UI接続 + Unit Test」を1セットで進める フェーズ7 :統合テスト・動作確認 各フェーズで意識したのは常にアプリが動く状態を保つことです。UIを組み立てている段階でも仮データを渡してアプリを起動すれば、実機でUIレイアウトやインタラクションを確認できます。フェーズ6の機能ごとの繋ぎ込みでも、1機能ずつ実データに接続していくため繋ぎ込んだ瞬間に動作確認ができます。 これにより、レビューも1機能単位の小さなPRで進められるようになりました。差分が小さければレビュアーの負担も減ります。次の章で扱うアーキテクチャの整理と合わせて「変更影響を局所化したまま、確実に進める」工程設計が成立しました。 リファクタリング後のアーキテクチャ 2つのアプローチを経て、対象画面はMVVMベースの構造に生まれ変わりました。Fragment内に集中していた検索ロジックやデータ取得処理はRepositoryに移しています。Fragmentフィールドに散在していた状態と、DomainモデルからViewDataへの変換はViewModelが担います。UI(Composable)はViewModelが公開するUiStateをStateFlowとして購読するだけになり、データの流れが一方向に整理されました。Beforeと比べて状態の流れが追いやすく、責務の切り分けも明確になっています。 責務を分散させるにあたって、特に型安全性を取り戻すことを意識した設計判断が2つあります。いずれもBeforeで挙げた「Object型のリストに見出しとアイテムが混在」「状態がFragmentフィールドに散在」という課題への直接的な解です。 sealed interfaceで画面状態を表現する 画面全体の状態は、 sealed interface を使ってContent/TextSearch/Errorの3つに分けました。構造を抽象化して示すと次のとおりです。 sealed interface ScreenUiState { val selectedFilter: FilterViewData data class Content( override val selectedFilter: FilterViewData, val items: List <ListItemViewData>, val sortType: SortType, ) : ScreenUiState data class TextSearch( override val selectedFilter: FilterViewData, val searchText: String , val items: List <ListItemViewData>, ) : ScreenUiState data class Error ( override val selectedFilter: FilterViewData, val errorMessage: String , ) : ScreenUiState } 旧実装では searchText 、 sortType 、 selectedGender のような画面の状態がFragmentフィールドに散在していました。これらを1つのUiStateに集約することで、状態の遷移はStateFlowを流れるUiStateの差し替えだけで表現できます。 具体的には、Content(テキスト絞り込みなしの一覧表示)・TextSearch(テキスト絞り込み中の一覧表示)・Error(エラーダイアログ)の3状態に分けています。 when 式での網羅性チェックも効くため、状態の追加や変更にも強い構造になりました。 sealed interfaceでリストアイテムを型安全に表現する 見出しとアイテムが混在していたリストには sealed interface で型を分けて表現する方法を導入しました。 sealed interface ListItemViewData { data class Section(...) : ListItemViewData data class Item(...) : ListItemViewData } これにより、LazyColumnの items などでリスト要素を扱うコードは when 式で分岐するだけで網羅的に処理できます。キャストもなく、見出しとアイテムの取り違いもコンパイル時に検出されます。Object型のリストでキャストを繰り返していたBeforeと比べて、型システムが安全に守ってくれる範囲がぐっと広がりました。 AI活用Tips 最後に、このリファクタリングを通して得られたAI活用のTipsをいくつか共有します。 Tips 1:タスク分解書を「AIへの依頼の共通言語」にする 設計書とタスク分解書は、実装フェーズで「AIへの依頼の共通言語」として活用しました。私たちはタスク分解書の1タスクごとにIssueを作成し、Issueに対応するタスクの記述を書く運用にしました。必要に応じて設計書の該当箇所も転記しておくことで、コードベースだけでは判断しづらい「どこから手をつけるべきか」が伝わり、AIの出力は私たちの期待に近づきやすくなりました。 Tips 2:設計ドキュメント自体もGit管理する 実装を進めていると、「設計変更が必要になる」場面は必ず出てきます。たとえば今回も、当初はUiStateをContent/Empty/Errorの3状態で設計していました。しかし実装中にEmptyを独立させるよりテキスト絞り込み中の状態として表現する方が適切と判断し、TextSearchへ置き換えました。他にもタスク分解の途中で機能の依存関係を見直したり、Repository層に切り出す責務の境界を再定義したりと、設計書を書き換える場面が複数回ありました。 私たちは設計書やタスク分解書をマークダウンとしてリポジトリに置き、Gitで履歴を追えるようにしています。変更があった際はコミットを切り、なぜ変えたかをメッセージに書き残します。これによりAIが設計書を参照するときも常に最新の状態を渡せますし、レビュアーも「設計と実装の差分」を追いやすくなりました。同じような取り組みをするチームには、設計書を書いた段階から「変更を前提にドキュメントを置く場所と更新ルールを決めておく」ことをおすすめします。 Tips 3:レビューで得た知見を「AI参照用」のドキュメントに蓄える 個別のレビュー指摘はそのPRで修正して終わりにしてしまいがちです。しかし「次に同じ画面を触るとき」「次にAIに似た実装を依頼するとき」に、また同じ指摘を繰り返さないようにしたいところです。私たちはレビューで頻出した観点やコーディング規約に書ききれていない暗黙のルールを整理し、リポジトリ内のドキュメントとして残しています。たとえば、HiltのスコープごとのDIモジュール構成や、Coroutinesでのスレッド切り替えを抽象化するためのプロジェクト独自ルールなどです。Android開発ではフレームワークやプロジェクト固有の制約が多く、コードを読むだけでは伝わりにくい暗黙のルールが意外と多くあります。AIへの実装依頼時にこれを参照させることでPRの品質が安定し、レビューの往復回数も減っていきました。 Tips 4:AIが苦手なところを人が補う AIに任せれば実装まで一気通貫で終わる、というほど単純ではありませんでした。仕様調査や設計書の雛形作成、機能単位のIssueから始まる定型的な実装はAIに頼りやすい一方で、人の判断が必要だった場面もいくつかあります。 たとえば、複数画面にわたる責務の整理、Repository層に切り出す境界の判断、実装方針のトレードオフを取る場面などはAIに丸投げするとブレやすい部分です。これらは対象の画面以外の前提知識やプロジェクトの方向性を踏まえた判断が必要なためです。こうした判断には、人がドラフトを書いて設計書に落とし込み、それをAIに参照させて実装してもらうというハイブリッドな進め方が結果的に効率的でした。 まとめ 本記事では、共通基底クラスを継承したレガシーな画面をKotlin + Jetpack Compose + MVVMへ移行する際に採用した2つのアプローチを紹介しました。1つは実装より先に設計書を書ききること、もう1つはUIを先にComposeへ移行し動かしながら段階的に進めることです。 これらを組み合わせて進めた結果、状態管理とリスト表現の型安全性も取り戻し、Fragmentに貼り付いていたロジックをRepositoryとViewModelへ整理できました。 リファクタリング前は対象画面のロジックがFragment内に閉じており、ユニットテストが書きづらい構造でした。リファクタリング後はViewModelやRepositoryの単位で70件以上のユニットテストを実装できる構造になり、その後の関連機能の追加・修正もユニットテストで検証しながら進められました。 レガシーなAndroidViewのCompose化や肥大化した基底クラスの解体と向き合っているチームの参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
















