
はじめに
こんにちは!技術本部 Sansan Engineering Unit Mobile Applicationグループの桑原です。
このたび、Mobileチームでは「技術負債返済」をテーマとしたTech Blogリレー企画を開始することになりました。
本記事はその第一弾として、Epoxy削除プロジェクトを取り上げます。今後も隔週でMobileチームから技術負債返済に関する記事を投稿予定ですので、ぜひご期待ください。
さて、技術負債、溜まっていませんか?「いつかやろう」「時間ができたらやろう」と思いながら、気づけば半年、1年と放置してしまう。そんな経験をお持ちの方も多いのではないでしょうか。
本記事では、私たちのチームが長らく停滞していた技術負債返済プロジェクトを、わずか3カ月で完遂した軌跡をご紹介します。 同じような状況でお悩みの方の参考になれば幸いです。
技術負債の概要
立ちはだかるEpoxy
まず、今回取り組んだ技術負債について説明します。私たちのAndroidアプリでは、リスト画面の実装にEpoxyというライブラリを長年使用していました。
EpoxyはAirbnbが提供するRecyclerViewを簡単に構築するためのライブラリで、一時期は便利に使っていました。しかし、時代の変化とともに次のような課題が顕著になってきました。
- 技術的制約: DataBindingに依存しており、KSPに対応できない
- 開発体験: 自動生成コードが読みにくく、Jetpack Composeの方が直感的
- メンテナンス性: EpoxyModel、XML、EpoxyControllerの3箇所を改修する必要がある
そして何より、DataBindingがKSP未対応であることが、プロジェクト全体のKSP移行の最大のボトルネックとなっていました。
現状把握の結果:
- EpoxyController: 48個
- EpoxyModel: 1〜12個/Controller
この数字を最初に見た時は絶望しました。多すぎ...

