Slide 1

Slide 1 text

© LayerX Inc. Effective Form Flutterによる複雑なフォーム開発の実践 たまねぎ(@_chocoyama)

Slide 2

Slide 2 text

© LayerX Inc. 2 所属 株式会社LayerX バクラク事業部 申請‧経費精算開発 略歴 iOS 10年(ヤフー株式会社 → STORES株式会社) Flutter 1.5年(現職) 趣味 ⽿で楽しむもの全般(⾳楽‧ラジオ) 「Podcast エンジニア 関⻄⼈」で検索! たまねぎ(@_chocoyama) ⾃⼰紹介、会社‧プロダクト紹介

Slide 3

Slide 3 text

3 © LayerX Inc. すべての経済活動を、デジタル化する。 会社概要 会社名 株式会社LayerX(レイヤーエックス) 代表取締役 代表取締役CEO 福島 良典 代表取締役CTO 松本 勇気 創業 2018年 8⽉1⽇ 資本⾦ 約132.6億円 拠点 東京本社 〒104-0045 東京都中央区築地1-13-1 銀座松⽵スクエア 5階 関⻄⽀社 〒530-0003 ⼤阪市北区堂島1-1-5 関電不動産梅⽥新道ビル B2F WORKING SWITCH ELK 内 中部⽀社 〒466-0064 愛知県名古屋市昭和区鶴舞1-2−32 STATION Ai内 九州⽀社 〒810-0801 福岡県福岡市博多区中洲3-7-24 WeWorkゲイツ福岡 11F 内 従業員数 338名 (2024年6⽉末時点)

Slide 4

Slide 4 text

© LayerX Inc.  4 「バクラク」シリーズラインナップ ‧AIが請求書を5秒でデータ化 ‧仕訳 / 振込データを⾃動作成 ‧電帳法‧インボイス制度にも対応 仕訳‧⽀払処理効率化 ‧年会費無料で何枚でも発⾏可 ‧カード利⽤制限で統制を実現 ‧すべての決済で1%以上の還元 法⼈カードの発⾏‧管理 ‧帳票の⼀括作成も個別作成も⾃由⾃在 ‧帳票の作成‧稟議‧送付‧保存を⼀本化 ‧レイアウトや項⽬のカスタマイズも可能 請求書発⾏ ‧スキャナ保存データも直接取込  ‧AI-OCRが⾃動読取&データ化 ‧[取引先][取引⽇][取引⾦額]での検索 帳票保存‧ストレージ ‧AIが⾒積書‧請求書を5秒でデータ ‧スマホからも申請‧承認OK ‧柔軟な通知設定‧承認の催促機能 稟議‧⽀払申請 ‧直感的UIで従業員の負担を軽減 ‧Slack連携で打刻や⾃動リマインド可能 ‧わかりやすい残業 / 休暇管理レポート 勤怠管理 ‧AIが領収書を5秒でデータ化 ‧スマホアプリとSlack連携あり ‧領収書の重複申請などミス防⽌機能 経費精算

Slide 5

Slide 5 text

© LayerX Inc. 5 バクラク申請‧経費精算アプリ、リニューアル中! 9⽉17⽇にリリースしました ご希望に応じて、従来のアプリと並⾏してご利⽤が可能です!

Slide 6

Slide 6 text

Flutterでの⼀般的なフォーム開発

Slide 7

Slide 7 text

© LayerX Inc. 7 ● ユーザー⼊⼒を受け取るためのインターフェース ● TextField、Checkbox、RadioButtonなど、様々な⼊⼒フィールドを組み合わせたもの ● フォームを通すことでユーザーはサービスに柔軟なデータ送信を⾏うことができる ”フォーム”とは Flutterでの⼀般的なフォーム開発

Slide 8

Slide 8 text

© LayerX Inc. 8 おおまかな実装の流れ Flutterでの⼀般的なフォーム開発 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit!

Slide 9

Slide 9 text

© LayerX Inc. 9 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ログインフォームの例 TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ); TextField( obscureText: true, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), );

Slide 10

Slide 10 text

