Slide 1

Slide 1 text

Riverpod3.xで実現する実践的UI実装 Fumiya Sakai potatotips #94 @ 株式会社YouTrust 2026/01/23

Slide 2

Slide 2 text

自己紹介 ・Fumiya Sakai ・Mobile Application Engineer アカウント: ・Twitter: https://twitter.com/fumiyasac ・Facebook: https://www.facebook.com/fumiya.sakai.37 ・Github: https://github.com/fumiyasac ・Qiita: https://qiita.com/fumiyasac@github 発表者: ・Born on September 21, 1984 これまでの歩み: Web Designer 2008 ~ 2010 Web Engineer 2012 ~ 2016 App Engineer 2017 ~ Now iOS / Android / sometimes Flutter

Slide 3

Slide 3 text

技術書同人誌博覧会スタッフ&デザイン協力もしてます (告知)第13回技術書同人誌博覧会は5/10大田区産業プラザPiOにて開催予定! 🍀 #10 一般参加募集チラシ 🍀 #11 次回案内チラシ 🍀 #12 サークル参加募集 🍀 #13 次回案内チラシ Conpassから一般参加者を募集していますのでぜひ参加登録をお願いします🙇 https://gishohaku.connpass.com/event/372013/

Slide 4

Slide 4 text

技術書典18では新刊をオンライン頒布しました インプットを整理して次へ繋げる→アウトプットに応用できる様に

Slide 5

Slide 5 text

iOSDC Japan 2025では原稿を1つ寄稿しています 原稿名: 見た目に惑わされないUI設計と実装の勘どころ 〜SwiftUI・UIKitで読み解く複雑度の見極め方〜 iOS/Androidで類似したUIを実装する場合に気になるポイントを整理しました

Slide 6

Slide 6 text

Riverpodでの状態管理と組み合わせたUI実装サンプル UIパッケージを活用&連携することで実現する検索可能なマルチセレクト形式 Android iOS GitHub: https://github.com/fumiyasac/searchable_combobox

Slide 7

Slide 7 text

UI実装における便利なパッケージを上手に組み合わせる 同様の機構をゼロから作成すると考えると意外と作り込みが必要なUI実装 ✅ 複数のマスタデータをタブで切り替えたい ※課題提起※ この様なUI実装要件はありませんか? ✅ 検索しながら複数選択できるドロップダウンが欲しい ✅ よく使う項目は上に表示してほしい(履歴機能) ✅ 選択内容はアプリを閉じても保持したい 実現が難しそうに感じる点を洗い出す ① 検索ボックス内部の会計項目は上部タブに表示されているカテゴリー項目と紐づく ※複雑な条件を選択する際のWebフォームの様なイメージに近い形 ② 検索ボックス内部で入力キーワードに合致する会計項目を絞り込み検索をできる ③ 会計項目複数選択可能かつ直近で選択した項目を上に表示する

Slide 8

Slide 8 text

紹介するUI実装サンプルの要件を元に作成した例 カテゴリー要素選択→会計項目検索または選択→Chipでの検索項目表示の流れ ① タブ切り替え ② 検索可能マルチセレクト ③ 選択Chip表示 📺 画面構成 🖐 主要機能 🚥 UIコンポーネント ① タブ選択 → マスタ種別を切り替え ② 検索入力 → 候補をフィルタ ③ 複数選択 → 項目をチェック ④ クリア → 選択をリセット ① buttons_tabbar(タブ) ② dropdown_search(検索+マルチ選択)

Slide 9

Slide 9 text

パッケージ利用部分以外で自前で頑張って構築する部分 UI実装全体をパッケージに寄せる方針ではなくいい感じに組み合わせるイメージ 選択済み項目Chipは自前で作成可能 📦 buttons_tabbar: https://pub.dev/packages/buttons_tabbar 📦 dropdown_search: https://pub.dev/packages/dropdown_search 自力でiOS/Android差異を考慮し た上での実装が難しい部分に対し てパッケージで補う様な形

Slide 10

Slide 10 text

