ネイティブからFlutterへの段階的な移行を計画する

最初に移行する画面の選び方、段階的なロールアウト、ストア審査への対応、そして初日から純粋なFlutterアプリへの出口を計画する方法を解説します。

Flutterへの段階的な移行では、既存アプリを少しずつ置き換えていきます。画面ごと(あるいはユーザージャーニーごと)にFlutterへ移し、各ステップを本番にリリースしながら、最後のネイティブ画面がなくなるまでアプリを動かし続けます。このページでは、Flutterへの段階的な移行の成否を分ける判断、つまり何から移行するか、どうロールアウトするか、出口をどう計画するかを扱います。

最初に何を書き直すか:最初のFlutter画面の選び方

最初に移行する画面の選択は、その後のどの画面よりも重要です。うまくいけば、チームは実際に価値のあるものをリリースし、自信をつけ、ユーザーの前でアプローチの正しさを検証できます。うまくいかなければ、誰も見積もっていなかった数か月分の統合作業を抱えたうえ、本番にはまだFlutterが一つもない、という事態になりかねません。そして移行に懐疑的な人たちに、格好の反対材料を与えてしまいます。

最初の画面に向いている条件

それ単体で成り立つ画面を探しましょう。

  • 自己完結している:ネイティブへの依存が少なく、画面の外から必要なデータもわずかです。設定、プロフィール、オンボーディングのフローが典型例です。
  • ネイティブにまだ存在しない新機能:ピクセル単位で合わせる対象がないので、まっさらな状態から始められます。ただし、ステークホルダーを説得するのに役立つ新旧の比較はできなくなります。
  • iOSとAndroidで見た目が同じ:一つのFlutter実装で両プラットフォームをカバーでき、Flutterが最も得意とするケースです。プラットフォーム間でUIが乖離してしまった画面も、一つのUIに戻したいなら良い候補です。
  • ユーザージャーニーの末端(リーフ)にある:ユーザーがその画面に到達し、その後ネイティブに戻る画面で、さらに別のネイティブ画面へ進むことはありません。ネイティブとFlutterをまたぐたびに摩擦が生じるので、チームがまだ学習中の段階では避けたいところです。
  • 埋め込みがない:全画面のFlutterビューは、ネイティブビューの中にFlutterを配置する(あるいはその逆の)構成よりもはるかにシンプルです。埋め込みは、最初の全画面移行をリリースした後に取っておくのが賢明です。
  • 十分なトラフィックがある:注目度の高い画面からは率直なフィードバックがすぐに得られ、ステークホルダーへの説得材料としても強力です。

最初の画面に向いていない条件

最初に避けるべきなのは、作業の大半がネイティブとFlutterの境界で発生する画面です。

  • ネイティブSDKと深く結びついている:カメラ、地図、AR、オーディオ、生体認証などです。労力の大半が境界をまたいだデータの受け渡しに費やされますが、それはadd-to-appの中でも初日から学びたくない部分です。
  • プラットフォーム固有のナビゲーションに依存している:共有要素トランジション、独自のモーダル表示、画面の境界をまたぐアニメーションなどです。たいていは解決できる問題ですが、解決策はコストが高く、やや無理のある実装になりがちで、何かをリリースする前に開発のペースを落としてしまいます。
  • 埋め込みが必要:ネイティブビューの中のFlutter、あるいはFlutterの中のネイティブビューです。埋め込みは、この段階では不要なライフサイクル、ジェスチャー、サイズ調整の問題をもたらします。詳細と代替案は埋め込みのページで説明しています。
  • 長いネイティブのジャーニーの途中にある:ユーザーが進むたび、戻るたびに境界をまたぐコストを払うことになります。こうした画面に手を入れる必要がある場合は、ネイティブ画面の間にFlutter画面を一つ挟むよりも、ジャーニー全体をまとめて移行するほうが良いことが多いです。
  • 認証の一部である:ログイン、ユーザー登録、セッション管理です。他のすべてのフローがその後ろにあり、ネイティブの認証基盤と密に結合しており、そこにバグがあるとユーザーはアプリ全体から締め出されてしまいます。私たちがインタビューしたあるチームはログイン画面から始めましたが、振り返ってみれば、認証をまったく必要としない画面から始めるべきだったと話していました。

