Slide 1

Slide 1 text

Flutter の アプリアーキテクチャ現在地点 Tokyo, Japan Daichi Furiya

Slide 2

Slide 2 text

降矢 大地
 @wasabeef @wasabeef_jp CyberAgent, Inc
 - CyberAgent Developer Expert
 - 現在は「ジャンプTOON」の開発中
 
 
 Google Developers Expert for Android
 Wasabeef


Slide 3

Slide 3 text

Flutter 公式のアーキテクチャページ ジャンプTOON のアーキテクチャ 最後に一言 今日話すこと

Slide 4

Slide 4 text

1.他を否定しない 2.変化を歓迎する 3.先人を尊重する 4.(正直) なんでもいい スタンスの表明 まず、話を始める前にスタンスを表明したいと思い ます。 Flutter に限らずアプリのアーキテクチャパターンは 沢山あり、先人たちの知恵がその時代背景だった り、大事思っていることが人それぞれあると思うの で否定はしません。 自分自身、自チームで良いと思ったアーキテクチャ を使うのが一番の正解だと思ってます。

Slide 5

Slide 5 text

Tokyo, Japan Flutter 公式に公開された アーキテクチャを紹介

Slide 6

Slide 6 text

https://link.medium.com/iRZETrmtROb 2020/08 に書いた MVVM の記事

Slide 7

Slide 7 text

https://docs.flutter.dev/app-architecture Flutter 公式ドキュメントにて公開

Slide 8

Slide 8 text

① アーキテクチャによる利点 ② アーキテクチャの原則 ③ Flutter チームが推奨するアプリアーキテクチャ ④ MVVM と状態管理 ⑤ 依存性注入 ⑥ 堅牢なアプリを作るためのデザインパターン Tokyo, Japan

Slide 9

Slide 9 text

① アーキテクチャによる利点 ② アーキテクチャの原則 ③ Flutter チームが推奨するアプリアーキテクチャ ④ MVVM と状態管理 ⑤ 依存性注入 ⑥ 堅牢なアプリを作るためのデザインパターン Tokyo, Japan

Slide 10

Slide 10 text

Flutter チームが推奨するアプリアーキテクチャ https://docs.flutter.dev/app-architecture/guide MVVM (Modal-View-View-Model)

Slide 11

Slide 11 text

UI layer は View と ViewModel で構成します。 ViewModel は状態管理や Repository とのやりとり を行います。 MVVM Data layer は Repository と Service で構成します。 より厳密にいうと Repository 以降は MVVM というより は Repository pattern のことを指します。 Repository は DDD の提唱から一般的になりました。 ViewModel から命令された Repository は Service か らデータ取得を行います。

Slide 12

Slide 12 text

View に対しては 1 つの ViewModel を持ち、ViewModel は複数の Repository を持ちます。 ※ ViewModel が UI 完結の状態管理だけの場合は Repository を必要としないこともあります。 MVVM

Slide 13

Slide 13 text

MVVM この図はオブジェクト同士のデータの流れを説明です。

Slide 14

Slide 14 text

MVVM まずは画面上の初期表示やボタンなどを起因に View から ViewModel へアクションします。 ①

Slide 15

Slide 15 text

MVVM ② アクションを受け取った ViewModel は Repository に対しデータを取得するように指示します。

Slide 16

Slide 16 text

MVVM Repository は Service を使ってサーバなどからデータを取得します。 ③

Slide 17

Slide 17 text

MVVM Service は取得したデータを Repository に返します。 ④

Slide 18

Slide 18 text

MVVM Repository は Service から受け取ったデータをドメインモデルに加工して ViewModel に返します。 ⑤

Slide 19

Slide 19 text

MVVM ViewModel は View に対して画面の状態を更新するように指示します。 ⑥

Slide 20

Slide 20 text

MVVM この図はオブジェクト同士のデータの流れを説明です。 View migi ViewModel へのトリガーからデータの流れが開始されます。 ① ② ③ ④ ⑤ ⑥

Slide 21

Slide 21 text

UI layer - Views View の主な役割は次の通りです。 ● ViewModel のデータに基づいて表示状態を変える ● アニメーション ● 画面サイズや向きなどデバイス情報に基づいたレイアウト ● 画面遷移などのルーティング

Slide 22

Slide 22 text

ViewModel の主な役割は次の通りです。 ● Repository からデータを取得し、 View で表示するためのデータ加工を行う ● View で必要な状態管理(保持)を行い View が再構築できるようにする(ボタンの On/Off など) ● ユーザ操作によるイベントを処理できるようにコールバックを View に公開する ※ ViewModel のデータを直接 View から更新できないようにする UI layer - ViewModels

Slide 23

Slide 23 text

Repository の主な役割は次の通りです。 Repository は Service から取得したデータをドメインモデルに変換する役割担います。 Data layer - Repositories ● データの取得・更新 ● キャッシング ● エラーハンドリング ● リトライ ● ポーリング処理

