SODA Engineering Blog

Flutterアプリをもっと楽に速く開発したいのでリポジトリ構成に手を入れようとした話

2024/12/20に公開

\スニダンを開発しているSODA inc.の Advent Calendar 2024 20日目の記事です!!!/

はじめに

SODAではここ1,2年の間に新機能開発に携わるFlutterエンジニアが4人から9人へ拡大し、日々機能開発や運用作業に力を注いでいます。私たちは主にSNKRDUNKというアプリを運用しており、実はJP版とGlobal版が存在しています。
これら2つのアプリケーションを運用する中で直面する問題にどのように対処していくか紹介します。

問題の背景

SNKRDUNKは現在2つのリポジトリで機能開発を進めています。
JP版の開発チームの方が規模も大きく開発が先行しているため有用な実装資産は主にJP側のリポジトリに蓄積されるような状態になっています。

機能開発は基本的にチームごとに分かれておりその中で完結させますが、PRレビューに関しては全体へのランダムレビューとしています。リリースタイミングに合わせて各種変更を取り込んだりする必要が少なからずある点と他のチームであっても開発内容や実装スタイルをレビューし合えるようにする点が主な狙いです。

また、リポジトリが分かれているかつ人数も少ないためかGlobal版のPRレビューはJP版と比較して遅い傾向にあります。主に開発で見ているリポジトリが違うということも集団の開発においては壁になっているのではないかと感じています。

今回課題に感じていたこと

開発をしているとすでにある実装は利用したいと誰でも思うはずですが、リポジトリが別々なので共通で利用することはできません。そのため似ているようで少し違うような実装が散見されます。
同様にあるサードパーティのサービスをどちらかで先行して導入してまたどちらかでも導入する場合、共通利用できるようなインターフェースにできたはずがJP/Globalどちらかからしか呼べないような実装になりがちです。

また、Global版の開発は当時私一人で担当していたためどうしてもレビューのリードタイムが伸びてしまう傾向にありこれも地味に困る点でした。
さらには実装に対してほとんどコメントがないような状態でコードを読めば挙動が分かるとはいえヒントは欲しいものです。public_member_api_docs を適用できそうなところには導入したいと密かに適用範囲に当たりをつけているような状態でした。

まとめると下記のような問題点がありました。

  1. JP, Globalのリポジトリで既に実装があるものを互いに再利用できない
    • 主にUIコンポーネントやサードパーティのSDK用実装など
    • 将来的にAPI通信を行うレイヤーもJPとGlobalで共通化されるため共通で利用できる状態にしたい
  2. Globalはレビューリードタイムが遅くなる傾向がありリポジトリを合わせることで一緒に開発している感を高めたい
  3. Linter設定をディレクトリ単位で設定したい

議論したこと