© LayerX Inc. 10 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ); TextField( obscureText: true, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), ); ⼊⼒状態を保持して参照する必要がある ● Stateful Widget ● flutter_hooks ● Riverpod ● etc… ログインフォームの例

Slide 11

Slide 11 text

© LayerX Inc. 11 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! import 'package:flutter_hooks/flutter_hooks.dart'; final emailController = useTextEditingController(); TextField( controller: emailController, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ); FilledButton( onPressed: () { final email = emailController.value.text; final password = passwordController.value.text; login(email: email, password: password); }, child: const Text("Login"), ), flutter_hooksによる⼊⼒状態の管理 ephemeral stateを作成 特定のタイミングで参照

Slide 12

Slide 12 text

© LayerX Inc. 12 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ● ⼊⼒されたデータが、特定のルールや条件に合致しているかどうかを確認するプロセス ● サーバーサイドバリデーション → セキュリティやデータの⼀貫性を担保できる ● クライアントサイドバリデーション → ユーザーに早期フィードバックをしてより良い体験を提供できる バリデーション 正しい形式か? 登録済みでないか? 空欄のままじゃないか? 不正な⽂字が含まれていないか?

Slide 13

Slide 13 text

© LayerX Inc. 13 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! シンプルなメアドバリデーション import 'package:email_validator/email_validator.dart'; final emailController = useTextEditingController(); final isValidEmail = useListenableSelector( emailController, () => EmailValidator.validate(emailController.value.text), ); TextField( controller: emailController, decoration: InputDecoration( border: const OutlineInputBorder(), labelText: 'Email', errorText: isValidEmail ? null : 'Invalid email', ), ), 有効 無効 ※GlobalKeyへの依存を許容すれば、標準のForm+FormFieldでも同様の対応は可能

Slide 14

Slide 14 text

© LayerX Inc. 14 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! シンプルなメアドバリデーション 初期状態でいきなり Invalid! ユーザーフィードバックタイミングをコントロールしたい

Slide 15

Slide 15 text

© LayerX Inc. 15 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ● ⼊⼒フィールドの内容がどのタイミングで「変更が確定された」かを管理 ● ユーザーが変更を確定したタイミングで「dirtyになった」とみなしてdirty状態に更新する ● 以下のような体験向上に役⽴つ ○ エラーメッセージ表⽰タイミングのコントロール ○ 変更確認ダイアログの表⽰ Dirty管理によるフィードバックタイミングのコントロール

Slide 16

Slide 16 text

© LayerX Inc. 16 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! formzを⽤いたDirty管理 ⼊⼒値(pure / dirty) バリデーションロジック import 'package:formz/formz.dart'; class EmailInput extends FormzInput { const EmailInput.pure({String value = ''}) : super.pure(value); const EmailInput.dirty({String value = ''}) : super.dirty(value); @override EmailInputError? validator(String value) { return EmailValidator.validate(value) ? null : EmailInputError.invalid; } } enum EmailInputError { invalid } バリデーションエラーのパターン

Slide 17

Slide 17 text

© LayerX Inc. 17 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! formzを⽤いたDirty管理 const emailInput = EmailInput.pure(); print(emailInput.value); // '' print(emailInput.isValid); // false print(emailInput.error); // EmailInputError.invalid print(emailInput.displayError); // null const emailInput = EmailInput.dirty(); print(emailInput.value); // '' print(emailInput.isValid); // false print(emailInput.error); // EmailInputError.invalid print(emailInput.displayError); // EmailInputError.invalid 同⼀の値でも dirtyの時だけエラーが取れる 💡うれしいポイント 内部的にはエラー扱いにしつつ、UI表⽰は遅延可能

Slide 18

Slide 18 text

© LayerX Inc. 18 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! formzを⽤いたDirty管理 final emailInput = useState(const EmailInput.pure()); TextField( controller: emailController, onChanged: (value) => emailInput.value = EmailInput.pure(value: value), onSubmitted: (value) => emailInput.value = EmailInput.dirty(value: value), decoration: InputDecoration( border: const OutlineInputBorder(), labelText: 'Email', errorText: switch (emailInput.value.displayError) { EmailInputError.invalid => 'Invalid email', null => null, }, ), ) ⼊⼒中 確定後 でもログイン処理⾃体は ブロックしたい... submitタイミングでdirtyに → dirty時のみエラー表⽰ submitしない限りはpureのまま

