$30 off During Our Annual Pro Sale. View Details »

[FlutterKaigi2024] Effective Form 〜Flutterによる複雑...

たまねぎ
November 26, 2024

[FlutterKaigi2024] Effective Form 〜Flutterによる複雑なフォーム開発の実践〜

たまねぎ

November 26, 2024
Tweet

More Decks by たまねぎ

Other Decks in Programming

Transcript

  1. © LayerX Inc. 2 所属 株式会社LayerX バクラク事業部 申請‧経費精算開発 略歴 iOS

    10年(ヤフー株式会社 → STORES株式会社) Flutter 1.5年(現職) 趣味 ⽿で楽しむもの全般(⾳楽‧ラジオ) 「Podcast エンジニア 関⻄⼈」で検索! たまねぎ(@_chocoyama) ⾃⼰紹介、会社‧プロダクト紹介
  2. 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⽉末時点)
  3. © LayerX Inc.  4 「バクラク」シリーズラインナップ ‧AIが請求書を5秒でデータ化 ‧仕訳 / 振込データを⾃動作成 ‧電帳法‧インボイス制度にも対応

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

    TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Email', ), ); TextField( obscureText: true, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Password', ), );
  5. © 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… ログインフォームの例
  6. © 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を作成 特定のタイミングで参照
  7. © LayerX Inc. 12 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit! •

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

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

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

    ⼊⼒値(pure / dirty) バリデーションロジック import 'package:formz/formz.dart'; class EmailInput extends FormzInput<String, EmailInputError> { 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 } バリデーションエラーのパターン
  12. © 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表⽰は遅延可能
  13. © 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のまま
  14. © 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 内部的にはエラーの状態
  15. © LayerX Inc. 25 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. デフォルト値をセットするかのチェック

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

    3. 別のフィールドの値を元に、動的に対象フィールドを表⽰すべきかを判定 4. 別のフィールドの値を元に、動的に対象フィールドを必須にするかどうかを判定
  17. © LayerX Inc. 27 バクラクのこのフォームの裏で起きることはどれでしょう? リアルワールドフォーム 1. 未⼊⼒かどうかのチェック 2. アップロード上限数を超えていないかのチェック

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

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

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

    3. マイナス値や⼩数値を許可するかの判定 4. ⽇本円以外を許可するかのチェック 5. 別のフィールドでアップロードしたファイルからOCRで読み取った値を利⽤しているかの判定 6. 別のフィールドの⼊⼒有無で、変更をできないようにする制御 7. ここの⼊⼒値を元に、⾃動算出して表⽰するフォームの制御 8. ユーザーにより設定された閾値を超えた場合、特定のフィールドの値を決められたものにする制御
  21. © LayerX Inc. 32 バクラクのフォームの複雑性 UIの表⽰ ⼊⼒状態の管理 バリデーション Dirty管理 Submit!

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

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

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

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

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

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

    必要な処理の呼び出し忘れ データや表⽰不整合の懸念 ロジック差異による挙動差分
  28. © LayerX Inc. 45 1.SSOTの担保 状態を1元管理することで、 データや表⽰に⼀貫性を保つ • 状態は共通の値を参照‧更新 •

    それぞれで判定するのではなく、同 ⼀の判定結果を参照 https://riverpod.dev/ 判定結果 ⼊⼒値 Riverpod
  29. © 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
  30. © 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
  31. © LayerX Inc. 48 1.SSOTの担保 @freezed class RequestFormState with _$RequestFormState

    { const factory RequestFormState({ @Default([]) List<RequestFormDefaultFieldInputState> defaultFieldInputStates, @Default([]) List<RequestFormFieldInputState> formFieldInputStates, RequestFormRequestDetailsFieldInputState? requestDetailsInputState, // … }) = _RequestFormState; const RequestFormState._(); } class RequestFormAsyncNotifier extends AutoDisposeFamilyAsyncNotifier< RequestFormState, RequestFormStateProviderDependencies> { RequestFormAsyncNotifier(); @override FutureOr<RequestFormState> build( RequestFormStateProviderDependencies arg, ) async { // ... } } バクラクの場合 画⾯に応じて表⽰に差異がでない フォームの状態をGlobalStateとして⼀元管理
  32. © LayerX Inc. 49 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い → 状態をどのように持つか 状態更新による多彩な動的制御が必要

    → 状態をどうコントロールするか ⼤量の機能と複雑な依存 Single Source Of Truthの担保 状態の⼀貫性が担保できる
  33. © LayerX Inc. 51 2.宣⾔的な状態管理の徹底 宣⾔的な状態管理の徹底 状態決定後、命令的な呼び出しで 反映する対応が必要 BAD import

    UIKit class AlbumDetailViewController: UITableViewController { private var album: Album // ... func refresh() async { album = await fetchAlbum() tableView.reloadData() } }
  34. © 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
  35. © 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
  36. © LayerX Inc. 54 2.宣⾔的な状態管理の徹底 class ApprovalReminderDefaultFieldCompositeState extends RequestFormCompositeState<DateTime?, ApprovalReminderDefaultField>

    implements AllRequestFormCompatible { ApprovalReminderDefaultFieldCompositeState( ApprovalReminderDefaultFieldInputState inputState, List<Query$Holiday$holidays>? holidays, ) : super._( inputState.input, inputState.compositeField, inputState.input.validator .bind(requiredDefaultField(inputState.field.option)) .and(minDateTime(clock.now())) .and(recommendedWeekend(holidays)), ); } ⼊⼒状態が決まればバリデーション結果が決まっている (エラーのフィードバックはdirty管理でコントロール) バクラクの場合 明⽰的なバリデーションが必要ないため、 ⼀貫した結果になる 経路(下書き復帰など)に応じて 壊れるリスクが少ない
  37. © 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; } }
  38. © 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 各状態の対応が宣⾔的になる
  39. © LayerX Inc. 57 複雑なフォーム実装の課題 複雑なフォーム実装の課題 管理すべき状態が多い → 状態をどのように持つか ⼤量の機能と複雑な依存

    → 状態をどう切り分けるか Single Source Of Truthの担保 状態の⼀貫性が担保できる 状態更新による多彩な動的制御が必要 → 状態をどうコントロールするか 宣⾔的な状態管理の徹底 状態が決まれば挙動が決まる
  40. © LayerX Inc. 60 3.関⼼範囲の局所化 関⼼範囲の局所化 画⾯A A/B共通の状態 A固有の状態 B固有の状態

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

    画⾯B あらゆる状態を⼀箇所で管理 画⾯A A/B共通の状態 画⾯B A固有の状態 B固有の状態 AppState Ephemeral State 関⼼領域ごとに切り分けて管理
  42. © LayerX Inc. 62 • AppState ◦ Riverpodで実現 ◦ 特定の画⾯に依存せず、グローバルな状態として保持したいものはこっち

    • EphemeralState ◦ flutter_hooksで実現 ◦ 特定の画⾯に依存しており、ローカルな状態として保持するだけで⼗分なものはこっち AppStateとEphemeralState 3.関⼼範囲の局所化
  43. © LayerX Inc. 65 AppStateの肥⼤化対策 3.関⼼範囲の局所化 申請承認画⾯ フォーム設定 申請作成画⾯ 合成された

    フォーム状態 ⼊⼒値のまとまり ユーザーデータ バクラクの場合 final requestFormCompositeStateProvider = Provider.autoDispose.family< List<RequestFormCompositeState<dynamic, RequestFormBaseFieldComponent>>, RequestFormStateProviderDependencies>( (ref, dep) { final user = ref.watch(userProvider(dep)); final formSetting = ref.watch(requestFormSettingProvider(dep)); final inputStates = ref.watch(requestFormInputStateProvider(dep)); // ... }); AppStateの責任領域を最⼩化 意図しない影響の発⽣を最⼩化
  44. © LayerX Inc. 69 3.関⼼範囲の局所化 フォーム種別ごとの Component Page ⼊⼒種別ごとの Component

    ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component フォーム種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component ⼊⼒種別ごとの Component バクラクの場合
  45. © 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レベルで種別ごとに切り分け
  46. © 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, ), // ... }, 種別ごとに 必要な⼊⼒フィールドが異なる コンポーネントのまとまりで 切り分け
  47. © 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を活⽤
  48. © 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を活⽤
  49. © LayerX Inc. 74 複雑なフォーム実装の課題 複雑なフォーム実装の課題 Single Source Of Truthの担保

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

    作する機能。権限管理や固有ハンドリングなどで 瀕死に追い込まれる可能性がある。