以上の3つの感じていた課題感に対処するためディレクトリ構成の見直しを行い、共通のコンポーネントを運用していくにはどれが最適かADR(Architectural Decision Record)を作成してチームで相談していくことにしました。
(本当は3つ案を用意していましたが書きながらこれはないなとなり、そのままボツにしました

現状のディレクトリ構成

概ねディレクトリ構成は同じで別パッケージでopenapiやその他SDKが入っていたりいなかったりします。SNKRDUNKはレイヤー構成になっており、views には画面やアプリケーション内で利用できるWidgetが入っています。systems にはFirebaseやAdjust等のサードパーティ用のClientクラスがあったり例えば別のアプリケーションでも利用できそうな処理系のクラス等が存在しています。

JP

.github/                  # JPのCI設定
lib/
│
├── controllers/          # Provider(Notifier)
├── models/               # Repository, DTO, Entity
├── systems/              # Firebase, Adjust, etc
├── views/                # Screen, components
│
openapi/                  # JP OpenAPI client
│
snkrdunk_design/          # JP Figma コンポーネント
│
test/                     # テストファイル

Global

.github/                  # GlobalのCI設定
lib/
│
├── controllers/          # Provider(Notifier)
├── models/               # Repository, DTO, Entity
├── systems/              # Firebase, Adjust, etc
├── views/                # Screen, components
│
openapi/                  # Global OpenAPI client
│
test/                     # Test Files

案1 Global版のコードをJP側に全て移動してマルチパッケージ構成にする。

新しくpackagesディレクトリを追加しその中に全て入れ込んでパッケージとして管理する案です。
packages配下にアプリケーションと共通のモジュールを配置することで依存関係を管理します。
新しくパッケージを追加したい場合も容易ですが、コードの調査を行う場合に2つのアプリケーションロジックが引っかかってしまう可能性がある点や配信設定・フローなどに影響出そうな点が課題になりそうだという話が出ました。

.github/                       # JP/Global共通のCI設定
packages/
│
├── app_jp/                    # JPアプリケーション
│   ├── lib/
│   │     ├── controllers/     # Provider
│   │     ├── systems/         # アプリケーション固有なもの
│   │     └── views/           # Screen, components
│   └── test/
│   └── analysis_options.yaml  # アプリケーション用Linter
│
├── app_en/                    # Globalアプリケーション
│   ├── lib/
│   │     ├── controllers/     # Provider
│   │     ├── systems/         # アプリケーション固有なもの
│   │     └── views/           # Screen, components
│   └── test/
│   └── analysis_options.yaml  # アプリケーション用Linter
│
├── snkrdunk_jp_models/        # JPのmodel層(Repository, DTO, Entity)
├── snkrdunk_en_models/        # Globalのmodel層(Repository, DTO, Entity)
├── snkrdunk_view_component/   # JP/Global共通のview層
├── snkrdunk_system_component/ # JP/Global共通のsystem層
├── snkrdunk_model_component/  # JP/Global共通のmodel層(バックエンドがJP/Global共通基盤になった後を想定)
│
├── openapi_jp/                # JPのAPIクライアント
│   └── analysis_options.yaml 
│
├── openapi_en/                # GlobalのAPIクライアント
│   └── analysis_options.yaml
│
├── snkrdunk_design/           # JPのFigmaデザインコンポーネント
│   └── analysis_options.yaml
│
├── snkrdunk_design_en/        # GlobalのFigmaデザインコンポーネント集として進める場合はこのように追加する想定
│   └── analysis_options.yaml
│
├── melos.yaml                 # マルチパッケージとして設定、ビルドスクリプト
│
└── pubspec.yaml               # melos 依存のみ

補足

各パッケージに配置する analysis_options はアプリケーションパッケージとライブラリパッケージで2種類のLinterを設定する。
- ライブラリには public_member_api_docs を設定して実装の仕様化を目指す。
- アプリケーションには public_member_api_docs は設定せず、現状の analysis_options をそのまま流用する。
- 但し、openapiなどの自動生成ライブラリについてはこのルールを適用しない。

snkrdunk_view_component にはJP/Globalで共通で利用ができるようなレイアウトに関連する実装であったり、UI Widgetなどを配置する。
- snkrdunkのアプリケーションロジックに依存せずどこでも使える実装を目指す。
- snkrdunk_system_component にはJP/Globalで共通で利用ができるsnkrdunkアプリケーションに依存しないサードパーティライブラリのインターフェースなどを配置する。
- 例えば analyticsの中でも Firebase Analytics, Adjustや独自でネイティブSDKと繋ぎ込みをしているような実装

メリット

  • JPかGlobalで一度実装されたものは簡単に別のアプリケーションから利用ができる状態になる
  • packageの責務範囲を明確にしpackage間の依存関係の制約を強制することができる
  • analysis_optionsをパッケージ単位で設定できる
  • CIワークフローの設定も共通で使えるものが増える

デメリット

  • 移行するための変更コストがかかる
    • melosのマルチパッケージ化のコスト
    • CI/CD設定の更新
    • 配信ワークフローの更新
  • Git履歴が遡れない
    • Globalはリポジトリを残しておけば移行時点までの履歴は追える
    • 移行時点のコミットを残しておけばある時点までの履歴は遡れる(JP
  • melosに強く依存した構成になる
  • 第3のアプリケーションを作成するときに同じリポジトリに入れないと再利用ができない

案2 共通パッケージ群をさらに別リポジトリで管理する

共通で使える実装を別のプライベートリポジトリに切り出し管理する案です。
プライベートな実装を切り出すので必然的にプラベートリポジトリへの移行を前提にしていますが、扱いやすさでいえばパブリックにしても良いのではないかとすら思ってはいます。

packages/
│
├── snkrdunk_view_component/   # JP/Global共通のview層
│
├── snkrdunk_system_component/ # JP/Global共通のsystem層
│
├── snkrdunk_model_component/  # JP/Global共通のmodel層
│
├── openapi/                   # JP/Global共通のAPIクライアント
│   └── analysis_options.yaml. 
│
├── snkrdunk_design/           # JPのFigmaデザインコンポーネント
│   └── analysis_options.yaml
│
├── snkrdunk_design_en/        # GlobalのFigmaデザインコンポーネント
│   └── analysis_options.yaml
│
├── some_native_sdk_client/    # Flutter用のSDK Clientを用意したい場合
│
├── melos.yaml                 # マルチパッケージとして設定、ビルドスクリプト
│
└── pubspec.yaml               # melos 依存のみ

補足

  • JP/Globalアプリケーションで共通利用することができるものだけパッケージとして別リポジトリで管理する
    • openapiディレクトリは現状だとアプリケーション固有になるため現状のアプリケーションリポジトリ(snkrdunk_flutter)にそのまま配置する
      • 将来的にJP Global共通で利用できるAPIが誕生したらpackagesディレクトリで管理する

メリット

  • JP/Globalで共通利用できる実装はpackagesとして管理できる
  • analysis_optionsをパッケージ単位で設定できる
  • 外に切り出すだけなので失敗してもリカバリーが簡単

デメリット

  • 別のPrivateリポジトリに依存することでビルド環境が複雑化する
  • melosの学習コスト
  • melosに強く依存した構成になる

比較

比較するとこのような評価でしょうか。
再利用できる実装を用意する目的としては案2の方がCIなどへの影響範囲も限定的だと思われますし、修正コストも2の方が手軽そうという結果となりました。

再利用性 依存関係の制約 Linterの柔軟性 CIの重複排除 移行コスト サードパーティ依存
案1 ⚪︎ ⚪︎ ⚪︎
案2 ⚪︎ ⚪︎ ⚪︎ ⚪︎

また、レビューのリードタイムに関しては現在ではチーム内のメンバーも増えたこともあり基本的にレビューはチーム内で完結できるようなアサインルールに変更して対応しています。1ヶ月くらいを目安にPRのリードタイムなどをモニタリングしその時の状況に応じた課題に対して相談をしています。

まとめ

SODAではアーキテクチャに関わるような課題であれば各自がADRを作成し、チーム内で相談し意思決定をしていくような開発を取っています。それぞれがオーナーシップを持ち今ある情報を元に判断をしていきます。

今回は2つのアプリケーションを運用する中で共通な実装はパッケージ化して別のプライベートリポジトリで参照するという構成にシフトを始めています。プライベートなリポジトリにpub getするのは少し設定が必要なのでその辺りの詳細な設定方法も次回紹介したいと思います。

SODAの開発に興味を持ってくれた方はぜひ採用ページも覗いてみてください!
https://recruit.soda-inc.jp/

SODA Engineering Blog
SODA Engineering Blog

Discussion