Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

I/O Extended 2023 - Flutter 活用事例

I/O Extended 2023 - Flutter 活用事例

Daichi Furiya (Wasabeef)

August 24, 2023
Tweet

More Decks by Daichi Furiya (Wasabeef)

Other Decks in Programming

Transcript

  1. • エディターは? • CI は? • 動作確認・テストは? • どういう技術使いたい? •

    開発期間は? • Flutter 経験者は? • どの技術を使えばシステム要件を満 たすことが出来る? 要件定義 エンジニアの体制 モチベーション • アーキテクチャどうする? • 参考にできるものはある? • 慣れてる MVVM?Redux? Flutter の将来性 開発体験 アーキテクチャ はじめに 01 02 03 04 05 06 • Flutter で大丈夫?
  2. 5 対象端末 • Android と iOS が対象? • タブレット最適化はある? •

    テレビデバイスは? 要件定義 Image by macrovector on Freepik
  3. • 動画再生(ライブ、オンデマンド) • 課金 • PUSH 通知 • 検索 •

    OS 毎に違うデザイン 機能要件は? 要件定義 WINTICKET
  4. Flutter では難しいことのポイント • ライブラリなどで対応していない機能を iOS/Android をそれぞれで作る事になった時 • Swift/Kotlin で作るほどのパフォーマンス Flutter

    では難しいこと 要件定義 • ユーザー認証(Firebase など) • 単純な動画再生(ライブ、オンデマンド) • 課金 • PUSH 通知 • 検索 • OS で共通したデザイン Flutter でも出来ること Flutter だと難しいこと • OS 毎に全く違うデザイン ◦ Flutter を使うメリットがない • リッチなアニメーションでパフォーマンス • OS 最新機能への追従
  5. ここで説明するのは現在携わっているプロ ジェクトの状況です。 Android/iOS 経験者であれば Flutter 開 発は敷居が低いのかなと思っていますし、 アプリ開発経験がなくても Web の

    React 経験者があれば Flutter の開発も敷居が 思ったほど高くないと思っています。 エンジニアの体制 エンジニアの体制 要件定義 & 技術検証 アーキテクチャ & 環境構築 Flutter 経験者 1 人 Android 経験者 1 人 Flutter 経験者 1 人 Android 経験者 2 人 機能開発 Flutter 経験者 1 人 Android 経験者 2 人 iOS 経験者 1 人 新卒 1 人
  6. 6 つのプラットフォーム (Android、iOS、Web、 Windows、macOS、Linux) での Flutter サポートによ り、100 万以上ものアプリが Flutter

    を利用していま す。 サイバーエージェントの場合は新規にアプリを作るもの 関してはほぼ Flutter を選択しています。 Flutter の将来性 Flutter の将来性 Image by iwat1929 on Freepik
  7. Device Preview Device Preview は別のデバイスでの表示をシミュ レートしてくれるパッケージです。 例えば Flutter Web (Chrome)

    上で iPhone で表 示した時の UI を出してくれます。 開発体験 Flutter Gallery https://pub.dev/packages/device_preview
  8. Flutter Web (Chrome) で iPhone や Samsung 端末をシミュレートしたり端 末の向きを変えています。 Flutter

    Web にさえすれば GitHub で コードレビューする際などで動作確認 が楽になります。 Device Preview 開発体験 https://pub.dev/packages/device_preview
  9. 例えば、GitHub の PR に対して IssueOps として GitHub Actions で Device

    Preview をビルド して GitHub Pages には PR 毎にデプロイし、コメントにそ の URL を貼り付けるようにしています。 その後、マージされたらこの URL は削除しています。 GitHub の PR で動作させる 開発体験
  10. また、main branch にマージされた際にも main branch の状態を GitHub Pages に デプロイされる

    ようにし、README に URL を貼って確認できるように しています。 ※ GitHub Pages は一般公開しないように Private 設定にしています。 GitHub の PR で動作させる 開発体験
  11. Widgetbook フロントエンド界隈で利用されている Storybook の Flutter 版です。UI コンポーネ ントをカタログのように表示することができ、 Flutter Web

    にも対応しているので GitHub Pages などにデプロイすることで Web 上で 表示できます。 ※ Screenshot の取得には対応していない ので別でやる必要があります。 開発体験 Maestro https://www.widgetbook.io/
  12. Playbook + REG SUIT Playbook は UI コンポーネントのスクリーンショット を撮るためのパッケージ。 REG

    SUIT は Visual Regression Test (VRT) のため のツール。 Playbook で取得したスクリーンショットを REG SUIT で読み込み VRT を行っています。 https://github.com/playbook-ui/playbook-flutter https://github.com/reg-viz/reg-suit 開発体験
  13. Maestro E2E テストが出来るフレームワークです。 YAML でテストケースを書き iOS/Androidエ ミュレータで実行することができます。 YAML で書ける点と YAML

    内で JavaScript を実行することができるので他のツールに比 べると柔軟性が高いです。 開発体験 Maestro https://maestro.mobile.dev/
  14. グローバルステート 基本的にはサーバをグローバルス テートとして捉えているのでリクエス トデータのキャッシュがほとんど解 決してくれるはずです。 ただし、複数のスクリーンなどで使われ るような認証トークンなどには Riverpod で管理しています。 ローカルステート

    スクリーン、コンポーネント内で完 結するデータの管理方法です。よ くある例で UI に表示するローティ ングの状態だったり、ボタンの有 効無効の切り替え用だったりする ものは Flutter Hooks で管理して います。 アーキテクチャ アーキテクチャ サーバリクエストとキャッシュ GraphQL Flutter を利用しています。 GraphQL Flutter はレスポンスデータ のキャッシュもしてくれます。
  15. GraphQL Flutter サーバリクエストとキャッシュは GraphQL Flutter が担ってもらっています。サーバリクエ ストに必要な情報(Firebase Auth の Id

    Token など)以外は基本的にグローバルス テートとしては持たないようにしています。 https://pub.dev/packages/graphql_flutter https://pub.dev/packages/graphql_codegen 開発体験 サーバリクエストとキャッ シュ graphql_flutter graphql_codegen
  16. サーバリクエストと キャッシュ GraphQL Flutter GraphQL の設計については Fragment Colocation を基本設計としており、それぞれ のコンポーネントで必要なデータは

    Fragment としてそれぞれでコンポーネントと 同様の場所で .graphql ファイルを定義して います。 Query は画面から呼びます。 アーキテクチャ Maestro Home Screen User Component Feed Component query Home { user { ...UserParts } feed { ...FeedParts } } # ユーザー情報 fragment UserParts on User { id } mutation CreateAccount { signUp { user { id } } } # フィード情報 fragment FeedParts on Feed { id title body } https://pub.dev/packages/graphql_flutter https://pub.dev/packages/graphql_codegen
  17. GraphQL Flutter 右のディレクトリ構成のようにコンポーネント と同階層のところに .graphql の定義を置い ています。 サーバリクエストと キャッシュ アーキテクチャ

    Maestro lib/ ├── data │ └── schema │ └── main.graphql └── ui └── screen ├── home │ ├── component │ │ ├── item │ │ │ ├── feed_item.dart │ │ │ └── feed_item_fragment.graphql │ │ └── text │ │ ├── user_name.dart │ │ └── user_name_fragment.graphql │ ├── home_query.dart │ └── home_screen.dart ├── mypage └── settings https://pub.dev/packages/graphql_flutter https://pub.dev/packages/graphql_codegen
  18. ローカルステート アーキテクチャ Maestro @override Widget build(BuildContext context) { final isLoading

    = useState(false); ./ ... 何か return Scaffold( body: Stack( children: [ const Text('Body'), if (isLoading.value) const Center( child: CircularProgressIndicator(), ), ], ), ); } https://maestro.mobile.dev/ Flutter Hooks の useState(...) Flutter Hooks 例のようにこのウィジェット内で完結するよう なデータの場合は Flutter Hooks の useState(...) を利用して管理しています。 それ以外にも useContext や useCallback などもよく利用しています。
  19. グローバルステート Riverpod 基本的にはサーバをグローバルステート として捉えているのでリクエストデータの キャッシュがほとんど解決してくれるはず です。 ただし、複数のスクリーンなどで使われるよう な認証トークンなどには Riverpod で管理し

    ています。 アーキテクチャ Maestro https://maestro.mobile.dev/ part 'id_token_state.g.dart'; typedef IdToken = String; @Riverpod(keepAlive: true, dependencies: [firebaseAuth]) class IdTokenState extends _$IdTokenState { @override IdToken build() { return ''; } ./ 初期値は空 Future<void> fetch() async { final user = ref.watch(firebaseAuthProvider).currentUser; if (user .= null) return; update(await user.getIdToken()); } void update(String token) { if (state .= token) state = token; } } Riverpod の Notifier @override Widget build(BuildContext context) { final idToken = ref.watch(idTokenStateProvider); ./ ... 何か }
  20. 全体のディレクトリ構造はこういう形で定義し ています。作るにつれて苦しくなっていく箇所 は随時考え直しています。 ディレクトリ構成 アーキテクチャ Maestro lib/ ├── data/ │

    ├── network/ │ └── system/ ├── foundation/ │ ├── extension/ │ └── firebase/ │ ├── auth/ # Firebase Auth 関連 │ └── messaging/ # Firebase Messaging 関連 ├── state/ # グローバルの状態管理関連 ├── ui/ # UI 関連のモジュール(GraphQL は各画面で定義) │ ├── screen/ │ │ └── home/ │ │ ├── home_screen.dart # 画面 │ │ ├── home_screen_e2e.yaml # Maestro のファイル │ │ ├── home_screen_vrt.dart # Visual Regression Test のファイル │ │ ├── hook/ # 画面固有の Hooks │ │ │ └── use_update_home.dart │ │ ├── component/ # 画面固有のコンポーネント │ │ │ ├── home_navigation.dart │ │ │ ├── home_card.dart │ │ │ └── home_card_fragment.dart # コンポーネントの GraphQL ファイル │ │ └── top │ │ ├── home_top_screen_e2e.yaml │ │ └── home_top_screen.dart │ ├── theme/ # グローバルテーマ設定 │ │ ├── app_theme.dart │ │ └── app_text.dart │ ├── hook/ # 汎用的な Hooks │ │ ├── use_debounce.dart │ │ ├── use_debounce_test.dart │ │ ├── use_sign_in.dart │ │ ├── use_sign_in_test.dart │ │ ├── use_sign_out.dart │ │ └── use_sign_out_test.dart │ └── component/ # 汎用 UI コンポーネント │ ├── fab/ │ └── text/ └── use_case/