Riverpodでの状態管理と全体的なArchitectureに関して View & ViewModel(Provider) & Repository & DataSourceをベースにした構成 View ViewModel Repository DataSource ① UI描画と入出力のみ担当 ② 状態はProviderから取得 ① 状態管理とUseCase ② Notifier / Provider ① Data取得の抽象化 ② Testtable / 容易な差し替え ① Data永続化処理 ② 将来のAPI化 / Cache拡張 全体的にRiverpodを活用した オーソドックスな設計にする 必要に応じてコード自動生成 (Riverpod Generator等)の 便利な機能を活用していく

Slide 11

Slide 11 text

UI実装で活用しているパッケージに関する特徴まとめ 自前で実装できそうな部品とそうでない部品をうまく組み合わせる事で実現する 1. buttons_tabbar: 2. dropdown_search: 各Tab要素がトグルボタンとして表示されるTabBarを表示する 各TabIndicatorがトグルボタンになっている パッケージの取捨選択をする際は更新頻度等も考慮する 外観と動作に関するカスタマイズが可能 様々なスタイリングオプションに対応している 検索機能付きのドロップダウンウィジェットを提供する 検索可能なドロップダウン要素を提供する Menu・BottomSheet・ModalBottomSheet・Dialogの4種の表示に対応 単一&複数選択の両方にも対応する StatelessWidgetで実装可能 💡 自前で実装可能な表現も組み合わせてみると意外と難しい事もある 💡 iOS/Androidの両方のUIにしっかりと対応している事を確認する 💡

Slide 12

Slide 12 text

