WINTICKET Flutter での iOS アプリリプレースの道のりと成果
はじめに
こんにちは、株式会社 WinTicket でモバイルエンジニアをしている仙石 晃久(@akihisasengoku)です。
約 2 年間かけて WINTICKET の iOS / Android アプリを Flutter でリプレースしました。
本記事では、以前 WINTICKET アプリ Flutter リプレース戦略 でお伝えした Android アプリの Flutter リプレース後から iOS アプリのリプレース実現までについて、Flutter でのリプレースを経てアプリ開発がどのように変わったかについて紹介します。
WINTICKET iOS リプレースの道のり
2022 年 4 月に Android アプリ、2023 年 2 月に iOS アプリのリプレースを実現しました。
Android のアプリは、元々 Trusted Web Activity(TWA)を利用して Progressive Web Apps(PWA)をアプリとして配布していました。そのため、Swift で実装されたネイティブの iOS アプリに比べて、ユーザー体験に関する要求が緩やかであるため、Android アプリのリリースを先行しました。さらに、Android アプリのリリースを先行することで、ユーザーに実際に早い段階で使用してもらい、iOS アプリのリリース時の信頼性やユーザービリティの向上も目的としていました。
Android のリプレース後から iOS アプリのリプレースに向けて、下記の要件が求められました。
- TWA では実装されていなかった生体認証やバックグラウンド再生など、各 OS の API を活用した機能の実装
- WebView で実装されている部分を Flutter で置き換え
- 既存の iOS アプリと同等のユーザー体験を実現するためのインタラクションの改善やパフォーマンス向上
- リプレース作業と同時進行での機能開発
当初、これらの要件を満たした上で 2022 年 8 月末のリリースを目指して開発が進められました。どのような課題が存在し、当初の予定から 6 ヶ月ほど iOS アプリのリリースが遅れたかについて紹介します。
リプレースの延期
8 月末の iOS アプリのリプレースを目指して開発は順調に進んでいましたが、Flutter iOS で画面がちらつく(真っ黒な画面と正常な画面の表示が繰り返される)問題に遭遇しました。以降、この問題を Flickering と呼ぶことにします(当時の Flutter SDK の最新バージョンは3.0.5)。この問題は、アプリが操作できない状態で停止し、その後バックグラウンドから復帰すると操作できない状態で画面がちらつく現象です。詳細はこちらの issueで報告されています。
Flickering の問題は、Flutter 2.10.0 から確認されており、2023 年 3 月現在の最新の Flutter SDK 3.7.7 でも発生しています。さらに、Skiaに代わる開発中の新しいレンダリングエンジンImpellerでも同様の問題が確認されています。Flickering の今後の修正対応は、Skia での修正ではなく、Impeller での修正を目指し、対応が進められています。
Flickering 問題への対応
リプレース直前の 7 月にこの問題を発見し、改善の糸口を探ろうとしましたが、再現方法の確立ができず(issue 上でも再現方法が確立されていない)、問題の解決や軽減が困難でした。問題の原因は、Flutter Framework やその下の Engine のレイヤーであると考えられたため、改善のためにコミュニティとの連携が重要であると判断しました。コミュニティに有益な情報を提供し、改善が進むことを期待し、再現端末・再現 OS・発生しやすい状態(メモリ不足、ディスク I/O 処理、マルチスレッド処理)の特定を試みました。特定のために複数の iOS 端末を用意し、開発中のアプリに任意の負荷を与えられるようにした上で、負荷をかけた状態でアプリを操作し、Flickering の再現を試みました。実装した負荷機能は以下のとおりです。
- マルチスレッドでのメモリの大量消費
- 大量の Widget の再描画処理による UI と Rasterizer スレッド負荷
- SVGImage の描画による I/O 処理負荷
- 描画コストが高いメソッド呼び出しでの Rasterizer スレッド負荷
6 名が 7 日間調査にあたったものの、再現方法の確立はできず、調査で得た情報(Flickering 確認端末、OS、Console Log、パッケージ依存関係)をコミュニティに提供し、コミュニティ側での解決を期待しました。
WINTICKET 内部での調査では、7 日間の期間で約 5% のユーザーが Flickering に遭遇していました。
当時の Flutter SDK 3.0.5 では、発生時にクラッシュレポートやrunZonedGuardedやFlutter.onErrorに例外イベントが流れないため、エラーレポーティングツールでイベントを取得できませんでした。
そのため正確な発生率は不明ですが、5%を Flickering の発生参考値として考慮していました。
前述の通り、Flickering が発生するとアプリが停止し、クラッシュと同等またはそれ以上の悪影響を与えることが予想されました。リプレース前の iOS アプリのクラッシュフリーレートは 99.7%で、リプレースによる Flickering の発生により、ユーザー体験が大幅に低下することが懸念されました。そして、Flickering の修正はチームでは不可能であることを認識し、ユーザー体験の低下を避けるために、iOS アプリの Flutter リプレースを延期することを決定しました。
Flutter コミュニティの修正対応
延期を決定してから約 3 ヶ月後の 2022 年 11 月に Flutter SDK 3.3.7がリリースされました。3.3.7 では、Metal のコマンドバッファーのスケジューリング修正とコマンドバッファの失敗時のエラーログの修正が行われた Skia のバージョンが Flutter Engine に取り込まれました。この修正を試すために別のブランチで 3.3.7 へのアップデートを行い、Flickering 問題の再検証を開始しました。
検証の結果、Flickering の問題は解決されていませんでしたが、発生頻度は低下しました。また、以前は画面のちらつきがアプリを強制終了するまで続いていたのに対し、2 秒ほどちらつき、アプリがクラッシュするようになりました。検証結果は、issue のコメントで言及されているように、発生時の Console Log をコミュニティに提供し、引き続きコミュニティの解決のための情報を共有しました。
3.3.7 の修正により、Flickering でのユーザーへの影響が軽減され、ちらつき後にクラッシュすることでエラーレポーティングツールで Flickering 現象を計測できるようになりました。
リプレースの意思決定
コミュニティーからの Flickering 修正の提案を受け、リプレースに向けた準備を再開することを決定しました。Flickering は当時の最新の Flutter SDK 3.3.7 でも発生しており、クラッシュだけを見るとユーザー体験は既存の iOS アプリよりも低下することになります。しかし、クラッシュを 1 つの不具合と捉え、年間の不具合遭遇ユーザー数で比較すると、既存の iOS とほぼ同程度でした。不具合遭遇ユーザー数の算出式のイメージは以下になります。
不具合遭遇ユーザー数 = (クラッシュ率 * DAU * 1年の日数) + (障害率 * リリース回数 * 平均障害発生日数 * 影響ユーザー数)
既存 iOS 不具合遭遇ユーザー数 > Flutter iOS 不具合遭遇ユーザー数
(Flutter iOS は未リリースのため、不具合遭遇ユーザー数は 2022 年 4 月にリリースした Flutter Android の障害の実数値を元に算出しました。)
既存の iOS よりも Flutter で不具合が減る理由は、Flutter のリプレース当初から障害率や障害発生日数を抑えるための基盤整備と対応を行ってきたことに起因します。具体的には、自動テストでの品質担保、アラートやモニタリングを整備しました。これらの取り組みの詳細は、WINTICKET Flutter アプリの SRE for Mobile の取り組みで掲載していますので、ご参照ください。
Flutter iOS の 7 日間のクラッシュフリーレートの数字は、ユーザー数と信頼性がともに十分ではないため参考値にすぎません。Flutter iOS のクラッシュフリーレートについては、ベータテストや段階リリースを通じて事前に検証、想定通りの結果となるかを確認してからリリースすることで担保します。また、障害率や障害発生日数も 2022 年 4 月から 12 月までの Android リリースで 28 回しかなく、参考値にすぎません。しかし、自動テストで事前にバグを検知できたケースやアラートによって、ユーザーからの問い合わせより早く障害を検知できたことから、既存の iOS アプリよりも QA やモニタリング体制が優れている実感がありました。
リプレースの実現に向けて
リプレースでは、意図しない問題を最小限に抑え、リプレースによる事業全体のプロジェクト遅延を防ぐことが求められました。
段階的なリリース中に施策のリリースを行えないことや、既存の iOS の開発を停止するため、問題発生時にはプロジェクト全体の大幅な遅延につながるためです。
問題の発生を最小限に抑える取り組み
まず、Flutter SDK や依存しているパッケージの問題点を調査しました。
具体的には、flutter/flutterの P1
、P2
、P3
の Label が付いている issue と、依存しているパッケージの関係ありそうな issue を合計 119 個リストアップしました。
例えば、iOS での Jank 問題は、スクロールパフォーマンスに大きな影響を与えることがわかっていました。
しかし、調査時には PR が作成され修正目処が立っていたため、チームの余分な工数をかけず対応を待ってリリースする判断を早期に行うことができました。
さらに、image_picker の permission 問題に関しても、issue を通して不要な permission の削減できました。issue の調査後、サイバーエージェント内の別 Flutter プロダクトの iOS エラーレポートやインシデントを調査しましたが、自分たちのプロダクトに該当する問題は発見できませんでした。
これらの issue や他プロダクトの調査を通じて、修正可能な問題は修正して、修正できない問題については事業サイドと協議し事前に認識した状態でリプレースに望むことができました。
ユーザー体験の差分を最小限に抑える取り組み
ユーザー体験の差分を最小限に抑える取り組みについて紹介します。ユーザー体験の差分を生む要素は大きく 3 つに分類できます。
- 不具合
- 仕様差分
- パフォーマンス、操作感、デザイン、アニメーションなどの触り心地
不具合や仕様差分に関しては、既存機能の検証項目書が存在するため QC で担保しました。
Flutter Android は既にリリースされており、iOS と Android 共通に使われているアプリレイヤーのコードに関して品質が担保されています。そのため、Platform 分岐がある箇所や iOS 固有 の Platform API を使用している箇所に対して、OS と端末のバリデーションを網羅的に用意し、重点的に QC を進めました。
また、QC では触り心地の判断が難しいため、社内の 20 名ほどのメンバーにアプリを試用してもらい、デザインやアニメーション、パフォーマンスの差異に対するフィードバックを得ました。開発チームは、これらのフィードバックをもとに、リリース直前まで修正して、再度メンバーに試用してもらう改善ループを最大限繰り返しました。Android のリプレース時にモニタリングツールとしてSentryを使いエラー、クラッシュ、OOM のログのイベントや端末のメトリックスを取得できるように整備していました。そのため、QC や社内のメンバーの使用時に発生した内部エラーに関してはすべて目を通し、早期に対処可能なものに関しては解決しました。
最後にベータテストを実施し、一部の実際のユーザーにアプリを使ってもらうことで、想定外のケースも確認し、ユーザー体験の差分を解消していきます。また、Flickering の発生率が想定通りであるかとユーザーのアクティベーションに問題ないかの確認も含まれています。ベータテストの詳細は以下の通りです。
- Apple Store Connect の TestFlight でアプリを配布
- 期間は 9 日間
- 対象者は約 1,200 名(アクティベーションが高く、端末と使用する機能を網羅的に満たせる対象者を選定)
- ベータテスト用に問い合わせ用の導線を追加設置
- 優先度高く対応できるようにベータ専用問い合わせ対応フローを用意
- 終了後に定量的なアンケートを実施
対象ユーザーの選定と問い合わせ対応を工夫したことで、有益なフィードバックを多くもらうことができ、ユーザー体験の差分を明確にしました。実際にベータテストを通して、レンダリングエンジンの差によるフォントの違和感、タイマーの挙動バグ、デバイスのスリープ状態バグなど、内部テストでは気づけなかった細かい問題を検知し、修正対処できました。
ユーザーに影響を与えずに iOS のリプレースを実現するために、前述したできる限りのことを行いました。その結果、ベータテスト後の段階リリースや 100%リリースでは、問題が発生せずに iOS のリプレースを実現できました。
リプレースプロジェクト完遂後の変化
Flutter を用いて iOS と Android アプリをリプレースして 2 ヶ月が経ち、Flutter リプレースによって多くの恩恵を教授できていることを実感しています。
ソースコード比較
既存の iOS アプリと Flutter iOS アプリのアプリコードを比較すると、コード量が削減されていることがわかります。
files | blank | comment | code | |
---|---|---|---|---|
Swift | 3,209 | 28,490 | 4,298 | 206,489 |
Flutter | 1,917 | 17,278 | 5,851 | 144,462 |
注: この表は、clocを使用して出力しました。表示されている値はアプリコードのみです。また、自動生成されたファイル、Protocol Buffers のスキーマ定義ファイル、assets ファイル、プロジェクトや CI/CD の設定ファイル、およびテストコードは除外されています。
一般的に、ソースコード量の減少が必ずしも保守性や可読性の向上につながるわけではありませんが、このケースでは iOS アプリよりも少ないコード量で、iOS と Android の 2 つのプラットフォームを管理できることが大きな利点となっています。
これにより、両プラットフォーム間でのコードの共有や再利用が容易になり、開発効率の向上が期待できます。
Flutter アプリのソースコードにおいて、プラットフォーム依存のコードが含まれるファイルは全体の 3.0%となっています。
この割合は、TargetPlatform を使ったプラットフォーム分岐や独自のプラットフォーム判定のユーティリティ関数が含まれるファイル数を計算し、全体のファイル数で割り算出されています。
プラットフォーム依存のコード割合から、プラットフォームに依存しないコードが大部分を占めていることがわかります。
コード品質の向上
リプレースでは、コード品質の向上も目的の 1 つとして取り組んでおり、実際にリプレース時に自動テストによる品質保証が大幅に拡大しました。
Unit Test カバレッジ | Visual Regression Test のシナリオ数 | E2E のシナリオ数 | |
---|---|---|---|
Swift | – % | 514 | 0 |
Flutter | 21.0 % | 1,913 | 12 |
取り組めた背景として、リプレース当初から各レイヤーがテスタブルな設計であったことや、開発方針でテストを書くべきレイヤーを決めていたことが挙げられます。WINTICKET が考えたモダンな Flutter アプリ設計を完全解説で WINTICKET の Flutter アプリの設計について詳しく紹介しています。
開発プロセスの変化
Flutter でのリプレースに伴い、開発プロセスを変更し、開発生産性とデプロイパフォーマンスの向上を実現しました。プルリクエストのマージリードタイム(PR Merge Lead Time)とデプロイ頻度(Deployment Frequency)が、既存の Swift による開発と比較して改善されています。
PR Merge Lead Time | Deployment Frequency | |
---|---|---|
Swift | 185.4 hours | 施策リリースがあるときのみ(2~3 週間に 1 回) |
Flutter | 47.7 hours | 毎週 |
注: PR Merge Lead Time は、最近マージされた 200 個の PR から算出しています。また、依存関係の更新など、自動生成された PR は除外しています。
Flutter Web の活用
Flutter Web を用いた Web アプリを導入して、開発時の PR のレビュー効率を向上させています。具体的には、Flutter Web を使用して、本体アプリと後述する UI Catalog アプリを Web アプリとしてプルリクエスト(PR)ごとにデプロイして、PR の動作確認に使用しています。UI Catalog とは、弊社のエンジニアが作成した Playbook Flutter というライブラリを使った、UI コンポーネントの一覧を表示するアプリになります。
モバイルアプリの動作を確認するためには、手元でアプリをビルドや配信されているアプリをインストールする必要があります。PR の動作確認には多少なりとも時間がかかるため、モバイルアプリの開発においてはボトルネックとなることがあります。
しかし、Web アプリとしてデプロイされている場合は、これらのタイムラグなしで動作を確認できます。各 OS に依存した機能の確認はできませんが、表示やアニメーション、ログの発火の確認ができます。
トランクベース開発の導入
トランクベース開発とは、機能を細かい単位に分割して main ブランチにマージしていくバージョン管理手法です。既存の iOS 開発では、機能を別ブランチで開発し、レビューや QC が完了次第 main ブランチに取り込むフィーチャーブランチ運用をしてました。
システムのリプレースに伴い、コンフリクトリスクの軽減と短期間の改善サイクルの実現のためにトランクベース開発を導入し、プルリクエストのマージリードタイムの改善に成功しています。前述の「コード品質の向上」の章で触れた自動テストとの組み合わせによって、問題を早い段階で検知でき、main ブランチの品質を維持しています。さらに、フィーチャーフラグを使用して機能開発を行い、リリース時にバグが混入しても機能を無効化でき、開発速度を維持しつつリリース時のリスクを軽減しています。トランクベース開発とフィーチャーフラグについては別の記事で詳しく紹介しています。
デプロイパフォーマンスの向上
CI/CD の整備により、リプレース前は iOS のみの管理だったものが、iOS と Android の 2 つのプラットフォームを管理に増えながらもデプロイパフォーマンスを向上させています。
詳細のリリースフローに関してはWINTICKET の Flutter アプリを支えるリリースフローの紹介で紹介しています。
実際に CI/CD の基盤整備が進んでいることは CI/CD 関連のコード量の変化からもわかります。
files | blank | comment | code | |
---|---|---|---|---|
Swift | 56 | 231 | 90 | 1,175 |
Flutter | 68 | 360 | 105 | 3,603 |
注: この表は、clocを使用して出力しました。Github Actions や CircleCI などの CI /CD の設定ファイルのコード行数。
まとめ
WINTICKET では、Flutter を用いて Android と iOS アプリのリプレースを行いました。本記事では、iOS のリプレースを実現するまでの課題と対策に触れ、リプレースによる開発組織への影響を伝えさせていただきました。WINTICKET のリプレース事例が Flutter の採用やリプレースの参考になれば幸いです。今回紹介できなかった詳細な技術的な取り組みについては、今後情報発信していく予定です。