判断フロー

これまでに見てきたFlutter移行のパターン

私たち自身のプロジェクトと、インタビューした移行チームの事例を通じて、いくつかのパターンが繰り返し現れています。

1. Flutterで作る新しいビジネスモジュール

よくあるのが、成熟したアプリにまったく新しいモジュールを追加するケースです。小売アプリの中のロイヤルティプログラム、B2Bポータル、これまで存在しなかった新しい事業領域などです。このモジュールは独自のフローと状態、独自のデータを持ち、場合によっては別のバックエンドと通信することもあります。ユーザーは一か所(ホーム画面のカード、タブ、ディープリンク)から入り、離れるまでその中にとどまります。アプリの他の部分から見れば、入口が一つだけのブラックボックスです。

これはadd-to-appが最も得意とするケースです。このモジュールは実質的に、たまたまネイティブのシェルの中にある小さな独立したFlutterアプリなので、境界の作業はその一つの入口に集中します。開始時にいくつかのデータを渡し、終了時にいくつかのデータを返すだけです。チームはモジュール内で素早く開発でき、周囲のネイティブコードに手を入れることはほとんどありません。作業が入口一つの自己完結した領域であれば、ネイティブアプリを離れることなく、純粋なFlutterアプリの利点のほとんどを得られます。

2. 影響の大きい賭け:ビジネス側が最も中心的な画面を選ぶとき

私たちがインタビューした大手マーケットプレイス企業のあるチームでは、エンジニア側はリーフ画面から始めることを主張していました。外側から内側へと進める移行ロードマップも用意していました。しかしビジネス側の判断は違いました。アプリには数百の画面があり、当初Flutterになるのが一握りだけなら、最も訪問数の多い3〜4か所を何よりも先に書き直してほしい、というのがビジネス側の要望でした。こうしてホーム画面が最初に書き直す対象になりました。

残念ながら、ホーム画面はアプリの中で最も多くの部分とつながった画面でもありました。アカウントの状態、検索、レコメンド、広告、ディープリンクからデータが流れ込み、そのつながり一つひとつが、仕様を定めて保守しなければならないネイティブとFlutterのコントラクトになりました。そもそもリリースするために、チームはFlutter画面の中にネイティブウィジェットを埋め込む必要がありました。社内の広告ライブラリをFlutterで再実装できなかったからです。この経験から、同社はネイティブをFlutterに、あるいはFlutterをネイティブに埋め込むことは避けるという強い社内方針を打ち出しました(その理由と代替案は埋め込みを参照してください)。一方で、移行の本体はいまだ初期段階にあります。

最も訪問数の多い画面は、たいてい最も多くの部分とつながった画面でもあり、そのつながり一つひとつが、誰かが仕様を定めて保守しなければならないコントラクトです。ビジネス側がトップ画面を最初に求めるなら、このコストを必ず議論の俎上に載せてください。

3. リーフから始める段階的移行

3つ目のチームは逆の道を選び、リーフ(ユーザーが到達して戻ってくる末端の画面)と、ネイティブに前身のない新規機能から始めました。理由は境界のコストです。リーフ画面には入口と出口が一つずつしかないので、コントラクトは渡すデータと返す結果だけです。フローの途中でディープリンクを横取りする必要も、ネイティブの上にFlutter、その上にネイティブと積み重ねる必要も、ユーザーが動き回る間に同期し続けるべき共有状態もありません。新規機能はさらにすっきりしていて、再現すべきものも合わせるべき挙動もありませんでした。