Riverpodを利用した状態管理に関する処理の抜粋(1) stateによる状態管理をする処理を組み立ててProviderを公開する様な流れ class SelectedMasterTypeNotifier extends Notifier { // build()メソッド: 初期状態を返す // // このメソッドは、Providerが初めて使用されるときに1度だけ呼ばれます。 // ここで返した値が、stateの初期値になります。 @override MasterType build() { // 初期状態は「全て」タブを選択 return MasterTypes.all; } // タブを切り替えるメソッド // // 状態を変更するには、state = 新しい値 と代入します。 // stateが変更されると、このProviderをwatchしている // 全てのWidgetが自動的に再ビルドされます。 // // @param type 新しく選択されたマスタタイプ void update(MasterType type) { state = type; // この代入で状態が更新され、UIが再描画される } } // NotifierProvider: Notifierを提供するProvider // // NotifierProvider の形式で定義します。 // - 第1型引数: Notifierクラスの型 // - 第2型引数: 管理する状態の型 // // .new は、SelectedMasterTypeNotifier.new と同じ意味で、 // コンストラクタへの参照を渡しています(Dart 2.15以降の記法) final selectedMasterTypeProvider = NotifierProvider( SelectedMasterTypeNotifier.new, ); // UIからの更新処理 ref.read(selectedMasterTypeProvider.notifier).update(MasterTypes.values[index]); StateProvider → Notifier Provider公開

Slide 13

Slide 13 text

class SelectedItemsNotifier extends Notifier> { // build()メソッド: 初期状態を返す @override List build() { // 非同期でデータを読み込む(awaitしない) _loadSelectedItems(); // 初期状態として空リストを返す return []; } // リポジトリへのアクセス // // getterを使って、必要な時にリポジトリを取得します。 // ref.read(): Providerの現在の値を1回だけ読み取る // // ref.watch()との違い: // - watch: 値の変更を監視し、変更時に再実行される // - read: 値を1回読むだけで、変更を監視しない // // メソッド内で使う場合は、通常ref.read()を使用します。 MasterRepository get repository => ref.read(masterRepositoryProvider); …(省略)… Riverpodを利用した状態管理に関する処理の抜粋(2) stateによる状態管理をする処理を組み立ててProviderを公開する様な流れ StateProvider → Notifier Provider公開 // NotifierProviderの定義 // // このProviderを通じて、アプリケーション全体で選択状態を共有します。 // // 使用例: // - 状態を読む: ref.watch(selectedItemsProvider) // - 状態を変更: ref.read(selectedItemsProvider.notifier).addItem(item) final selectedItemsProvider = NotifierProvider>( SelectedItemsNotifier.new, );

Slide 14

Slide 14 text

補足:会計科目用のアイテム表示処理に関する処理例 選択履歴をMergeする処理とSortする処理を組み合わせる事で実現する final masterItemsProvider = FutureProvider.family, String>((ref, masterType) async { final repository = ref.watch(masterRepositoryProvider); final items = await repository.getMasterItems(masterType); final history = await repository.getSelectionHistory(); // 選択履歴をマージする処理 // 処理の流れ: // 1. 各itemに対して、履歴の中から同じID・タイプのitemを探す // 2. 見つかった場合、その履歴のlastSelected情報を使用 // 3. 見つからない場合、元のitemをそのまま使用 // ソート処理: 最近選択したアイテムを先頭に並べる // sort()メソッド: リストを並び替えます // 比較関数は、2つの要素a, bを比較し、以下を返します: // - 負の数: aがbより前に来る // - 0: 順序は変わらない // - 正の数: bがaより前に来る // 処理済みのリストを返す return itemsWithHistory; }); final itemsWithHistory = items.map((item) { final historyItem = history.firstWhere( (h) => h.id == item.id && h.masterType == item.masterType, orElse: () => item, ); return historyItem.lastSelected != null ? historyItem : item; }).toList(); itemsWithHistory.sort((a, b) { if (a.lastSelected != null && b.lastSelected == null) return -1; if (a.lastSelected == null && b.lastSelected != null) return 1; if (a.lastSelected != null && b.lastSelected != null) { return b.lastSelected!.compareTo(a.lastSelected!); } return a.name.compareTo(b.name); }); FutureProvoder.familyを利用した処理 Merge処理 Sort処理

Slide 15

Slide 15 text

UI実装で活用しているパッケージの実装部分について Riverpodとの連携処理を組み合わせつつもパッケージの実装方針に乗っかる形 1. buttons_tabbar: 2. dropdown_search: タブ切替と状態同期 タップ時は Notifier.update(...) を呼び、UIは ref.watch で同期 DefaultTabController + TabBarView でタブと内容の一貫性を担保 選択/非選択の配色・太字で状態を視覚化(アクセシビリティ配慮) 検索付きドロップダウンの実装と勘所(Riverpod連携) itemAsString・filterFnで表示名と絞り込みを統一 showSearchBox・showClearButtonで検索性と操作性を両立 選択状態はNotifierへ集約し、UIはref.watchで購読 アクセシビリティのためlabelText/hintTextを明確に設定 dropdown_search + buttons_tabbarで検索と切替のUXを両立 ✨ 👉 POINTはここだ! 👉 POINTはここだ! テストでは Provider.override を用いて初期タブを切替えて検証 このUI実装に関するポイント

Slide 16

Slide 16 text

Provider設計の指針と画面要素の構築ポイント 設計のベストプラクティス&Focus制御・Error制御・最適化等のTipsを整理する 1. 設計のベストプラクティス: 2. Focus制御・Error制御・最適化等: ⚙ UI操作で更新される状態はNotifier 選択値・フォーム入力などの変更意図をメソッドで明示 ⚙ 大きな状態は分割+selectで最小再描画 関心のある部分だけ購読してパフォーマンスを確保 ⚙ 依存は単方向(UI→状態→データ) 循環依存を避け、テスト容易性と保守性を高める ⚙ 非同期データは専用Provider併用 FutureProvider/StreamProviderと組み合わせて責務分離 ⚙ 内部リソースはProviderに閉じ込める Timer/Controller等はProviderで生成・dispose管理 ♻ フォーカス/キーボード制御 FocusNode・FocusTraversalGroupで自然な移動、IME確定を適切に処理 ♻ 再描画の最適化 Provider.select、const化、ValueKeyで差分更新とパフォーマンス向上 ♻ アクセシビリティ/テスト Semanticsでラベル付け、ゴールデン/Widgetテストで回 regress を防止 ♻ ローディング/エラーの一貫性 AsyncValue.whenと共通ウィジェットで統一表現(スケルトン/リトライ) ♻ レイアウト安定性 最小サイズ・SafeArea・MediaQueryで崩れ防止、リストはbuilderで

Slide 17

Slide 17 text

Thank you for listening !