Slide 19

Slide 19 text

© LayerX Inc. 19 FilledButton( onPressed: emailInput.value.isValid ? () { final email = emailController.value.text; final password = passwordController.value.text; login(email: email, password: password); } : null, child: const Text("Login"), ) UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! formzを⽤いたDirty管理 isValid = false isValid = true 内部的にはエラーの状態

Slide 20

Slide 20 text

© LayerX Inc. 20 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! formzを⽤いたDirty管理 初期表⽰ ⼊⼒中 確定後 すべてValid

Slide 21

Slide 21 text

これで完璧!

Slide 22

Slide 22 text

ほんとうに?

Slide 23

Slide 23 text

リアルワールドの フォームを⾒てみよう

Slide 24

Slide 24 text

© LayerX Inc. 24 申請作成(App) フォーム設定(Web) バクラク クイズタイム!

Slide 25

Slide 25 text

© LayerX Inc. 25 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. デフォルト値をセットするかのチェック 3. 別のフィールドの値を元に、動的に対象フィールドを表⽰すべきかを判定 4. 別のフィールドの値を元に、動的に対象フィールドを必須にするかどうかを判定

Slide 26

Slide 26 text

© LayerX Inc. 26 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. デフォルト値をセットするかのチェック 3. 別のフィールドの値を元に、動的に対象フィールドを表⽰すべきかを判定 4. 別のフィールドの値を元に、動的に対象フィールドを必須にするかどうかを判定

Slide 27

Slide 27 text

© LayerX Inc. 27 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. アップロード上限数を超えていないかのチェック 3. アップロードしたファイルにOCRをかけて、読み取った値を別のフィールドの値としてセットする 4. アップロードしたファイル内にインボイス登録事業者番号が記載されているかのフィードバック 5. 電⼦帳簿保存法という法律で定められた基準を満たしているかのチェック

Slide 28

Slide 28 text

© LayerX Inc. 28 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. アップロード上限数を超えていないかのチェック 3. アップロードしたファイルにOCRをかけて、読み取った値を別のフィールドの値としてセットする 4. アップロードしたファイル内にインボイス登録事業者番号が記載されているかのフィードバック 5. 電⼦帳簿保存法という法律で定められた基準を満たしているかのチェック

Slide 29

Slide 29 text

© LayerX Inc. 29 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. 許容する最⼤値を超えていないかのチェック 3. マイナス値や⼩数値を許可するかの判定 4. ⽇本円以外を許可するかのチェック 5. 別のフィールドでアップロードしたファイルからOCRで読み取った値を利⽤しているかの判定 6. 別のフィールドの⼊⼒有無で、変更をできないようにする制御 7. ここの⼊⼒値を元に、⾃動算出して表⽰するフォームの制御 8. ユーザーにより設定された閾値を超えた場合、特定のフィールドの値を決められたものにする制御

Slide 30

Slide 30 text

© LayerX Inc. 30 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. 許容する最⼤値を超えていないかのチェック 3. マイナス値や⼩数値を許可するかの判定 4. ⽇本円以外を許可するかのチェック 5. 別のフィールドでアップロードしたファイルからOCRで読み取った値を利⽤しているかの判定 6. 別のフィールドの⼊⼒有無で、変更をできないようにする制御 7. ここの⼊⼒値を元に、⾃動算出して表⽰するフォームの制御 8. ユーザーにより設定された閾値を超えた場合、特定のフィールドの値を決められたものにする制御

Slide 31

Slide 31 text

現実のフォーム開発は そう単純じゃない

Slide 32

Slide 32 text

© LayerX Inc. 32 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ‧多種多様なアラート ‧フォーム設定や フォーム種別に応じた 動的UI ‧フォームバージョン の管理

Slide 33

Slide 33 text

