やりたかったこと
2つの似たようなアプリを作りたい。
1つのデータソースに対して、利用者側と運営者側のアプリがあるイメージです。
全体的な機能・画面構成は異なるため、1つのコードベースで2つのアプリを出力する(内部に条件分岐などを入れる)では辛いと考えました。
ただし、API通信基盤やUIパーツ、一部の画面は共通化できそうだったので、それをどのように管理・更新していくかが課題となりました。
先に結論
common用のリポジトリを作成し、共通化できる箇所はその中に実装しました。
各アプリリポジトリでは、 Git submodule で packages
ディレクトリにそれを読み込むこととしました。
それを、 pubspec.yaml
にて相対パスで読み込みました。
(A, Bという2つのアプリと、commonリポジトリがあるイメージ)
良かった点
- 共通化することで、不具合が発生した場合に、1箇所の修正で済んだ。
- ただし、各アプリリポジトリ側で common のコミットハッシュを更新する必要がある。(これは、「取り込みタイミングを任意にできる」という利点でもある)
- GitHubのPR上で、「どのコミットハッシュに書き換えられたか」が可視化されていた。
- それぞれのアプリ、共通部分の履歴(コミット・PR・tagなど)を分離できるため、それぞれのアプリライフサイクルを個別に管理できる。
- CIを分離しやすかった。(共通部分の修正の場合には、共通部分のCIのみ実行するなど)
悪かった点
- 修正に手間が増えた。それにより、「commonリポジトリを修正せずに問題を解決する方法」を探す力学が働きやすくなった。
- commonリポジトリの修正 -> 各リポジトリでsubmoduleのcommit hashの更新 -> 利用箇所の修正の手順が必要。
- また、各リポジトリで修正をマージする前にレビューも行ったため、さらに手間が大きかった。(commonのfeatureブランチを指している状態 -> commonのmainブランチに取り込まれたコミットを指している状態 -> マージ可能な状態という手順が必要。)
- 1つの横断的な修正に対して、PRが最大3リポジトリに分離されるため、人の手でリンクさせてやる必要がある。(PRのdescriptionにURLを記載するなど)
- pull したあとに
git submodule update --init
する必要がある。 - 今回は、1つのアプリ実装後に慌ててcommonリポジトリを作成したため、共通化部分の精査が甘かった。「何を共通化するべきか」は未来を考える必要もあるので難易度が高かった。
- また、あとになって「これも入れておけば」とか発生しても、3リポジトリを修正する必要があり、腰が重くなってしまった。
- 最新を指しているのかを差分から判断しづらい(PR上で見えるのはコミットハッシュなので)。
- 初学者などがチームに混ざっていると、気をつけてレビューしないと巻き戻ってしまったりする。
他に考えた案
案1: Flutter packageとして定義する
共通部分を1つのgitリポジトリとして作成しておき、 pubspec.yaml
にて git
を利用して指定する。
参考: https://dart.dev/tools/pub/dependencies#git-packages
メリット
-
ref
を使って"バージョン番号"で管理できるため、最新かどうかを判断しやすい。 - 他のライブラリなどと同様に扱うことができる。
デメリット
- バージョン番号の管理が面倒。
- 複数人で更新する場合には、バージョン番号の衝突を避けるようなフローが必要。
- パッケージをプライベートにするのは少し大変(CI上でトークンの管理したり?未確認。)
案2: 1つのリポジトリにまとめる
リポジトリルートに、それぞれのアプリディレクトリとcommon部分のディレクトリの計3つを作成する。
それぞれのアプリでは、 pubspec.yaml
に相対パスでcommonパッケージを指定する。
参考: https://dart.dev/tools/pub/dependencies#path-packages
メリット
- 比較的わかりやすい。
- 1つのリポジトリをcloneするのみで、特別な操作なく開発を始めることができる。
- 1つのPRに、すべての変更を含めることができる。
デメリット
- コミット履歴・PR・タグなどが2アプリ分混在してしまい、単一アプリに対する変更が追いづらくなる。
- CIの設定が複雑化する(やり方を工夫したり、CIの設定を複雑化させると対応は可能だと思われる)
- アプリ毎のルートが1つ下のディレクトリになるため、
working_directory
などを指定する必要がある。 - どちらのリポジトリに対する変更可を判断してビルドを開始するなど、ビルド時間短縮のための設定が追加で必要になる。(もしくは、富豪的に解決するか)
- アプリ毎のルートが1つ下のディレクトリになるため、
- アプリ単位でのアクセスコントロールが不可能。
- リポジトリが別れていれば、「Aリポジトリは書き込みできるが、Bは読み込みのみ」などがGitHubの機能で実現可能。
案3: 共通化を諦めてファイルコピー
リポジトリを2つ作成し、それぞれの物理的な依存関係をなくす。
共通にできそうな部分であっても、それぞれのリポジトリのコードとして管理する。
メリット
- 一番わかりやすい
- それぞれのアプリで、個別の要件が発生した場合にも、柔軟に対応できる。
デメリット
- 重複コードが大量に発生し、メンテナンスが大変になる。
- 2アプリ間で同じように見えるコードであっても、差が無いかをしっかり確認して修正する必要がある。
まとめ
先にも書いたように、今回は git submodule + pubspec.yaml
による相対パス指定を選択しました。
ただし、共通化を考えきれなかった部分では、案3のような状態になってしまいました。
仕様やデザインレベルで、「何が共通的で、何が個別のものか」を、プロダクトオーナー・デザイナー・エンジニアが同じ認識を持ち、それをコードに反映されているのが、あるべき姿のように感じました。(時間かかるし、難しいですが)