一つひとつの移行が小さく、それ単体でリリースできたため、一度のつまずきがプログラム全体を脅かすことはありませんでした。チームはリスクの低い画面でパターンを実証し、共有インフラ(コントラクト、エンジン管理、デザインシステムのコンポーネント)を少しずつ構築し、中心的な部分に手を付ける前に全員がツールチェーンに慣れるようにしました。四半期ごとにリーフから内側へと進め、つながりの多い画面にたどり着いたときには、難しいインフラはすでに整い、本番で実証済みでした。

そのアプリは今ではほぼFlutterになり、チームは最後の切り替えを進めています。シェル自体をFlutterに移し、ネイティブのホストを廃止する段階です。リーフから始めるやり方は序盤こそ時間がかかりましたが、最後までやり遂げることができました。

4. 「add-to-appのほうが安全」という誤算

最後のチームが引き継いだのは、本当にひどい状態のネイティブアプリでした。何年分ものレガシーコード、何層にも重なったアンチパターン、最新のデザイン資料はなく(Figmaもなく、動いているアプリがあるだけ)、ドキュメントもありません。元の挙動を理解していた人たちはとうの昔に去っていました。開発者はリライトを主張しましたが、クライアントはadd-to-appにこだわりました。実績のあるネイティブコードを動かし続け、画面ごとに置き換えていく段階的なアプローチのほうが安全に感じられたからです。それ以上の移行戦略はなく、最初に選ばれた画面はログイン画面、つまり他のすべてのフローが依存する画面でした。

落とし穴は、この「実績のある」ネイティブコードがブラックボックスだったことです。画面を移行するには、まずその画面が何をしているのかをリバースエンジニアリングする必要がありました。もはや誰も説明できなくなったビジネスルールを、絡み合ったネイティブコードを読み解いて取り戻し、そのうえで両者がまったく同じように動くよう、同じクセまでFlutterで再実装しなければなりませんでした。時間の大半は、何か新しいものを作ることではなく、ドキュメント化されていないロジックの再現に費やされました。しかも準備フェーズ(挙動をドキュメント化する、最悪の部分を整理する、デザイン資料を先に整えるといった取り組み)がなかったため、どの画面でもリバースエンジニアリングをゼロからやり直すことになりました。

このケースでは、おそらくリライトのほうが良い選択だったでしょう。既存コードがここまで不透明だと、add-to-appでは二重にコストを払うことになります。一度目は古いコードを理解するため、二度目はその上で境界を保守するためです。リライトなら理解のコストは一度で済み、境界はまるごとなくなります。段階的なアプローチが自動的に安全になるわけではありません。ネイティブコードがレガシーで、ドキュメントがなく、オーナーもおらず、その知識を取り戻す準備フェーズもない場合、クリーンなリライトのほうがリスクの低い道になることも多いのです。

段階的なロールアウト

add-to-appのリスクを管理可能な範囲に収めるのが、段階的なロールアウトです。最も価値の高い画面は、移行コストも最も高い画面です。そうした画面を初日から全ユーザーに公開するのは、トラブルを招くようなものです。add-to-appなら別のやり方が簡単にとれます。各Flutter画面をフィーチャーフラグの裏に置く(Firebase Remote Config、LaunchDarkly、Optimizely、あるいは社内の同等の仕組み)だけで、A/Bテストと段階的なロールアウトがほぼ無料で手に入ります。同じフラグの裏にメトリクスを追加すれば、公開範囲を広げる前に、Flutter版がユーザーに与える実際の影響を測定できます。

ロールアウト中は、add-to-app特有の2つのリスクに特に注意を払う価値があります。

エンジンの起動時間