© LayerX Inc. 33 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ‧多種多様なアラート ‧フォーム設定や フォーム種別に応じた 動的UI ‧フォームバージョン の管理 ‧画⾯間での共有 ‧⼊⼒状態の条件⼀致 による⾃動更新 ‧⼊⼒経路の管理 (OCR, 履歴, ⼿動, …) ‧複数の種類の値 (⽇本円,外貨,…) ‧状態の復元 (下書き復帰, …)

Slide 34

Slide 34 text

© LayerX Inc. 34 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ‧多種多様なアラート ‧フォーム設定や フォーム種別に応じた 動的UI ‧フォームバージョン の管理 ‧フォーム設定に応じ た複雑な制御 ‧別フィールドの値に 依存した動的チェック ‧通信を介した⾮同期 確認 ‧画⾯間での共有 ‧⼊⼒状態の条件⼀致 による⾃動更新 ‧⼊⼒経路の管理 (OCR, 履歴, ⼿動, …) ‧複数の種類の値 (⽇本円,外貨,…) ‧状態の復元 (下書き復帰, …)

Slide 35

Slide 35 text

© LayerX Inc. 35 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ‧多種多様なアラート ‧フォーム設定や フォーム種別に応じた 動的UI ‧フォームバージョン の管理 ‧フォーム設定に応じ た複雑な制御 ‧別フィールドの値に 依存した動的チェック ‧通信を介した⾮同期 確認 ‧コンテキストに応じ たタイミング制御 ‧画⾯間での共有 ‧⼊⼒状態の条件⼀致 による⾃動更新 ‧⼊⼒経路の管理 (OCR, 履歴, ⼿動, …) ‧複数の種類の値 (⽇本円,外貨,…) ‧状態の復元 (下書き復帰, …)

Slide 36

Slide 36 text

© LayerX Inc. 36 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! ‧多種多様なアラート ‧フォーム設定や フォーム種別に応じた 動的UI ‧フォームバージョン の管理 ‧フォーム設定に応じ た複雑な制御 ‧別フィールドの値に 依存した動的チェック ‧通信を介した⾮同期 確認 ‧コンテキストに応じ たタイミング制御 ‧新規作成 ‧更新 ‧コピー ‧第三者による修正 ‧画⾯間での共有 ‧⼊⼒状態の条件⼀致 による⾃動更新 ‧⼊⼒経路の管理 (OCR, 履歴, ⼿動, …) ‧複数の種類の値 (⽇本円,外貨,…) ‧状態の復元 (下書き復帰, …)

Slide 37

Slide 37 text

© LayerX Inc. 37

Slide 38

Slide 38 text

どう⽴ち向かっていくか

Slide 39

Slide 39 text

© LayerX Inc. 39 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い 状態更新による多彩な動的制御が必要 ⼤量の機能と複雑な依存

Slide 40

Slide 40 text

© LayerX Inc. 40 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い → 状態をどのように持つか 状態更新による多彩な動的制御が必要 ⼤量の機能と複雑な依存

Slide 41

Slide 41 text

1. Single Source Of Truthの担保

Slide 42

Slide 42 text

© LayerX Inc. 42 ● Single Source Of Truth:信頼できる唯⼀の情報源 ○ 信頼できる唯⼀の情報源 (英語: single source of truth、SSOT) とは、情報システムの設計と理論においては、すべてのデータが1か所でのみ作 成、あるいは編集されるように、情報モデルと関連するデータスキーマとを構造化する⽅法である。(from. Wikipedia) ● 管理すべき状態が多いと、各コンポーネントが同じ状態を重複管理する可能性 ● 状態の⼀貫性が損なわれ、データや表⽰上の不整合が発⽣ Single Source Of Truthの担保 1.SSOTの担保 状態は⼀箇所でのみ管理 各コンポーネントは共通の状態を参照‧更新する

Slide 43

Slide 43 text

© LayerX Inc. 43 1.SSOTの担保 判定処理 判定処理 ⼊⼒値 更新値 同じ状態や結果を複数の場所で作らない 必要な処理の呼び出し忘れ データや表⽰不整合の懸念 ロジック差異による挙動差分

Slide 44

Slide 44 text

