- Webエンジニア
- ディレクター
- 新規事業開発エンジニア
- 他37件の職種
- 開発
- ビジネス
こんにちは。ギフティのエンジニアの加藤 (@kato1628) です。昨年、gifteeの C 向けアプリを Flutter を使ってフルリニューアルしました。
経緯
これまで Swift と Java でそれぞれ iOS, Android 向けアプリを提供していましたが、以下のような経緯により、2020 年 12 月にリニューアルを行いました。
- サービスの大幅アップデートに付随して、アプリも機能・UI 両面でアップデートをしたい。
- サーバーサイドのエンジニアが片手間でアプリを運用する体制となっており、しっかりした開発体制を整えたい。
- アーキテクチャを再構築し、継続的に改善・運用が可能なコードベースにしたい。
この記事では、Flutter でのアーキテクチャ設計と判断理由、実際に開発してみての所感について書きます。本記事は元々 web フロントエンドを開発した人間による記事なので web 開発のバイアスがかかった意見であることをご了承下さい。
なぜ Flutter なのか
当初、会社として Flutter での開発の知見は無かったのですが、以下のような理由により Flutter を採用しました。
- iOS, Android のアプリを Swift, Kotlin を使って専任のエンジニアが担当する体制をすぐに整えることが難しく、web の知識を活かせるフレームワークを採用して既存のメンバーで開発したかった。
- エミュレーターとの連携、エディタのプラグイン、Hot reload 、CI/CD でのテスト自動化やビルド、テスト版のリリースなどを公式でちゃんとサポートしており、周辺ライブラリが整っていた。
- シームレスな画面描画、画面レンダリングのパフォーマンス。UI 構築がさくっとできる。
- 攻めた OS の API を使うような要件は今の所なく、ベーシックな API のみ(Deep Link, Camera, Photo など)が必要とされていた。
- ビジネス側の要求もあり決められたスケジュール内で開発する必要があった。
web の知識を活かせるという意味では React Native も選択肢に上がりました。当然 React Native でも同様の要件を満たすプロダクトを作る事は可能だったと思います。
今回はコミュニティの盛り上がり具合、メンバーの開発モチベーション、 Google トレンド上昇の加速度(将来性)などを理由に Flutter を採用しました。(当然、使用する API や UI・デザインの複雑さに寄って最適な判断は異なってくると思います)
アーキテクチャ
認証・認可
Flutter のアプリからユーザーの認証・認可を行うために以下を採用しました。
- OAuth 2.0 + OpenID Connect 1.0
- Authorization Code Grant + PKCE(認可コード横取り攻撃対策)
特にアプリや web の SPA で、Auth2.0 の Authorization Code Grant で認証を行う際には、Client ID がパブリックに露出せざるを得ない状態となります。その場合でも、安全に認可コードを受け取れるようにするために PKCE (Proof Key for Code Exchange) を実装しました。PKCE の仕様については、RFC と Auth0 のブログで詳しく解説してくれています。
また、Flutter アプリで Auth2.0 のクライアントサイドを実装するための Dart のライブラリもありますが、そこまで複雑なロジックではないことと、この部分に関しては外部ライブラリ依存を減らすため、今回は自前で実装しました。
今回はサービス要件からこの設計にしましたが、Flutter と Firebase の相性の良さからも、他に制約が無ければ Firebase を使うのをおすすめします。開発スピードもその方がかなり早いです。良し悪しはさておき、OAuth の仕組みを完全に理解しなくても実装でき、認証・認可の責務をアーキテクチャ上きれいに外に移譲させられます。
リソース取得
リソース取得には GraphQL を採用しました。弊サービスでは web とアプリを提供しているため、クライアントサイド都合でほしいリソースのかたちが異なる(正確にはその将来が来る)と見込んで採用しました。また、プライベートなリソースについては、認可サーバーから受け取ったトークンを元にリクエストするようにしました。
Flutter で GraphQL を利用するためのライブラリが提供されており、このあたりはありがたかったです。エラーハンドリングの仕組みも比較的実装しやすい IF でした。
余談ですが、今回はアプリを開発するメンバーが、サーバーサイドの GraphQL の実装も行いました。知識のインプットに時間がかかりましたが、各メンバーがサーバーからアプリまで実装できるようになり、機動力のあるチームになったのではと思います。
状態管理
クライアントサイド開発のコアとなる状態管理ですが、Flutter ではフレームワークとしては状態管理の方法に縛りはありません。今回は web フロントエンド の知識を活かせることから Redux を採用しました。GraphQL で取得したリソースを正規化して Redux の state に保持し、reselect を使ってそれを各画面の表示したいかたちに変換させ、ViewModel に渡し表示するようにしました。
宣言的 UI と immutability
もちろん、Flutter は宣言的 UI が可能なので状態変化に応じて UI の更新が行われます。そのため、state に保持するデータを安全に更新するために immutability を担保したくなります。Dart の場合は、JavaScript のようにデータを Object ではなくクラスで保持するケースが多く、クラスの immutability を担保することになります。今回は生の Dart で@immutableアノテーションを駆使して実装しました。またデータ更新の際には、新しいインスタンスを生成するための関数を独自で定義する必要があったりするので、freezed を使うのも良さそうです。
余談ですが、Redux に関わらず、状態管理を実装する際に宣言的 UI には immutability は避けて通れないはずですが、Flutter のドキュメントは React などと比較して immutability への言及は多くはないので、このあたりは自分で理解を深める必要があります。(ただこれは JavaScript をこれまで書いてきたバイアスが強く影響してることは認めます。)
また、今回は Redux で状態管理を行いましたが、サーバー側のデータ更新を websocket を使って購読する処理を実装するケースなどデータストリームを扱う場合には、Redux 単体ではなく Riverpod などの状態管理の方が向いているかもしれません。
UI 構築
Flutter では widget という単位で UI を構築します。widget を使った実装は慣れると手に馴染んで書いていて楽しかったです。また今回は、Material Components widgets を使用しました。Material Components widgets には、AppBar、BottomNavigationBar、Drawer など、アプリでよく使うコンポーネントがまとまっており、これらを組み合わせて実装していきます。
デザイン
アプリ全体の共通テーマは Material Components widgets のThemeData
に定義し、デザインをカスタマイズしていきます。細かな制約はありますが、カスタマイズの柔軟性は高く、デザイナーが作成したデザインガイドラインをアプリに適用することは比較的容易でした。 また web の場合は、CSS でスタイルを書きますが、Flutter の場合は、スタイルも widget で表現されます。当然 widget はクラスで実装されているのでコード補完との相性も良いです。CSS の場合は記述の自由さ故にメンテナブルなコードを書くには設計上の工夫が必要ですが、Flutter の場合はフレームワークとしてスタイルの型が提供されているようなものなので、秩序のあるコードを書きやすいです。
リスト表示
要素をリストで表示するようなケースはよくあると思います。弊アプリでも商品を一覧で表示する画面がありました。実際にアプリで表示する要素の数は数百程度ではありましたが、簡単な動作検証をした限りでは、数万件のデータでも上手くレイジーロードされるようになっており、描画のパフォーマンスに影響が出ないつくりになっていました。
また、今回は描画時にサーバーからリソース取得するのではなく、描画に必要な要素をアプリが起動した直後に予め取得してローカルにキャッシュしておき、描画時には通信は発生せずローカルデータをそのまま描画させ描画速度を早くするようにしました。
テスト
今回は、主に Redux 周りの Unit テストを中心に書きました。mockito でクラスや関数をモックし、各種 matcher を用いて検証するかたちで書いていきました。テストコードの表現力という意味では、正直なところこれまで書いてきた rspec の方が柔軟性があると思いました。(特に関数の返り値をモックするあたり)
今後は、Unit テストのカバレッジもまだまだ足りないのと、Flutter では Widget テスト、Integration テストも書けるので、今後このあたりはテストコードを厚くしていきたいです。
ビルド
Build Mode と Flavor の組み合わせより用途別に、4 つの Build Environments (※Build Mode と Flavor の組み合わせを便宜的に Environments と呼んでいます)を用意して使い分けました。Flavor を元に、環境毎の設定値と、ビルド時のエントリーポイントを指定してビルドします。ビルドに関しては、開発から公開までに必要な用途毎に各環境を滞り無く準備できるような仕組みが Flutter 側で提供されていて助かりました。
このあたりの設定は mono さんの記事を参考にさせていただきました。(非常に助かりました、ありがとうございます。) ※Flutter 1.17 から Fravor を使わなくても、環境毎の設定値をビルド時に指定できるようです。
デプロイ
デプロイは fastlane、CI/CD には GitHub Actions を利用しました。 テスト版・本番向けアプリともに、PR の merge 毎に GitHub Actions でビルドしたバイナリを App Store Connect と Google Play Store に自動でバイナリをアップロードするようにしました。また、チーム開発時の証明書の管理など面倒な部分も fastlane match を利用してオンボーディングしやすい管理方法にしました。Flutter に限った話ではないですが、このあたりのサービス・デリバリーに必要な機能についても、周辺ライブラリや CI/CD でのサポートが整っていて、非常に助かりました。
ただ具体的な実装方法については、まだまだ情報が少ない印象を受けました。fastlane と GitHub Actions を使ってビルド・デプロイを自動化する方法に関して、記事を書いてみたので良ければ見てみてください。
エラー検知・分析
エラー検知には Sentry、Firebase Crashlytics、分析には Firebase Analytics を使用しました。 プロダクションで利用するフレームワークにこのあたりは欠かせないと思います。特に Firebase の Crashlytics, Analytics に関しては同じ Google 製ということもあって、導入がしやすかったです。
分析に関しては、今回は状態管理に採用した Redux の action を Firebase Analytics にとばすことでユーザー行動の分析に活用したり、Firebase Analytics 上でコンバージョンを定義して実績をみるようにしました。また Flutter はクロスプラットフォーム向けのフレームワークではありますが、特定の OS でのみ発生するバグなどに遭遇するケースもあるため、Sentry で OS 情報などを送信しておいてデバッグに活かすようにしました。
採用してみて
実際に Flutter を使ってプロダクション環境のアプリを開発してみて良かった点と困った点(改善されると嬉しい点)を挙げてみます。
良かった点
- UI 構築が手軽、コード補完で画面やスタイルをサクサク作れるのが楽しい。シームレスに描画され、表示パフォーマンスも高い。宣言的 UI を実装できる。
- DX 面での快適さ。エミュレーターとの連携、VS Code のプラグイン、デバッグ、Hot reload などがちゃんと動作する。公式サイトに記載の手順を一通り実施すると、すぐに開発ができる。
- サンプルコードを動かすまでの設定手順などのドキュメントもちゃんとしてて、カウントアップアプリをさっと動かせる。Xcode での code sign の手順も記載があって、実機端末へのデプロイもサクッとできる。
- CI でのテスト自動化、環境毎のビルド、テスト配信、ユーザーレビュー導線など業務で使うための機能やそれらをサポートする周辺ライブラリが整ってきている。公式サポートが手厚いのも安心。
- コミュニティの活発さ。開発途中に Flutter 本体や周辺ライブラリがどんどんアップデートされていった。
- Firebase との相性が良い。認証機能や分析、クレッシュレポートなどの周辺ツールの導入がしやすい。
困った点(改善されると嬉しい点)
- Dart のシンタックス、セミコロン必須に慣れるまで数日かかった。
- クライアントサイド開発の視点で TypeScript と比較すると null 安全でない、union 型が無いなど、型表現の弱さを感じた。(NNDB 対応が進められるなど徐々にアップデートされそう。)
- データのモデルクラスを実装する際に、古めかしい(?)クラスの記述が避けられない。JavaScript に見られる関数型の傾向はあまり無く、書いていて気持ち良いかと言われるとうーん、、という感じ。(個人差はあると思います)
- Flutter の StatefulWidget の設計の理解。State を状態を表現するクラスとして widget とは別に定義するのかと思いきや、そのクラスが widget を返す build 関数をもっていたりと、直感的でなく「なぜその関数をそのクラスに定義するの?」と感じることが多かった。
- ローカライズ面の対応。Android の WebView の日本語キーボードが使えないなど機能開発面で困るケースがあった。(こちらも徐々に改善されつつある)
周辺ライブラリ
また周辺ライブラリに関して、Flutter のライブラリ選定全般に言えることですが、公式と非公式のライブラリがあり、やはり公式の方が安定してメンテナンスされているようで安心感がありました。GitHub の issue での議論も公式ライブラリの方が活発な印象を受けます。OSS ただ乗りおじさんになるつもりはないですが、周辺ライブラリが成熟しきってはいないので、そのあたりの判断は実際に GitHub のコードの中身をみたり issue 内容を見て慎重に行うべきかなと思います。
ざっくりとしたまとめとしては、UI 構築と周辺ツールには非常に助けられたが、Dart での開発が少しつらかった、という感じでしょうか。また、今後 OS の攻めた API や複雑なデザインの画面を開発する必要が出てきた際にはまた別の問題が出てくるかもしれません。
今回は Flutter を使ったアプリのアーキテクチャについての記事でしたが、実装上で工夫した点など詳細な記事なども今後書いていけたらと思います。