Slide 24

Slide 24 text

Service の主な役割は次の通りです。 Service はアプリケーションの最下層に位置し、 Repository に対して Future や Steam を公開します。 ● iOS や Android など異なるプラットフォームの API を呼び出し ● サーバなどからデータ取得 ● ローカルストレージなどからのデータ取得 Data layer - Services

Slide 25

Slide 25 text

ViewModel と Repository の間にユースケースを追加する場合もあります。 以下のいずれかのパターンの場合などで利用されます。 ● ViewModel が複数の Repository 持ちデータをマージする場合 ● データ加工などが複雑なロジックの場合 ● 異なる ViewModel で使われる共通処理の場合 Optional: Domain layer - Use-Cases

Slide 26

Slide 26 text

https://docs.flutter.dev/app-architecture

Slide 27

Slide 27 text

Tokyo, Japan ジャンプTOON の アプリアーキテクチャ紹介

Slide 28

Slide 28 text

アーキテクチャ 何にする? 時は 2022 年の年末

Slide 29

Slide 29 text

MVVM?

Slide 30

Slide 30 text

Redux?

Slide 31

Slide 31 text

やったことないものに したいな

Slide 32

Slide 32 text

アーキテクチャを限り なく薄くしたいな

Slide 33

Slide 33 text

Web 界隈に目を向けてみる アーキテクチャってわけじゃないけど、 Web の世界では使われるようになってきたようだから これらのベースに考えてみるのはどうか? TanStack Query は今でこそ色々機能増えてますが、昔の React Query だった時は SWR よりちょっと高機能くら いの立ち位置だった。

Slide 34

Slide 34 text

SWR Sample import useSWR from 'swr' function Profile() { const { data, error, isLoading } = useSWR('/api/user', fetcher) if (error) return
failed to load./div> if (isLoading) return
loading..../div> return
hello {data.name}!./div> } コンポーネントでサーバリクエストを(キャッシュの確認)して UI として表示する SWR のサンプルです。 これによりコンポーネントのポータビリティ性を上げて取り回しをしやすくする。

Slide 35

Slide 35 text

まずは状態管理を どうするか考える

Slide 36

Slide 36 text

クライアントの状態管理とキャッシュを整理する ローカルステート スクリーン、コンポーネント内 で完結するデータの管理方 法です。よくある例で UI に表 示するローティングの状態 だったり、ボタンの有効無効 の切り替え用だったりするも のは Flutter Hooks で管理す る。 グローバルステート 基本的にはサーバをグローバ ルステートとして捉えているの でリクエストデータのキャッシュ がほとんど解決してくれるはず です。 ただし、複数のスクリー ンなどで使われるような認証 トークンなどには Riverpod で 管理する。 サーバリクエストとキャッシュ GraphQL Flutter を利用してい ます。GraphQL Flutter はレス ポンスデータのキャッシュもし てくれます。 React Hooks 参考 Recoil 参考 SWR/TanStack 参考

Slide 37

Slide 37 text

ローカルステート Flutter Hooks

Slide 38

Slide 38 text

Widget build(BuildContext context) { final visibility = useState(false); |/ ||. 何か return Scaffold( body: Stack( children: [ const Text('Body'), if (visibility.value) const Center(child: CircularProgressIndicator()), ], ), ); } 先程の MVVM の ViewModel で持つような Widget の表示状態を管理するものに関しては Flutter Hooks の useState などで全部管理してみる。

Slide 39

Slide 39 text

グローバルステート Riverpod

Slide 40

Slide 40 text

@Riverpod(keepAlive: true, dependencies: [firebaseAuth]) class IdTokenState extends _$IdTokenState { @override IdToken build() { return ''; } |/ 初期値は空 Future 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; } } これも先程の MVVM でいう ViewModel で持つようなアプリが起動中は保持しておきたい ユーザ情報だったり、 Firebase のトークンだったりは Riverpod で管理してみる。

Slide 41

Slide 41 text

1 年くらいこの形で実 装して、現在もそう なっているが ...

Slide 42

Slide 42 text

グローバルステートと しての Riverpod 辞 めたいかも ..

Slide 43

Slide 43 text

Riverpod には大きく分けて、DI と状態管理の側面(機能)がある。 便利だから使っているけど。。 DI 機能を使う分の Riverpod には良いと思っている。 グローバルステートに関しては Riverpod じゃなくてもっと Hooks に寄せていきたい。た だ、先の説明した使い方の useState だとグローバルステートにはならないので、Flutter の InheritedWidget で useState を用いた(ような)形に変更したい。 グローバルステートの今後の修正したいポイント

Slide 44

Slide 44 text

まだチームメンバーには提案で きるほどの形にはなってないの で自分への宿題 ..

Slide 45

Slide 45 text

サーバリクエストとキャッ シュ GraphQL

Slide 46

Slide 46 text

