Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutter_Kaigi_2025.pdf

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for 須永高弘 須永高弘
September 17, 2026
2

 Flutter_Kaigi_2025.pdf

Avatar for 須永高弘

須永高弘

September 17, 2026

Transcript

  1. 自己紹介 須永 ⾼弘 Takahiro Sunaga 執⾏役員 CTO ⼤学卒業後、総合コンサルファームに⼊社し、 主に製造やメディアの顧客に対して 戦略、業務、ITに掛かる多様なプロジェクトを経験

    • 若⼿転職⽀援アプリ「ASSIGN」の開発・運⽤、 LLM の事業活⽤、エンジニア組織開発等に従事 • ©ASSIGN Inc. All Right Reserved. 1
  2. 信頼性:ユーザー情報を預けられる安心感 目指すユーザー体験 自身の情報を安心して入力・管理できる。 アプリから安定して情報や機会を獲得することができる。 追求する指標 (SLO・KPI) 1. セッションあたりのFatalエラー率 ≦ 0.1%

    2. フォーム途中離脱の低減 定義:セッション中に発生するFatalエラーの割合 定義:フォーム入力中離脱ユーザー / 全ユーザー 目的:アプリの突然の終了を防ぎ、ユーザーの思考と作業 を中断させない 目的:自己分析や経歴入力を完了させ、その後の情報や提 案を受けられるようにする ©ASSIGN Inc. All Right Reserved. 1
  3. なぜ Fatalエラー率 / session ≦ 0.1% を目指すのか? 外部ベンチマーク ストア品質基準では、一定のアプリクラッシュ率を超えると発見性や評価に悪影響 Google

    Play Consoleでは、ユーザーが知覚するクラッシュ率が1.09%が不正な動作の閾値 先行事例でも 0.1%〜0.05% を目標に据えるケースが一般的 モバイルアプリのクラッシュフリーセッション率の 目標値は99.95%が基本的な基準であり、トップク ラスのアプリは99.99%を達成している。一方、 99.77%を下回るとユーザー離れのリスクが高ま る。 出典: alphabin, "Mobile App Crash Rate Benchmarks 2025: What's a Good Crash Rate?" ©ASSIGN Inc. All Right Reserved. 1
  4. レイヤーごとの適切なエラーハンドリング 基本的な考え方 区分 定義 想定内の例外 アプリ設計上、起こり得る制御可 状態で表現(AsyncValue / Result<T, Failure>)し、

    能な例外(例:通信エラー、入力 UIで安全に処理 ミス) 想定外の例外 想定していない異常(例:Null参 try-catchで捕捉し、CrashlyticsへNon-fatalとして送 照、データ不整合、型不一致) 信(category付与) Unhandled Exception どのレイヤーでも捕捉されずアプ runZonedGuarded / PlatformDispatcher.onErrorで リ全体に伝搬 捕捉し、CrashlyticsへFatalとして送信 ©ASSIGN Inc. All Right Reserved. 対応方針 1
  5. レイヤーごとの適切なエラーハンドリング レイヤー 主な責務 行うこと AsyncValue.whenで状態を網羅し、 ErrorWidget.builderでUI破綻を防ぐ 想定内はResultで返し、想定外はtry-catchで捕捉して UseCase層 業務ロジック category=logic_unexpectedとして送信

    で通信やパースを保護し、 Repository層 通信・DBアクセス try-catch 想定外はcategory=infra_unexpectedとして送信 をtry-catchで捕捉し、 Platform層 ネイティブ連携 PlatformException 必要に応じてcategory=native_bridgeとして送信 UI層 表示・状態管理 ©ASSIGN Inc. All Right Reserved. 1
  6. レイヤーごとの適切なエラーハンドリング Non-fatalで扱うCategoryの設定 カスタムキー 意味 category=ui_break UI破綻(ErrorWidget, AsyncValue のerrorなど) category=logic_unexpected ビジネスロジックの想定外

    category=infra_unexpected 通信・DB・ファイルなどのインフラ例外 category=native_bridge Platform Channel/ネイティブ連携の例外 想定される修正内容の例 • レイアウト制約不足 • 非同期状態での不正Build • Null/空リスト想定漏れ • ドメインルール未考慮 • UseCase例外漏れ • 非同期競合による状態不整合 • APIスキーマ変更/デシリアライズ失敗 • ネットワーク切断・タイムアウト • キャッシュ不整合 • 戻り値型不一致 • PlatformException未捕捉 • スレッド制約違反 監視体制 Google Cloud Monitoring Alert fatal_rate >= 0.1% または ui_break >= 0.3% で通知 ©ASSIGN Inc. All Right Reserved. 1
  7. Drift 課題 による入力の即時ローカル保存 長文フォーム入力中の 通信エラー/OS kill/画面遷移 で入力が失われうる。 消える不安が入力継続を阻害し、途中離脱率を押し上げる。 解決策 入力は

    即時に Drift へ短期一時保存 Drift:Flutter向けのリアクティブなSQLiteラッパー(ローカルストレージ) 暗号化対応(sqlcipher統合)、型安全、ストリーム対応 一定期間のTTLまたはログアウトなどのアクションで未送信ドラフトを自動クリーンアップ。 ©ASSIGN Inc. All Right Reserved. 1
  8. Drift による入力の即時ローカル保存 1. 入力フォームからDriftへの保存 ポイント デバウンス保存: 入力500ms後に自動 保存(過度な書き込みを防止) フィールド単位: formId

    + fieldId で細 かく管理 復元機能: initState時に既存ドラフトを 読み込み TTL設定: 7日間の有効期限を付与 ©ASSIGN Inc. All Right Reserved. void _onTextChanged() { _debounce .cancel(); _debounce = Timer(Duration(milliseconds: 500), () async { await ref.read(draftRepositoryProvider).saveDraft( formId: 'self_analysis', fieldId: 'motivation', value: _controller.text, ttl: DateTime.now().add(Duration(days: 7)), ); }); } Future<void> _loadDraft() async { final draft = await ref.read(draftRepositoryProvider) .loadDraft(formId: 'self_analysis', fieldId: 'motivation'); if (draft != null) _controller.text = draft.value; } 1
  9. Drift による入力の即時ローカル保存 2. API登録・更新成功時に削除 ポイント 成功時即削除: API成功後、該当フォー ムのドラフトを全削除 エラー時保持: 送信失敗時はドラフトを

    残し、再試行を可能に 分析イベント: 送信成功・失敗を記録 try { // API await ref.read(apiServiceProvider).upsertSelfAnalysis(formData); へ送信 送信成功 ドラフトを削除 // → await ref.read(draftRepositoryProvider) .deleteDraftsByFormId('self_analysis'); analytics.logEvent('form_submit_success'); context.go('/self-analysis/complete'); } catch (e) { // showErrorDialog(' } エラー時はドラフトを保持(再試行可能に) 送信に失敗しました。時間を置いて再度お試しください。'); ©ASSIGN Inc. All Right Reserved. 1
  10. Drift による入力の即時ローカル保存 3. TTLによる自動クリーンアップ ポイント TTL管理: 7日経過したドラフトを自動 Future<void> cleanupExpiredDrafts() async

    { final now = DateTime.now(); 削除 起動時クリーンアップ: アプリ起動時に 期限切れをチェック ログアウト時: 全ドラフトを削除 切れのドラフトを削除 // TTL final deletedCount = await (_database.delete(_database.formDrafts) ..where((tbl) => tbl.ttl.isSmallerThanValue(now)) ).go(); if (deletedCount > 0) { analytics.logEvent('draft_cleanup', parameters: {'deleted_count': deletedCount}); } } ログアウト時は全削除 // Future<void> cleanupAllDrafts() async { await _database.delete(_database.formDrafts).go(); analytics.logEvent('draft_cleanup', parameters: {'trigger': 'logout'}); } ©ASSIGN Inc. All Right Reserved. 1
  11. 応答性:機会の鮮度を逃さない、即時的な反応 目指すユーザー体験 スムーズなUI反応や情報提示ができ、興味や動機をそのままにアプリを操作することができる。 追求する指標 (SLO・KPI) 1. INP(体感レイテンシ) ≤ 200ms 2.

    TTIの短縮(目標 3,000ms以下) 定義:ユーザーが操作してUIが「反応した」と感じるまで の時間 定義:画面遷移 / プッシュ通知タップから目的画面が使え る状態になるまでの時間 目的:認識遅延を防ぎ、ユーザーの行動意欲が落ちる前に 次の情報を届ける 目的:通知タップ時の高いモチベーションを失わせず、機 会を確実に活かしてもらう ©ASSIGN Inc. All Right Reserved. 1
  12. なぜ体感レイテンシ ≤ 200ms を目指すのか? 外部ベンチマーク Googleの推奨基準(INP: Interaction to Next Paint)では、ユーザーインタラクションから次の描画までの

    応答時間は200ms以下が良好と提示 良好なレスポンシブネスを提供するためには、ペー ジ読み込みの75パーセンタイルで測定されるINPが 200ミリ秒以下であることが推奨される。 200ms〜500msは改善が必要、500ms以上は不 良なレスポンシブネスを示す。 ©ASSIGN Inc. All Right Reserved. 1
  13. なぜ TTI ≤ 3,000ms を目指すのか? 外部ベンチマーク Googleの推奨基準(LCP: Largest Contentful Paint)では、ページの主要コンテンツが表示されるまでの時

    間は2.5秒以下が良好 2.5秒〜4.0秒は改善が必要、4.0秒以上は不良とされる モバイルアプリにおいても、この基準を満たす速度が期待される 良好なユーザー体験を提供するためには、サイトの LCP(最大コンテンツの描画)が2.5秒以下である ことを目指すべきである。この目標値は、ページ読 み込みの75パーセンタイルで測定される。 ©ASSIGN Inc. All Right Reserved. 1
  14. 応答性を実現する設計パターン 追求するSLO・KPI 設計パターン 体感レイテンシ ≤ 200ms ① 画面遷移ファーストな設計 ② Optimistic

    UI ③ 差分ロード・キャッシュ活用 TTIの短縮(目標 3,000ms以下) ©ASSIGN Inc. All Right Reserved. 1
  15. 体感レイテンシ ≤ 200ms を実現する設計 課題 ユーザーが操作してから画面反応があるまでの遅延が長いと、 興味・集中が途切れ、次の行動に繋がりにくくなる。 特に「画面遷移」や「送信アクション」は遅延の体感が強い。 解決策 パターン①:画面遷移ファーストな設計

    通信完了を待たずに「意味のある最小UI」を即描画(メタ情報・スケルトン先出し) パターン②:Optimistic UI サーバー応答を待たずにUIを即時更新 失敗時のみロールバックし、体感200ms以内の反応を保証 ©ASSIGN Inc. All Right Reserved. 1
  16. Optimistic UI による体感速度向上 ポイント StateNotifierで状態管理: class MessageNotifier extends StateNotifier<List<Message>> {

    MessageNotifier(this.api) : super([]); final MessageApi api; 明示的な状態更新で必要な箇所だけ再 描画 即時UI反映: isSending: true で吹き出しを即座 に追加 成功時確定: API成功でインジケーター非表示 失敗時ロールバック: エラー時は吹き出しを削除 Future<void> sendMessage(String text) async { final tempMessage = Message( id: 'temp_${DateTime.now().millisecondsSinceEpoch}', text: text, isSending: true, // ); messagesState = [...messagesState, tempMessage]; // 1. UI try { final confirmed = await api.sendMessage(text); // 2. → state = state.map((m) => m.id == tempMessage.id ? confirmed.copyWith(isSending: false) : m ).toList(); } catch (e) { // 3. → state = state.where((m) => m.id != tempMessage.id).toList(); showErrorSnackBar(' '); } } サークルインジケーター表示 即時 に追加 成功 インジケーター非表示 失敗 ロールバック 送信に失敗しました ©ASSIGN Inc. All Right Reserved. } 1
  17. Optimistic UI による体感速度向上 ポイント StateNotifierで状態管理: 明示的な状態更新で必要な箇所だけ再 描画 即時UI反映: isSending: true

    で吹き出しを即座 に追加 成功時確定: API成功でインジケーター非表示 失敗時ロールバック: エラー時は吹き出しを削除 ©ASSIGN Inc. All Right Reserved. 1
  18. 補足:Optimistic UIの使い所 基本的な考え方 状態変更が軽量で、失敗してもデメリットが少ないものに限定する。 操作の取り消しが困難または不可能で、失敗時の影響がクリティカルなものや、 トランザクションの整合性が求められる操作には適用しない。 私たちのアプリで適用するケース メッセージの送信 求人のお気に入り/ 興味なし登録

    サクサクと情報整理を進め、思考の流れを止めない ©ASSIGN Inc. All Right Reserved. 適用しないケースと代替案 スカウト・求人へのエントリー 代替案: 処理中、エントリー完了の状態を管理し、UIに も明確にフィードバックする。 ユーザーに安心感を与え、二重応募などの混乱 を防ぐ。 1
  19. 応答性向上に関する計測 検証したい仮説 応答性向上(INP、TTI短縮)によって、ユーザーのタスク完了率や継続操作率が改善するか? 検証のためのイベント設計 No. 計測イベント名 タイミング パラメータ(例) 目的、計測するもの ①

    action_start 操作開始 action_type: 'open_scout' screen_id: 'chat_room_list' ユーザーが操作を開始 ② feedback_shown UI反応 action_type: 'open_scout' ③ content_interactive コンテンツ 操作可能 screen_id: 'chat_room' ④ task_complete タスク完了 task_name: 'entry_scout' from_action: 'open_scout' ©ASSIGN Inc. All Right Reserved. [INP] 操作からUIが反応するまでの 体感レイテンシ [TTI] 遷移開始から操作可能になるまでの時間 [効果] 最終的なタスク完了に至ったか 1
  20. 応答性向上に関する計測:TTI ポイント AsyncValue.whenの data 節で発火 データ取得完了後を検知 addPostFrameCallbackで描画完了を待つ content_interactiveイベント発火 データ取得を伴う画面 //

    class JobDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final jobState = ref.watch(jobDetailProvider); return jobState.when( loading: () => Skeleton(), error: (e, _) => ErrorView(), data: (job) { // WidgetsBinding.instance.addPostFrameCallback((_) { analytics.logEvent('content_interactive', parameters: 'screen_id': 'job_detail', }); }); return JobDetailView(job: job); }, ); データ取得完了後(操作可能時) } } ©ASSIGN Inc. All Right Reserved. 1
  21. 継続可能性:キャリアのあらゆるフェーズで支援する 目指すユーザー体験 転職活動・就業中・キャリア形成など、ユーザーのライフステージが変化してもアプリが文脈を維持し、再び 役立つ存在であり続ける その瞬間の課題解決ツールではなく、キャリアのどのタイミングでもサポートできる長期的な伴走者となる 追求する指標 (SLO) 1. ディープリンク遷移成功率 ≥

    99.0% 2. 文脈完全性率 ≥ 99.5% 定義:通知や外部リンクからアプリを開いた際に、期待さ れたルートへ正常に遷移できた割合 定義:遷移後の画面が提案内容(求人ID・イベントID・分 析結果など)と一致して正しく描画される割合 目的:「アプリに戻ってきたとき、必ず目的地にたどり着 ける」という復帰体験の信頼性を維持すること 目的:提案された内容が正しく再現されるというサポート 体験の一貫性を担保すること ©ASSIGN Inc. All Right Reserved. 1
  22. 単一路線ハンドラ + ガード 課題 初期リンク、通知復帰、外部URL…入口が複数 → 処理が分散すると整合性が崩れる リンクパラメータ不正・未ログイン状態での再訪 → 目的画面にたどりつけない

    解決策 入口は DeepLinkHandler に一本化 認証や前提条件の検証は go_router.redirect に集約 初期リンク 通知 DeepLinkHandler 外部URL ©ASSIGN Inc. All Right Reserved. URI parse 内部ルートへ変換 router.go go_router redirect OK 目的地へ遷移 NG ログイン/一覧へフォールバ ック (認証/前提条件) 1
  23. 単一路線ハンドラ + ガード ポイント 初期リンク / 通知 / 外部URL →

    DeepLinkHandler で受付 Handler はURIの解析、遷移先を解決、 軽いバリデーションに専念 認証チェック / リソース存在チェックは go_router.redirect に一本化 フォールバック遷移も redirect が担当するためロ ジックが分散しない ©ASSIGN Inc. All Right Reserved. (責務:外部 をアプリ内部ルートに変換) /// DeepLinkHandler URI class DeepLinkHandler { Future<void> handle(Uri uri) async { final route = _parseUri(uri); // DeepLink → アプリ内ルートに変換 if (route == null) { // router.go('/login'); // return; } analytics.logEvent('deep_link_route', { 'success': true, 'uri': uri.toString(), }); router.go(route.path, extra: route.params); // ディープリンク失敗のイベント送信を行う ログイン画面へフォールバック } 目的地へ遷移 } (責務:認証・前提条件ガードの一元化) /// go_router final router = GoRouter( routes: [...], redirect: (context, state) { final isAuth = ref.read(authProvider); if (!isAuth && state.matchedLocation != '/login') { return '/login?redirect=${state.matchedLocation}'; } return null; }, ); 1
  24. 単一路線ハンドラ + ガード ポイント 初期リンク / 通知 / 外部URL →

    DeepLinkHandler で受付 Handler はURIの解析、遷移先を解決、 軽いバリデーションに専念 認証チェック / リソース存在チェックは go_router.redirect に一本化 フォールバック遷移も redirect が担当するためロ ジックが分散しない ©ASSIGN Inc. All Right Reserved. 1
  25. 単一路線ハンドラ + ガード ポイント 初期リンク / 通知 / 外部URL →

    DeepLinkHandler で受付 Handler はURIの解析、遷移先を解決、 軽いバリデーションに専念 認証チェック / リソース存在チェックは go_router.redirect に一本化 フォールバック遷移も redirect が担当するためロ ジックが分散しない ©ASSIGN Inc. All Right Reserved. 1
  26. 目的画面における文脈検証 画面アクセス時に文脈検証し、不正/欠落は上位画面にフォールバック 通知 チャットルーム詳細 データ存在検証 // Push → class ChatRoomScreen

    extends ConsumerStatefulWidget { final String roomId; final String? messageId; // } final room = await ref.read( // 2. chatRoomProvider(widget.roomId).future ).catchError((_) => null); スクロール先メッセージ if (room == null) { isValid = false; reason = 'room_not_found'; } class _ChatRoomScreenState extends ConsumerState { @override void initState() { super.initState(); _validateContext(); } analytics.logEvent('context_fidelity', parameters: { // 3. 'ok': isValid, 'route': 'chat_room', 'reason': reason, }); イベント送信 Future<void> _validateContext() async { bool isValid = true; String? reason; if (widget.roomId.isEmpty) { // 1. isValid = false; reason = 'room_id_missing'; } 必須パラメータ検証 ©ASSIGN Inc. All Right Reserved. フォールバック チャット一覧へ if (!isValid && mounted) { // 4. router.go('/chat/rooms'); // } } } 1
  27. 継続可能性の計測 継続可能性のSLOを検証するため、2つのイベントで計測 No. 計測イベント名 タイミング パラメータ(例) 目的、計測するもの ① ディープリンク処理完了 時

    uri: 'https://...' success: true reason: 'not_found' [遷移成功率] 画面入場時の 文脈検証完了時 ok: true route: 'chat_room' reason: 'room_id_missing' [文脈完全性] ② deep_link_route context_fidelity ディープリンクが正常に処理されたか 遷移先画面で文脈が正しく維持されてい るか SLO目標値 ディープリンク遷移成功率: success: true の件数 / 全リンク処理回数 ≥ 99.0% 文脈完全性率: ok: true の件数 / 全画面遷移回数 ≥ 99.5% ©ASSIGN Inc. All Right Reserved. 1
  28. まとめ:顧客価値を実現する柱と設計パターン 3つの柱 信頼性 SLO・KPI Fatalエラー率 ≤ 0.1% 応答性 体感レイテンシ ≤

    200ms TTIの短縮(目標 3,000ms以下) 柱 フォーム離脱率の低減 ≥ 99.0% 継続可能性 ディープリンク遷移成功率 文脈完全性率 ≥ 99.5% ©ASSIGN Inc. All Right Reserved. 設計パターン ① レイヤーごとの適切なエラーハンドリング ② Driftによる入力の即時ローカル保存 ① 画面遷移ファーストな設計 ② Optimistic UI ③ 差分ロード・キャッシュ活用 ① 単一路線ハンドラ + ガード ② 目的画面における文脈検証 1
  29. SLOの先にある、複合的な価値実現を目指す 組み合わせ 上位KPI 価値 信頼性 × 応答性 登録動線の完了率向上 クラッシュや入力喪失がなく、即時反応するUIが最後まで やり切る体験を支える

    軽快に動作し、文脈を保ったまま再開できる体験が 応答性 × 継続可能性 再訪率・復帰ユーザーの 完了率向上 戻ってきたくなる動機を作る 信頼性 × 継続可能性 長期的な利用継続 ・LTV向上 「データが安全に残る」「再開が正確」の積み重ねで ブランド信頼を作る 継続して行うこと 重要KPIに直結するSLOの選定・強化 定義済みSLOのより高い基準設定(例:99.9→99.95) ©ASSIGN Inc. All Right Reserved. 1
  30. 顧客価値を実現するアプリケーション開発 顧客価値とSLOの往復によって、技術と体験の深化の循環を生み出す 1. 顧客価値の理解 アプリケーションが果たすべき責務を明 確にする 4. 測定と検証 データで効果を検証し、次の改善へつな げる

    ©ASSIGN Inc. All Right Reserved. 2. 仮説と定量目標の設定 ↻ SLO・KPIを定義し、技術選定の基準を 明確にする 3. 体験の実装 適切な実装をもって、洗練された 体験としてユーザーに価値を届ける 1