- 1. はじめに
- 2. UDFをどう実装に落とすか
- 3. 候補を学び、Reduxの文脈を取り入れた理由
- 4. 独自に定義したUDFアーキテクチャ
- 5. UseCaseDispatcherで役割分担を明確にする
- 6. 既存(国内版食べログアプリ)アーキ文脈を残した理由
- 7. 設計の結果として得られたもの
- 8. まとめ
1. はじめに
基盤チームで環境整備や開発基盤のアップデートを担当している須賀です。今回は新規開発で多言語版食べログアプリの開発に関わったときの話としてまとめます。
新規アプリの立ち上げで、アーキテクチャ設計を改めて考える必要がありました。昨年の6月〜10月の期間で、エンジニア4名体制での立ち上げでした。スケジュールを逆算すると、設計は早い段階で固める必要がありました。新規立ち上げで前提が揃っていない分、合意形成を早期に進めること自体が設計の一部でした。
Jetpack Composeを採用するにあたり、Androidの公式では「UDF(単方向データフロー)」を推奨していますが、具体的な構成は提示されていません。UDFを守るのは前提でも、「どう守るか」は各チームの文脈で決める必要があります。
公式ドキュメントでもUDFについて触れられています。
「単方向データフロー(UDF)は、状態が下方に流れ、イベントが上方に流れる設計パターンです。単方向データフローに従うことで、UI に状態を表示するコンポーザブルを、アプリ内で状態を保存および変更する部分から分離できます。」
この状況で私たちがやったのは、複数のアーキテクチャを学び直し、比較した上で「自分たちの文脈で説明できる形」を選ぶことでした。結果として、ViewModelを中心に据えつつ、状態管理の部品としてReducer/Actionを取り入れ、UseCaseDispatcherで責務を分離する構成に落ち着きました。表現としては「Android MVVM(MVI)+Redux」に近い形ですが、本稿では便宜上「独自に定義したUDFアーキテクチャ」と呼びます。
本記事では、「どう決めたか」を紹介します。どこを守り、どこで現実的な選択をしたのか。同じように迷っている方の材料になれば嬉しいです。
2. UDFをどう実装に落とすか
UDFは「守るべき指針」であって、「構成の正解」そのものではありません。公式がUDFを推奨しているのは、状態とイベントの流れを明確にすることで、UIと状態管理を分離できるからです。一方で、公式は特定のアーキテクチャを強制しません。ここがこの話の出発点です。
UDFは目的で、構成は手段です。UDFを守るための具体的な構成は、チームの経験・既存資産・運用のしやすさといった文脈で決める必要があります。
今回は、説明コスト、学習コスト、運用コストを判断軸にしました。ここがずれると、「UDFっぽいけど運用が苦しい」「設計が説明できない」といった状態になりがちです。
UDFという指針を満たすことを前提にしつつ、構成は自分たちの文脈で説明できる形に寄せる、という方針で進めました。以降は、その判断の具体的な中身を順に書いていきます。
3. 候補を学び、Reduxの文脈を取り入れた理由
最初にやったのは、Composeで自由に設計してみることでした。一度、自分たちなりの構成を組んでから学習を進めると、結局はMVIに近い構造に寄っていることに気づきました。つまり、自由設計→学び直し→共通文脈に寄せる、という順序で整理したのが今回の進め方です。ならば、独自設計で説明コストを上げるよりも、世の中で共有されているアーキテクチャの文脈に寄せた方が、学習や説明がしやすいと判断しました。
その過程で、完全Redux/完全MVI/既存(国内アプリ)設計の流用/自作設計も検討しましたが、以下の理由で見送りました。
- 学習と説明のコストが上がる
- 運用の複雑さが増える
- 既存設計はMVPかつCompose/UDF前提ではないため、そのまま流用はできない
そこで取り入れたのがReduxの文脈です。「Reduxを全部入れる」というより、UDFを実装に落とし込むための枠組みとして、Reducer/Actionという概念を借りる方針にしました。Reducerは「状態 + Action → 新しい状態」という形で書けるため、SSOTを維持しながら状態変化を整理しやすくなります。UIイベントからの状態更新はReducerに集約する前提で整理しました。
もう一つの理由は、チーム内外への説明のしやすさです。「Reducer/Action」という単語は設計思想のある読者に伝わりやすく、共通言語として機能します。結果として、状態管理の筋を通しつつ、設計意図を共有しやすい形にできました。
4. 独自に定義したUDFアーキテクチャ
Androidでは、UIイベントの受け口や状態保持の責務をViewModelに集約する設計が一般的です。一方で、ComposeのUIは状態駆動であり、UIとイベントの流れを明確にするという意味ではMVI的な構造に寄るのが自然でした。そこで、ViewModelを前提にしつつ、状態更新はReducer/Actionで整理する構成にしました。
結果として、UDFの流れは守りながら、Androidの文脈に馴染む構成にできました。
5. UseCaseDispatcherで役割分担を明確にする
設計上もう一つ重要だったのが、ViewModelの責務を分離し、UseCaseをまとめて扱う必要がある場合に対応できる構成にすることです。ViewModelで全ての処理をやると肥大化するため、「UseCaseDispatcher」を置き、ViewModelは受け口とルーティングに限定する方針にしました。
役割が増える分、初期の学習コストが上がりやすくなります。そのため、責務の境界を明確にすることを意識しました。
具体的には、UIイベントのハンドリング自体はViewModelで行い、UseCase実行が必要な場合にUseCaseDispatcherへ処理を委譲します。UseCaseDispatcherは複数のUseCaseを束ねて結果をActionとして返す役割を担います。Actionで成功・失敗の状態も統一することで、状態遷移はReducerに一本化できます。
結果として、ViewModelは「受け口とルーティング」に専念でき、責務が明確になりました。ViewModelが痩せることで、画面が増えても責務の境界を保ちやすい構成になりました。
以下は構成のイメージです(クラス名は抽象化しています)。