GraphQL を使う前提です。 サーバリクエストとキャッシュは GraphQL Flutter が担ってもらっています。 サーバリクエストに必要な情報(Firebase Auth の Id Token など)以外は基本的にグ ローバルステートとしては持たないようにしています。 GraphQL Flutter には Flutter Hooks を使って TanStack にもある useQuery が用意さ れているのも選定した理由の大きな要因です。 利用ライブラリ:graphql_flutter、graphql_codegen サーバリクエストとキャッシュ

Slide 47

Slide 47 text

GraphQL を用いた設計の基本思想は Fragment Colocation を参考にしています。 - ホーム画面:Query - コンポーネント:Fragment と、それぞれデータが必要な Widget に対して .graphql ファイルを定義しています。 # ユーザー情報 fragment UserParts on User { id } mutation CreateAccount { signUp { user { id } } } # フィード情報 fragment FeedParts on Feed { id title body } # ホーム画面 query Home { user { ...UserParts } feed { ...FeedParts } } GraphQL ファイル

Slide 48

Slide 48 text

GraphQL Flutter と GraphQL Codegen を 使っています。 GraphQL Flutter には Flutter Hooks ベースで 書かれた useQuery が存在しています。 GraphQL Codegen は GraphQL のスキーマ ファイルから Dart を出力してくれて、 尚且つ useQuery を一部拡張し useQuery$Home という形で Home 用のクエリ オプションを設定しやすくしてくれる機構もありま す。 それらを組み合わせ使ってます。 ※ 詳しく知りたい方は後述するブログを Widget build(BuildContext context, WidgetRef ref) { final options = ref.watch(homeQueryOptionsProvider); final query = useQuery$Home(options); return Scaffold( body: GraphQLQueryContainer( query: query, onLoadingWidget: SkeletonHomeScreen(), onErrorWidget: (error, stackTrace) .> ErrorContainer( error: error, stackTrace: stackTrace, onAction: query.refetch, ), child: (data) { return ListView(...): }, ), ); } useQuery

Slide 49

Slide 49 text

ディレクトリ構成

Slide 50

Slide 50 text

lib/ ├── data/ # データソース、 GraphQL のスキーマなど ├── foundation/ # 共通処理( Firebase etc.)など ├── gen/ # FlutterGen など一部の自動生成ファイル ├── l10n/ # Localization 関連 ├── route/ # auto_route 関連 ├── state/ # グローバルの状態管理関連 ├── ui/ # UI 関連のモジュール( GraphQL は各画面で定義) │ ├── screen/ │ │ └── home/ │ │ ├── home_screen.dart # 画面 │ │ ├── home_screen_query.graphql # GraphQL のクエリ │ │ ├── hook/ # 画面固有の Hooks │ │ │ └── use_update_home.dart │ │ └── component/ # 画面固有のコンポーネント │ │ ├── home_card.dart │ │ └── home_card_fragment.dart # コンポーネントの GraphQL Fragment │ ├── theme/ # グローバルテーマ設定 │ ├── hook/ # 汎用的な Hooks │ │ ├── use_sign_in.dart │ │ ├── use_sign_in_test.dart │ │ ├── use_sign_out.dart │ │ └── use_sign_out_test.dart │ └── component/ # 汎用 UI コンポーネント │ ├── fab/ │ └── text/ └── use_case/ # main などで使うロジック 基本的な思想は co-location です。 関係者(関係するファイル)は近くに置くようにしてい ます。保守性・可読性を高める狙いがあります。 以下は例 - GraphQL Query + 画面.dart - GraphQL Fragment + コンポーネント.dart - use_xxx.dart + use_xxx_test Dart の場合は test ファイルは test/ ディレクトリに おかないと扱いづらいですが、テストを実行する際に は test ファイルをコピーするような処理を入れてい ます。 ディレクトリ構成

Slide 51

Slide 51 text

ビジネスロジックは どこで持つか

Slide 52

Slide 52 text

基本的にロジックは Flutter Hooks でカスタムフックとして作り、 Widget 側で使おう。 ||/ 開始時間と終了時間から公開中かどうか判定する bool usePublishing({ int? starMilli, int? endMilli,DateTime? now}) { return useMemoized(() { if (starMilli |= null) return false; final nowMs = (now |? DateTime.now()).epochMilli(); return starMilli |= nowMs |& (endMilli |= null || nowMs < endMilli); }, [starMilli, endMilli, now], ); }

Slide 53

Slide 53 text

このテストどう書こう ..

Slide 54

Slide 54 text

flutter_hooks_test

Slide 55

Slide 55 text

https://developers.cyberagent.co.jp/blog/archives/48761/ https://developers.cyberagent.co.jp/blog/archives/48956/ https://developers.cyberagent.co.jp/blog/archives/49290/

Slide 56

Slide 56 text

Tokyo, Japan 最後に一言だけ..

Slide 57

Slide 57 text

アーキテクチャは 自分で良いと思った ものが一番いい

Slide 58

Slide 58 text