ネイティブアプリの中でFlutterを初めて初期化するときには、少し時間がかかります。劇的な遅延ではありませんが、何も対処しなければユーザーは真っ白な画面を見ることになります。新旧を並べたデモでネイティブ画面のほうがきびきびして見えるのには、単純な理由があります。ネイティブ画面には立ち上げるべきランタイムがありませんが、最初のFlutter画面はエンジンのコールドスタートのコストを払うからです。ユーザーがFlutter画面に到達する前に(通常はスプラッシュ画面で)エンジンをプリウォームしておけば、すぐに描画されるので、この問題はほぼ回避できます。Inlayはstart() / init()でこれを自動的に行います(エンジンのプリウォームを参照)。それでも、特にローエンド端末向けに、このメトリクスはダッシュボードに残しておく価値があります。

ローエンド端末でのメモリ逼迫

複数のFlutterエンジンを適切に管理せずに動かすと、安価な端末でメモリを圧迫することがあり、これは早めに検知したいところです。最初の画面の描画時間とクラッシュ率を、ロールアウト全体のガードレールとして追跡しましょう。メモリ逼迫が見つかっても、フィーチャーフラグがあれば、修正している間はネイティブ画面にフォールバックできます。

出口戦略:Flutterへ完全に切り替える方法

add-to-appのままでいることには限界があります。境界には常にある程度の摩擦があり、Flutter開発者は単体のFlutterアプリと同じようにFlutterを使うことができません。出口はこの制約を取り除きます。ネイティブのシェルを、シェルとナビゲーションを所有するFlutterアプリに置き換えるのです。そうすれば、アプリはチームがすでに作り慣れている単体のFlutterアプリと同じように振る舞います。

切り替えのために100% Flutterに到達する必要はありません。 しきい値はもっと低くてもよく、主に、どの画面をネイティブのまま残してよいかという問題です。めったに訪問されない画面、別チームが所有するモジュール、ネイティブとの結合が深すぎて書き直す価値のない部分は、すべてそのまま残せます。変わるのは制御の向きです。ネイティブのシェルがFlutter画面をホストするのではなく、Flutterのシェルが残りのネイティブ画面をホストします。答えるべき主な問いは、Flutterのホストがそれらのネイティブモジュールとどうやってデータを同期させるかです。これは以前と同じ境界の作業で、向きが逆になっただけです。フレームワークがすでに提供している共有状態レイヤーが、この同期を扱いやすくしてくれます。

出口は初日から計画しておきましょう。

  • 移行率を追跡する:アプリのどれだけが、またどれだけのユーザージャーニーがFlutterで、どれだけがネイティブなのかを常に把握できるようにします。
  • 単一の画面ではなく、ユーザージャーニー単位で考える:密に結合した数画面をまとめて移行するほうが、一つだけ書き直して残りをネイティブのままにするよりも簡単なことがよくあります。境界をまたいで統合するコストが、ジャーニーを最後まで移行してしまうコストを上回ることがあるからです。
  • 「ラストワンマイル」の画面を特定する:移行が最も難しくなる画面を見極め、早めに調査を始めておけば、その番が来たときに準備が整っています。

私たちのadd-to-appフレームワークは、出口を念頭に置いて設計されています。 フレームワークが提供する抽象化(ナビゲーション、状態)は、純粋なFlutter アプリで使うパターンにきれいに対応するため、フレームワークを取り除いても Dartコードを大規模に再設計する必要はありません。ナビゲーションレイヤーは 最も人気のあるライブラリ(go_routerやauto_routeなど)と簡単に統合できるため、 出口の際にもそのほとんどはそのまま残ります。同様に、共有状態レイヤーも ネイティブプラットフォームから簡単に切り離せるため、既存のビジネスロジックは 実質的にすべてそのまま維持できます。

add-to-appはApp Store / Play Storeの審査に影響するか

影響しません。 AppleもGoogleも、ネイティブアプリへのFlutterの組み込みを禁止しておらず、add-to-appのプロジェクトも通常の審査プロセスを通ります。Flutterは実行可能コードをダウンロードして解釈するのではなく、ネイティブのARMコードに事前(AOT)コンパイルされるため、Appleのルールに準拠しています。私たちも、Flutterが含まれていることで審査期間に影響が出たケースを見たことはありません。

プライバシーポリシー

このページの内容