6. 既存(国内版食べログアプリ)アーキ文脈を残した理由
新規アプリだからといって、既存の設計資産をすべて捨てる必要はないと考えました。国内版食べログアプリはLayered ArchitectureかつMVPで、UIの文脈は異なりますが、UseCase/Repositoryの分離は一般的で有効なパターンであり、Compose向けの最適化とは別軸の話だと捉えています。実際、既存のUseCaseやRepositoryの設計は参考にしています。
実務的には、学習コストの低下とチーム内の共通理解が大きな理由でした。既存の文脈に合わせた方が説明は通りやすく、レビューやオンボーディングの負担を下げやすいと考えました。さらに、国内アプリと多言語版で設計思想を揃えることで、プロジェクト横断の知見共有もしやすいと感じています。
この選択は妥協ではなく、既存資産を戦略的に活かすための判断です。UDFの考え方は新しく取り入れつつ、既存のレイヤー構成は活かすというバランスに落ち着きました。ここが「新しい技術を採用しつつ、チームとして運用しやすい形」に落とし込むうえで重要だった点です。
レイヤー構成の全体像は以下のようなイメージです(名称は抽象化)。

7. 設計の結果として得られたもの
この構成で、UI/ViewModel/Reducerの役割分離は明確になりました。ViewModelが受け口とルーティングに専念する構成にしたことで、責務の境界が保ちやすくなっています。実装面では、国内アプリで肥大化しやすかった部分を整理でき、Reducerのテストもシンプルに書けました。
一方で、課題もあります。NavigationComposeを踏まえた設計の検討時間をもっと取るべきでした。実装スタイルに人による差が出ている部分があり、インターフェース設計の厳密さや柔軟性のバランスを事前にもっと詰めておくべきだったと感じています。変更の影響範囲やレビュー時の共有についても、引き続き検証が必要です。
8. まとめ
UDFの考え方を守りつつ、Androidで説明しやすく運用しやすい形として、独自に定義したUDFアーキテクチャに落ち着きました。
自由に設計してみたうえで学び直し、MVIに寄っている事実を確認し、Reduxの文脈(Reducer/Action)を取り入れ、UseCaseDispatcherで役割分担を明確にする、という流れです。合わせて、国内アプリのレイヤー構成も活かしました。 判断について補足すると、「なぜReduxの文脈を取り入れたか」「UseCaseDispatcherをなぜ置くか」といった意思決定はADR(Architecture Decision Record)で残しました。例えば以下のような形で記録しています。

この構成で、設計意図が伝わりやすくなり、チームで運用しやすい形にできました。特別な正解ではありませんが、一つのケースとして置いておきます。
最後に
食べログではエンジニアを募集しています!
もし、食べログにご興味を持っていただけた方は是非下記の採用情報ページをチェックしてみて下さい!