Flutter add-to-app移行のためのチームの組織づくり
チーム構成、採用かスキルアップか、プラットフォームチーム、そしてネイティブとFlutterの境界のオーナーシップ。
add-to-appの期間中にチームをどう組織するかは、その後のほぼすべてに影響します。リリースの速さ、境界コードの健全さ、共有基盤のオーナーは誰か、そして移行が最後までやり遂げられるかどうかです。この章では、私たちがうまくいくのを見てきた体制と、反対したい選択肢をいくつか紹介します。
PoCフェーズ
最初のFlutter画面を既存のネイティブチームに任せたくなるのは自然なことです。彼らはすでにアプリ、ビルドパイプライン、各種連携を熟知しています。問題は、add-to-appで最も難しいアーキテクチャ上の判断が、最初の数週間で下されることです。どのエンジンモデルを使うか、どのナビゲーションパターンにするか、コントラクトの形式をどうするか、状態が境界をどう流れるか、といった判断です。Flutterの経験がないネイティブ開発者は、その時点で当たり前に見えるものを選びがちで、そうした選択はプロジェクトがスケールしたときに高くつくことがあります。作業の大部分をネイティブ開発者が担う場合でも、PoCには少なくとも1人のシニアFlutterエンジニアを参加させることをおすすめします。社内の人材でも、パートナー企業からでも構いません。
こうした初期の判断は、そのまま定着しがちです。私たちは実際にそれを目にしてきました。本格的な移行の前に捨てるはずだったPoCが実際の出発点になり、その数週間に組み込まれた急ごしらえの判断やアンチパターンが、参加メンバーが増えるにつれてコードベース全体にコピーされていくのです。誰かが当初の選択を見直す余裕を持てる頃には、それはすでに標準的なやり方として扱われています。実際のところ、PoCのために迎えるシニアFlutterエンジニアは、組織全体が受け継ぐことになるアーキテクチャ上の判断を下しているのです。
プラットフォームチーム
PoCを終え、大規模に(複数のプロダクトチームで)運用する段階に入ったら、Flutterの、あるいはネイティブとFlutter合同のプラットフォームチームを立ち上げることを強くおすすめします。 add-to-appでは、多くの共有基盤が生まれます。エンジンのライフサイクル管理、ナビゲーションライブラリ、状態共有レイヤー、コントラクトの規約、ビルドのセットアップ、そしてそれらすべてに関するドキュメントです。各プロダクトチームがそれぞれの担当部分を個別に持つと、各パターンについて微妙に異なるバージョンがいくつもできてしまいがちです。フレームワークを担う小さなプラットフォームチームがあれば、パターンの一貫性が保たれ、プロダクトチームのオンボーディングが速くなり、アプリ全体にまたがるエンジン管理の判断を一か所で下せるようになります。
iOS/Android開発者のスキルアップか、Flutterスペシャリストの採用か
採用かスキルアップかの判断は、上記のチーム構成と相互に影響し合い、移行の長期的なコストを左右します。判断を誤ると、締め切りに追われながらFlutterを学ぼうとするネイティブチームを疲弊させるか、アプリを理解していないFlutterチームを長い時間をかけて作ることになります。私たちが見てきたパターンは3つです。
- ネイティブチームのスキルアップ - iOS開発者(Swift、Objective-C)とAndroid開発者(Kotlin、Java)がFlutterを学びます。採用コストは最も低いものの、学習曲線は最も急です。スケジュールに余裕があり、チームのキャパシティがある場合に最もうまくいきます。
- スキルアップに加え、少なくとも1人のシニアFlutterエンジニアの採用(またはパートナー) - 私たちが推奨するデフォルトです。ネイティブ開発者が慣れていく間、シニアエンジニアがアーキテクチャの軸となり、既存アプリに関する深い知識も失われません。ネイティブチームがFlutterに早く慣れるよう、社内ワークショップを継続的に開催することも強くおすすめします。
- 採用のみ - 立ち上げは最も速いものの、アプリについての前提知識がまったくないチームを迎えることになります。共通理解に大きく投資しない限り、その影響は境界コードに表れがちです。
境界コードのオーナーは誰か
境界コード、つまり共有の認証レイヤー、境界コントラクト(Pigeonの定義、またはInlayの場合はinlay_genのスキーマ)、既存のネイティブモジュールへのバインディングは、部門をまたぐ摩擦の大半が生じる場所です。コントラクトはFlutterのニーズに合わせて設計できますが、既存のネイティブ基盤に依存しがちで、その基盤はFlutter側がどう使っているかを知らないネイティブチームによって変更され続けます。逆方向のことも起こります。Flutterチームが自分たちの都合に合わせてネイティブコードを少し手直しし、ネイティブ側の何かを壊してしまうのです。いずれにしても、一方の小さなミスが、もう一方でランタイムのクラッシュとして表面化します。私たちの経験では、このコードのオーナーが誰かによって、境界が長期的にどれだけ健全に保たれるかがほぼ決まります。
境界には単一のオーナー、実際にはプラットフォームチームを置くことをおすすめします。よくある間違いは、境界コードを各プロダクトチームが必要に応じて編集する共有スペースとして扱うことです。誰もがコントラクトを触れるのに誰もオーナーシップを持たない状態では、規約は少しずつずれ、言語をまたいだ破損が日常茶飯事になります。フレームワークの基盤を担うプラットフォームチームは、コントラクトの置き場所として自然です。すでに規約を定めており、すべての利用者を把握し、変更が両側に与える影響をレビューできるからです。
とはいえ、プラットフォームチームがすべてのコントラクトを書くわけではありません。うまくいく役割分担は次のとおりです。
- プラットフォームチームは、コントラクトの形式と共有レイヤーを担当します。境界のスキーマ(Pigeonまたは
inlay_gen)、コード生成のセットアップ、認証、状態共有の基盤です。これらへの変更はプラットフォームチームを通します。 - プロダクトチームは、自分たちの画面と、境界のうち自分たちの担当部分を行き来する機能固有のデータを担当し、コントラクトの追加はプラットフォームチームのレビューを通じて提案します。
- ネイティブチームは、Flutterが依存している共有ネイティブ基盤を変更する際に、黙って変えるのではなく事前に知らせます。
境界をまたぐ変更には、必ず両側のレビューを必須にしてください。 コントラクトの変更や、コントラクトが依存するネイティブ基盤の変更を、ネイティブ側だけ、あるいはFlutter側だけの確認でマージすべきではありません。これは、うっかり相手側を壊してしまうことに対する安価な安全策であり、後から発生したクラッシュをデバッグするよりも、レビューのルールとして徹底するほうがはるかに安上がりです。ここでは生成されたコントラクトが役立ちます。型が単一のスキーマから生成されるため、両側が同じ生成結果からビルドされている限り、破壊的変更は気づかれないランタイムの不整合ではなくコンパイルエラーとして現れます。それが成り立たなくなる構成については、バージョニングで解説しています。
ネイティブとFlutterのロードマップを同期する
アプリの一部がネイティブ、一部がFlutterである間は、両側は一緒にリリースされます。1つのバイナリ、1回のApp Storeリリース、1回のPlay Storeリリースです。そのため、ロードマップの調整は明示的に計画しておく必要があります。
最初に合意すべきなのは、共通のリリースサイクルです。両側は同じビルドでリリースされるため、計画しているかどうかにかかわらず、同じサイクルで動くことになります。苦労するのはたいてい、それぞれ独立して計画を立て、リリースの段階になって初めて結合に気づくチームです。中途半端なFlutter移行がネイティブのリリースをブロックしたり、スプリントの途中でネイティブのリファクタリングが入って境界を壊したりします。サイクルについては事前に合意し、リリースの締め切りを両側共通のものとして扱いましょう。
フィーチャーフラグは、一方がもう一方をブロックしないようにするための最も確実な方法です。未完成の作業をランタイムで見えなくできるからです。移行の計画で解説しているとおり、移行するすべての画面をフィーチャーフラグ(Firebase Remote Config、LaunchDarkly、または社内の同等の仕組み)の裏に置きます。準備ができていないFlutter画面は単にオフのままにしておけばよく、コードはバイナリに含まれていても、どのユーザーもそこには到達しません。そのため、未完成の移行がリリースを妨げることはありません。同じフラグによって、いずれにせよリスク低減のために必要になる段階的なロールアウトと、ネイティブへの即時フォールバックも実現できます。
最後に、両側を実際に結合している依存関係を調整します。ネイティブとFlutterの作業の大半は独立しており同期は不要ですが、いくつかの例外は明示的に追跡する価値があります。
- コントラクトの変更 - 定義上、両側が一緒に反映する必要があります。変更のマージ、コード生成、利用が決まった順序で行われるよう順番を組み立てます。
- Flutterが依存する共有ネイティブ基盤(認証、ネットワーク、分析) - コントラクトが変わっていなくても、ネイティブ側の変更がFlutterを壊すことがあります。
- 境界にある画面そのもの - ネイティブのフローがFlutterに処理を引き渡す、あるいはその逆を行う箇所です。どちらかの側の入口や出口を変更する場合、もう一方の側がそれを想定している必要があります。
これらは両側から見られる共有のビューにまとめておきましょう。すでに使っている計画ツールで構いません。そうすれば、結合関係がリリース日よりずっと前に見えるようになります。
私たちは、移行計画をJiraのエピックとチケットに変換できるようConvergeを開発しています。これにより、境界をまたぐ依存関係を他の作業と同じバックログで管理できるようになります。