© LayerX Inc. 44 状態を⼀元管理する 1.SSOTの担保 判定結果 ⼊⼒値 状態を⼀元管理 データや表⽰に⼀貫性を保つ 同⼀の判定結果を参照

Slide 45

Slide 45 text

© LayerX Inc. 45 1.SSOTの担保 状態を1元管理することで、 データや表⽰に⼀貫性を保つ ● 状態は共通の値を参照‧更新 ● それぞれで判定するのではなく、同 ⼀の判定結果を参照 https://riverpod.dev/ 判定結果 ⼊⼒値 Riverpod

Slide 46

Slide 46 text

© LayerX Inc. 46 1.SSOTの担保 final emailInput = useState(const EmailInput.pure()); final passwordInput = useState(const PasswordlInput.pure()); final isValid1 = useMemoized( () => Formz.validate([emailInput.value, passwordInput.value]), [emailInput.value, passwordInput.value], ); final isValid2 = useMemoized( () => Formz.validate([emailInput.value, passwordInput.value]), [emailInput.value, passwordInput.value], ); 各画⾯が状態や結果をもつ BAD

Slide 47

Slide 47 text

© LayerX Inc. 47 1.SSOTの担保 final loginFormState = ref.watch(loginFormStateProvider); final isValid1 = loginFormState.isValid; final loginFormState = ref.watch(loginFormStateProvider); final isValid2 = loginFormState.isValid; final loginFormStateNotifier = ref.read(loginFormStateProvider.notifier); loginFormStateNotifier.updateEmail(email); @riverpod class LoginFormState extends _$LoginFormState { @override LoginFormInputs build() => const LoginFormInputs( email: "", password: "", ); void updateEmail(String email) { // ... } } 状態はProviderでのみ管理される フォーム全体で⼀貫性を担保できる Good

Slide 48

Slide 48 text

© LayerX Inc. 48 1.SSOTの担保 @freezed class RequestFormState with _$RequestFormState { const factory RequestFormState({ @Default([]) List defaultFieldInputStates, @Default([]) List formFieldInputStates, RequestFormRequestDetailsFieldInputState? requestDetailsInputState, // … }) = _RequestFormState; const RequestFormState._(); } class RequestFormAsyncNotifier extends AutoDisposeFamilyAsyncNotifier< RequestFormState, RequestFormStateProviderDependencies> { RequestFormAsyncNotifier(); @override FutureOr build( RequestFormStateProviderDependencies arg, ) async { // ... } } バクラクの場合 画⾯に応じて表⽰に差異がでない フォームの状態をGlobalStateとして⼀元管理

Slide 49

Slide 49 text

© LayerX Inc. 49 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い → 状態をどのように持つか 状態更新による多彩な動的制御が必要 → 状態をどうコントロールするか ⼤量の機能と複雑な依存 Single Source Of Truthの担保 状態の⼀貫性が担保できる

Slide 50

Slide 50 text

2. 宣⾔的な状態管理の徹底

Slide 51

Slide 51 text

© LayerX Inc. 51 2.宣⾔的な状態管理の徹底 宣⾔的な状態管理の徹底 状態決定後、命令的な呼び出しで 反映する対応が必要 BAD import UIKit class AlbumDetailViewController: UITableViewController { private var album: Album // ... func refresh() async { album = await fetchAlbum() tableView.reloadData() } }

Slide 52

Slide 52 text

© LayerX Inc. 52 2.宣⾔的な状態管理の徹底 宣⾔的な状態管理の徹底 import SwiftUI struct AlbumDetail: View { @State var album: Album var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } }.refreshable { album = await fetchAlbum() } } } import UIKit class AlbumDetailViewController: UITableViewController { private var album: Album // ... func refresh() async { album = await fetchAlbum() tableView.reloadData() } } 状態が決まれば 表⽰が決まることを徹底 Good

Slide 53

Slide 53 text

© LayerX Inc. 53 2.宣⾔的な状態管理の徹底 宣⾔的な状態管理の徹底 import SwiftUI struct AlbumDetail: View { @State var album: Album var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } }.refreshable { album = await fetchAlbum() } } } import UIKit class AlbumDetailViewController: UITableViewController { private var album: Album // ... func refresh() async { album = await fetchAlbum() tableView.reloadData() } } Flutterは宣⾔的UIフレームワークだが、 reloadやvalidateなどを命令的に呼び出すことで 状態更新を担保する設計にはできてしまう。 状態が決まれば 表⽰が決まることを徹底 Good

Slide 54

Slide 54 text

© LayerX Inc. 54 2.宣⾔的な状態管理の徹底 class ApprovalReminderDefaultFieldCompositeState extends RequestFormCompositeState implements AllRequestFormCompatible { ApprovalReminderDefaultFieldCompositeState( ApprovalReminderDefaultFieldInputState inputState, List? holidays, ) : super._( inputState.input, inputState.compositeField, inputState.input.validator .bind(requiredDefaultField(inputState.field.option)) .and(minDateTime(clock.now())) .and(recommendedWeekend(holidays)), ); } ⼊⼒状態が決まればバリデーション結果が決まっている (エラーのフィードバックはdirty管理でコントロール) バクラクの場合 明⽰的なバリデーションが必要ないため、 ⼀貫した結果になる 経路(下書き復帰など)に応じて 壊れるリスクが少ない

Slide 55

Slide 55 text

© LayerX Inc. 55 2.宣⾔的な状態管理の徹底 状態のパターンも型化して宣⾔的に⾏う 状態に複数のパターンが存在する場合 フィールド値によって判断しない Typeごとにどの値が利⽤できるか 実装に依存して判断することになる BAD enum InputType { manual, system } class InputState { InputState({required this.type, this.manualOnlyValue}); final InputType type; final String? manualOnlyValue; } void handle(InputState state) { switch (state.type) { case InputType.manual: print(state.manualOnlyValue!); case InputType.system: break; } }

Slide 56

Slide 56 text

© LayerX Inc. 56 2.宣⾔的な状態管理の徹底 状態のパターンも型化して宣⾔的に⾏う sealed class InputState {} class ManualInputState implements InputState { const ManualInputState({ required this.manualOnlyValue, }); final String manualOnlyValue; } class SystemInputState implements InputState {} void handle(InputState state) { switch (state) { case ManualInputState(): print(state.manualOnlyValue); case SystemInputState(): break; } } 型レベルで切り分け sealed classを活⽤ InputStateの型で取り回し、 使う時にswitchする enum InputType { manual, system } class InputState { InputState({required this.type, this.manualOnlyValue}); final InputType type; final String? manualOnlyValue; } void handle(InputState state) { switch (state.type) { case InputType.manual: print(state.manualOnlyValue!); case InputType.system: break; } } Good 各状態の対応が宣⾔的になる

Slide 57

Slide 57 text

© LayerX Inc. 57 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い → 状態をどのように持つか ⼤量の機能と複雑な依存 → 状態をどう切り分けるか Single Source Of Truthの担保 状態の⼀貫性が担保できる 状態更新による多彩な動的制御が必要 → 状態をどうコントロールするか 宣⾔的な状態管理の徹底 状態が決まれば挙動が決まる

Slide 58

Slide 58 text

3. 関⼼範囲の局所化

Slide 59

Slide 59 text

© LayerX Inc. 59 3.関⼼範囲の局所化 関⼼範囲の局所化 画⾯A A/B共通の状態 A固有の状態 B固有の状態 画⾯B あらゆる状態を⼀箇所で管理

Slide 60

Slide 60 text

© LayerX Inc. 60 3.関⼼範囲の局所化 関⼼範囲の局所化 画⾯A A/B共通の状態 A固有の状態 B固有の状態 画⾯B あらゆる状態を⼀箇所で管理 画⾯A A/B共通の状態 画⾯B A固有の状態 B固有の状態 関⼼領域ごとに切り分けて管理

Slide 61

Slide 61 text

© LayerX Inc. 61 3.関⼼範囲の局所化 関⼼範囲の局所化 画⾯A A/B共通の状態 A固有の状態 B固有の状態 画⾯B あらゆる状態を⼀箇所で管理 画⾯A A/B共通の状態 画⾯B A固有の状態 B固有の状態 AppState Ephemeral State 関⼼領域ごとに切り分けて管理

Slide 62

Slide 62 text

© LayerX Inc. 62 ● AppState ○ Riverpodで実現 ○ 特定の画⾯に依存せず、グローバルな状態として保持したいものはこっち ● EphemeralState ○ flutter_hooksで実現 ○ 特定の画⾯に依存しており、ローカルな状態として保持するだけで⼗分なものはこっち AppStateとEphemeralState 3.関⼼範囲の局所化

Slide 63

Slide 63 text

© LayerX Inc. 63 AppStateの肥⼤化対策 3.関⼼範囲の局所化 画⾯A AppStateA 画⾯B AppStateC AppStateB 分割 レイヤー化

Slide 64

Slide 64 text

© LayerX Inc. 64 AppStateの肥⼤化対策 3.関⼼範囲の局所化 申請承認画⾯ フォーム設定 申請作成画⾯ 合成された フォーム状態 ⼊⼒値のまとまり ユーザーデータ バクラクの場合

Slide 65

Slide 65 text

© LayerX Inc. 65 AppStateの肥⼤化対策 3.関⼼範囲の局所化 申請承認画⾯ フォーム設定 申請作成画⾯ 合成された フォーム状態 ⼊⼒値のまとまり ユーザーデータ バクラクの場合 final requestFormCompositeStateProvider = Provider.autoDispose.family< List>, RequestFormStateProviderDependencies>( (ref, dep) { final user = ref.watch(userProvider(dep)); final formSetting = ref.watch(requestFormSettingProvider(dep)); final inputStates = ref.watch(requestFormInputStateProvider(dep)); // ... }); AppStateの責任領域を最⼩化 意図しない影響の発⽣を最⼩化

Slide 66

Slide 66 text

© LayerX Inc. 66 Componentの分割 3.関⼼範囲の局所化 汎⽤申請 経費精算申請 ⽀払申請 動的に切り替わるUIパターンが膨⼤ 1Widgetでハンドリングすると分岐地獄に BAD

Slide 67

Slide 67 text

© LayerX Inc. 67 Componentの分割 3.関⼼範囲の局所化 汎⽤申請 経費精算申請 ⽀払申請 フォーム請種別ごとに切り分け Good

Slide 68

Slide 68 text

© LayerX Inc. 68 Componentの分割 3.関⼼範囲の局所化 汎⽤申請 経費精算申請 ⽀払申請 フォーム種別ごとに切り分け ⼊⼒種別ごとに切り分け Good

Slide 69

Slide 69 text

© LayerX Inc. 69 3.関⼼範囲の局所化 フォーム種別ごとの Component Page ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component バクラクの場合

Slide 70

Slide 70 text

© LayerX Inc. 70 3.関⼼範囲の局所化 Page フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component Form( child: switch (formType) { FormType.OTHER => OtherRequestForm( compositeStates: compositeStates, requestFormDependencies: requestFormDependencies, ), FormType.EXPENSE => ExpenseRequestForm( compositeStates: compositeStates, requestFormDependencies: requestFormDependencies, ), FormType.PAYMENT => PaymentRequestForm( compositeStates: compositeStates, requestFormDependencies: requestFormDependencies, ), // ... }, ) バクラクの場合 フォーム種別ごとに表⽰されうる ⼊⼒種別が異なる Widgetレベルで種別ごとに切り分け

Slide 71

Slide 71 text

© LayerX Inc. 71 3.関⼼範囲の局所化 フォーム種別ごとの Component Page ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component バクラクの場合 for (final compositeState in compositeStates.whereExpenseCompatible()) switch (compositeState) { TitleDefaultFieldCompositeState() => RequestTitleFormField( compositeState: compositeState, dependencies: requestFormDependencies, ), RequestDetailsFieldCompositeState() => RequestDetailsExpenseFormField( compositeState: compositeState, dependencies: requestFormDependencies, ), // ... }, for (final compositeState in compositeStates.wherePaymentCompatible()) switch (compositeState) { TitleDefaultFieldCompositeState() => RequestTitleFormField( compositeState: compositeState, dependencies: requestFormDependencies, ), JpyPaymentAmountDefaultFieldCompositeState() => RequestJpyPaymentAmountFormField( compositeState: compositeState, dependencies: requestFormDependencies, ), // ... }, 種別ごとに 必要な⼊⼒フィールドが異なる コンポーネントのまとまりで 切り分け

Slide 72

Slide 72 text

© LayerX Inc. 72 3.関⼼範囲の局所化 フォーム種別ごとの Component Page ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component バクラクの場合 個別の処理は 各コンポーネント側に委譲 状態管理はカスタムHooksで実現 class RequestForexPaymentAmountFormField extends HookConsumerWidget { const RequestForexPaymentAmountFormField({/** */}); // ... @override Widget build(BuildContext context, WidgetRef ref) { final ( state: ( :controller, :isNegativeAmount, ), action: ( :onChangedText, :onChangedSign, ), ) = useForexPaymentAmountFormField(/** */); // ... } } flutter_hooksを活⽤

Slide 73

Slide 73 text

© LayerX Inc. 73 3.関⼼範囲の局所化 フォーム種別ごとの Component Page ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component バクラクの場合 typedef ForexPaymentAmountFormFieldState = ({ TextEditingController controller, bool isNegativeAmount, }); typedef ForexPaymentAmountFormFieldAction = ({ Function(String) onChangedText, Function(({bool isNegative, String? text})) onChangedSign, }); ({ ForexPaymentAmountFormFieldState state, ForexPaymentAmountFormFieldAction action, }) useForexPaymentAmountFormField({/** */}) { final controller = useTextEditingController(/** */); final isNegativeAmount = useState(/** */); // ... return ( state: ( controller: controller, isNegativeAmount: isNegativeAmount.value, ), action: ( onChangedText: (text) => /** */, onChangedSign: useCallback(/** */), ) ); } AppStateにする必要がないものを切り離して、 依存を最⼩化 class RequestForexPaymentAmountFormField extends HookConsumerWidget { const RequestForexPaymentAmountFormField({/** */}); // ... @override Widget build(BuildContext context, WidgetRef ref) { final ( state: ( :controller, :isNegativeAmount, ), action: ( :onChangedText, :onChangedSign, ), ) = useForexPaymentAmountFormField(/** */); // ... } } flutter_hooksを活⽤

Slide 74

Slide 74 text

© LayerX Inc. 74 複雑なフォーム実装の課題 複雑なフォーム実装の課題 Single Source Of Truthの担保 状態の⼀貫性が担保できる 宣⾔的な状態管理の徹底 状態が決まれば挙動が決まる 関⼼範囲の局所化 状態の管理範囲が最⼩化される 管理すべき状態が多い → 状態をどのように持つか 状態更新による多彩な動的制御が必要 → 状態をどうコントロールするか ⼤量の機能と複雑な依存 → 状態をどう切り分けるか 各関⼼領域ごとにSSOTが担保されており、 UIはそれを参照‧更新するだけで全体の⼀貫性を保てる

Slide 75

Slide 75 text

⽴ち向かう⼒を得られた

Slide 76

Slide 76 text

© LayerX Inc. 76 宣⾔的な 状態管理の徹底 SSOTの担保 関⼼範囲の局所化

Slide 77

Slide 77 text

© LayerX Inc. 77 Fin…

Slide 78

Slide 78 text

ほんとうに? (2回⽬)

Slide 79

Slide 79 text

© LayerX Inc. 79 現時点での全体像 大きな課題は解決できているが、 紹介できたのはごく一部で、 まだまだ複雑󰷺

Slide 80

Slide 80 text

© LayerX Inc. 80 代 理 操 作 ※代理操作 ユーザーAがユーザーBの代理としてフォームを操 作する機能。権限管理や固有ハンドリングなどで 瀕死に追い込まれる可能性がある。

Slide 81

Slide 81 text

© LayerX Inc. 81

Slide 82

Slide 82 text

おしまい