なぜこれまで停滞していたのか
チームとしては「EpoxyをComposeに置き換えていきたい」という気持ちはあったものの、直近2年間における実績はゼロでした。その原因を振り返ると、次のような要因がありました。
1. 優先度の低さ
- Epoxyに大きな技術負債を感じていなかった
- 当時はKotlin2.0安定版がリリースされておらず、KSP移行のタイミングではなかった
- 機能的には問題なく動作しているため、改善の必要性を説明しづらかった
2. 案件スケジュールとの兼ね合い
- 案件見積もり時点で置き換えを考慮に入れておらず、案件内での対応が困難だった
3. 規模の大きさによる心理的障壁
- 紐づくEpoxyModelが多く、一度に進められない
- 使用している画面は古い設計が多く置き換えコスト増大
- 「48個」という数字の重圧
これらの要因により、技術負債返済は常に「やりたいけれど後回し」の状態が続いていました。
最初の挑戦と失敗(2024年11月)
Kotlin2.0リリースによりKSP移行の必要性が高まってきたため、2024年11月、私たちはepoxy-composeライブラリを導入することにしました。EpoxyModel単位でComposeに置き換えを進める戦略で、案件で触る機会が来た時に段階的に移行していこうというアプローチです。 このアプローチについては、私が「現実的なCompose化戦略 ~既存リスト画面の置き換え~」として技術登壇も行い、チーム一丸となって「これからやっていくぞ!」と宣言していました。
しかし、実態はというと、
- 都合よく当該画面を触る案件がなかなか来ない
- 半年間で実績は1画面のみ
という結果で、段階的移行という理想的に見えるアプローチも、狙った通りに機能しませんでした。
仕切り直し(2025年4月)
約5カ月が経過した2025年4月、状況が大きく変化しました。
まず、グループとしてMobileアプリの負債・リスクへの対処に力を入れていく方向性が明確化されました。さらに、Cursorがチームに導入され、実際に使えるようになったことで、AI支援による開発効率の向上を実感し、「これならEpoxy全置き換えが現実的にできるかもしれない」と感じるようになりました。
そこで改めて、本格的なEpoxy削除プロジェクトを立ち上げ、戦略の見直しをしました。
決断1: 完璧(Full Compose)より完了を優先
当初はEpoxy画面をFull Composeに置き換えるソリューションで進めていました。 しかし、AI(Cursor)の支援により実装速度は向上したものの、単純にUI置き換えだけでなく、実際に作業を進めると予想以上に大変なことが分かりました。
- 状態管理の見直し: ComposeのStateless設計に合わせて既存の状態管理を修正
- アーキテクチャ変更: 場合によっては画面全体の設計から見直しが必要
その結果:
- 1画面あたり平均で約30時間の工数
- 全置き換え完了に1年以上かかる見込み
この状況を打開すべく、2025年4月に入社したばかりの石田さん(@maxfie1d)から「AndroidViewBindingを使って、XMLを再利用したままEpoxyを置き換えるのはどうか」という提案が出ました。
AndroidViewBindingアプローチの採用
AndroidViewBindingを使うことで、次のメリットが得られます。
- 状態変更不要: 既存アーキテクチャを変更しないまま実装可能
- UI再構築最小限: EpoxyController(List部分)をComposeにするだけ
- 限定的な変更: DataBindingをViewBindingに置き換えることに集中できる
これらのメリットを検討した結果、私たちは「Full Composeにすることは理想だが、あくまで達成したい目標はEpoxyを排除することであり、今回のマスト要件ではない。それよりもスピードを重視すべき」という結論に至り、AndroidViewBindingアプローチの採用を決定しました。
移行前後の比較:
// 移行前(EpoxyModel) @EpoxyModelClass(layout = R2.layout.call_history_item_person) internal abstract class PersonCallHistoryEpoxyModel : DataBindingEpoxyModel() { @EpoxyAttribute lateinit var personCallHistory: CallHistory.Person @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: CallHistoryClickListener override fun setDataBindingVariables(binding: ViewDataBinding?) { if (binding !is CallHistoryItemPersonBinding) return // DataBinding設定 binding.personCallHistory = personCallHistory binding.callButton.setOnClickListener { clickListener.onCallButtonClicked(personCallHistory.telephoneNumber, personCallHistory.type) } } }
// 移行後(AndroidViewBinding) @Composable internal fun PersonCallHistoryItem( personCallHistory: CallHistory.Person, onCallClicked: () -> Unit, isDividerTopVisible: Boolean, modifier: Modifier = Modifier ) { AndroidViewBinding( factory = CallHistoryItemPersonBinding::inflate, modifier = modifier ) { // xml側で定義していたDataBindingロジックをViewBindingで実装 binding.name.text = personCallHistory.name.toString() binding.companyName.text = personCallHistory.companyName binding.dividerTop.isVisible = isDividerTopVisible // クリックリスナー設定 binding.callButton.setOnClickListener { onCallClicked() } } }
決断2: AI主導開発への転換
AndroidViewBindingアプローチにより置き換え手順が定型化できたことで、私たちはエンジニアがコードを書いてAIが支援する従来のスタイルから、AIが主体となってコードを書き、エンジニアは調整・確認に徹する開発手法に転換しました。
具体的には、詳細な移行ガイドを作ってCursorルールとして読み込ませ、「このEpoxy画面をガイドに従ってAndroidViewBindingを使ったComposableに変換して」といった指示で、コード変換の大部分をAIに任せました。開発者は生成されたコードの調整や動作確認に集中することで、作業効率が大幅に向上しました。
結果と成果
- 全48個のEpoxyController削除完了
- KSP移行の道筋確立
劇的な工数削減
AndroidViewBindingアプローチとAI活用により、次の効率化を実現しました。
- 工数削減: FullComposeにするのに1画面30hと見積もっていたもの → 1画面10h(約67%削減)
- 期間短縮: 1年以上見込み → 3カ月で完了
並列開発体制の構築
定型化により作業の属人性を排除し、次の体制強化を実現しました。
- チーム全体での参加: ほぼ全Androidエンジニアと一部iOSエンジニアもチャレンジとして開発に参加
- スキル制約の解除: 詳細なガイドとAI活用により、Epoxy未経験者でも対応可能に
- 開発リソースの最大化: 複数エンジニアが同時並行で異なる画面を担当
この「誰でも取り組めるようにする」アプローチこそが、大規模な技術負債を一気に解消できた最大の要因でした。
重要なポイントとしては、定型化により作業が単純労働化する前に、短期集中(3カ月)で移行を完了させたことです。これにより、エンジニアが長期間にわたって機械的な作業に従事することを避け、チーム全体のモチベーションを維持しながらプロジェクトを成功に導けました。
移行過程で発生した課題
一方で、AndroidViewBindingを使うことで発生した問題もあったので、ご紹介します。
問題1: 描画されない問題
現象:
- 特定のセクションが初回表示時に表示されないことがある(100%ではないが確率的に発生)
- スクロールして一度画面外に出してから戻ると、正しく表示される
- ViewModelの状態は正しく更新されており、ログでも確認済み
解決策:Composableのkey()による強制再描画で対処
問題2: 無駄な再描画問題
現象:
- リスト表示において、アイテムをスクロールする際のカクつきが発生
- itemごとにユニークkeyを設定しているが、リストアイテムが画面外から画面内に入る際に毎回recomposeされてしまう
解決策:問題部分をFull Composeに置き換えて対処
対処法についての振り返り
AndroidViewBindingは便利なアプローチですが、上記のような不安定な挙動が発生する問題がありました。 結局、ログ出力やissue trackerなどで調査しましたが、直接的な原因は特定できませんでした。
しかし、そもそもAndroidViewBindingは一時的な解決策であり、将来的にはFull Composeにしたいと考えています。 そのため、問題解決に時間をかけるよりも、工数に余裕がある場合は素直にComposeに置き換える方が効率的だと感じました。
プロジェクトを通じて学んだこと
1. 完璧主義の罠を避ける
「理想的な実装」を追求しすぎると、プロジェクトが停滞する原因となります。今回の場合、Full Composeという理想よりも、Epoxy削除という本来の目的を優先したことが成功の鍵でした。
ポイント:
- 本来の目的と理想的な手段を混同しない
- ROI(投資対効果)を明確に意識する
- 段階的な改善を許容する
2. 最初の言語化投資の重要性
機械的な改善作業では、最初は面倒でもルールドキュメントをしっかり作ることで、チーム全体の効率が劇的に向上し、結果としてトータルで早く完了できることを実感しました。
ポイント:
- 最初に言語化をサボらずに行うことで得られる恩恵は大きい
- 初期投資(ドキュメント作成)が後の大幅な効率化につながる
- 個人の属人的な作業から、チーム全体で再現可能な作業への転換
最後に
「いつかやろう」と思っている技術負債がある方は、まずは現状の課題を整理し、実現可能な方法を見つけることから始めてみてはいかがでしょうか。私たちも小さな一歩から始め、試行錯誤を繰り返しながらも、最終的に大きな変化を実現できました。
私たちの経験が、同じ悩みを抱える開発チームの参考になれば幸いです。
本記事は「技術負債返済」をテーマとしたTech Blogリレー企画の第一弾でした。次回は別の技術負債返済事例をご紹介予定です。Mobileチームの技術負債との戦いにご興味をお持ちいただけましたら、ぜひ今後の記事もお楽しみに!