Slide 1

Slide 1 text

""LJSB Master of Flutter lifecycle @_a_akira AAkira Flutter Kaigi 2023

Slide 2

Slide 2 text

% About me 2 M3, Inc. Akira Aratani https://aakira.app https://aakira.studio 🐕 ✈ 📷 🎾 🤸 🚁⊿ aakira.studio _a_akira AAkira chai_cavachon

Slide 3

Slide 3 text

% M3, Inc. 3 https://www.m3tech.blog/ https://jobs.m3.com/engineer/ m3_engineering m3techchannel160 https://jobs.m3.com/product/ エンジニアリングの力で医療の世界を変えていく The Power of Medical Innovation

Slide 4

Slide 4 text

% M3, Inc. 4 https://www.m3tech.blog/ https://jobs.m3.com/engineer/ m3_engineering m3techchannel160 https://jobs.m3.com/product/ エンジニアリングの力で医療の世界を変えていく The Power of Medical Innovation Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 5

Slide 5 text

% • Flutter Lifecycle • Widget Lifecycle • App Lifecycle • Navigation & Routing • Handling Flutter lifecycle • Summary 5 Agenda

Slide 6

Slide 6 text

% • Flutter Lifecycle • Widget Lifecycle • App Lifecycle • Navigation & Routing • Handling Flutter lifecycle • Summary 6 Agenda 目的 Flutterのライフサイクルの 仕組みを知って、 必要になった時に適切にイ ベントハンドリングできる ようになる。 OK CANCEL

Slide 7

Slide 7 text

% Lifecycleとは? 7

Slide 8

Slide 8 text

% 8 https://developer.android.com/guide/components/activities/activity-lifecycle https://developer.apple.com/documentation/uikit/uiviewcontroller

Slide 9

Slide 9 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル • アプリ全体のライフサイクル 9 Flutter lifecycle

Slide 10

Slide 10 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル • アプリ全体のライフサイクル • OSに依るもの • 画面遷移に依るもの 10 Flutter lifecycle

Slide 11

Slide 11 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル → 主にStateful Widget • アプリ全体のライフサイクル • OSに依るもの → WidgetBindingObserver • 画面遷移に依るもの → NavigatorObserver 11 Flutter lifecycle

Slide 12

Slide 12 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル → 主にStateful Widget • アプリ全体のライフサイクル • OSに依るもの → WidgetBindingObserver • 画面遷移に依るもの → NavigatorObserver 12 Flutter lifecycle Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 13

Slide 13 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル → 主にStateful Widget • アプリ全体のライフサイクル • OSに依るもの → WidgetBindingObserver • 画面遷移に依るもの → NavigatorObserver 13 Flutter lifecycle Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 14

Slide 14 text

% • ライフサイクルは主に2つある • Widget自体のライフサイクル → 主にStateful Widget • アプリ全体のライフサイクル • OSに依るもの → WidgetBindingObserver • 画面遷移に依るもの → NavigatorObserver X Flutter lifecycle Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 15

Slide 15 text

% • StatelessWidget • StatefulWidget • ProxyWidget(InheritedWidget, ParentDataWidget) • RenderObjectWidget 14 Widget

Slide 16

Slide 16 text

% • StatelessWidget • StatefulWidget • ProxyWidget(InheritedWidget, ParentDataWidget) • RenderObjectWidget 15 Widget

Slide 17

Slide 17 text

% • StatelessWidget • StatefulWidget • ProxyWidget(InheritedWidget, ParentDataWidget) • RenderObjectWidget 16 Widget

Slide 18

Slide 18 text

% • 状態を持つことを目的としたWidget • 状態があるView componentが多く利用している • Image, Button, Radio, DropDownMenu, Slider, Chip... 17 Stateful Widget

Slide 19

Slide 19 text

% • _StateLifecycle • created • initialized • ready • defunct 18 State lifecycle

Slide 20

Slide 20 text

% 19 State lifecycle 初期値、Stateオブジェクトが生成された後 Stateオブジェクトはまだない Stateオブジェクトが利用可能 Stateオブジェクトは利用不可 • _StateLifecycle • created • initialized • ready • defunct

Slide 21

Slide 21 text

% • createState • initState • didChangeDependencies • build • didUpdateWidget • setState • deactivate • dispose 20 Stateful Widgetで重要なライフサイクルのイベント

Slide 22

Slide 22 text

% • createState • initState • didChangeDependencies • build • didUpdateWidget • setState • deactivate • dispose 21 Stateful Widgetで重要なライフサイクルのイベント createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized

Slide 23

Slide 23 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized 22

Slide 24

Slide 24 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized 23

Slide 25

Slide 25 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized 24

Slide 26

Slide 26 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized 25

Slide 27

Slide 27 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized build 26 setState didUpdateWidget

Slide 28

Slide 28 text

% createState initState didChangeDependencies deactivate dispose defunct ready created build didUpdateWidget setState initialized deactivate 27 dispose

Slide 29

Slide 29 text

% • Flutterの描画にはWidget treeとElement tree, RenderObject treeがある • Elementにもライフサイクルが存在する(_ElementLifecycle) • initial • active • inactive • defunct 28 Element lifecycle

Slide 30

Slide 30 text

% 実際の挙動 29

Slide 31

Slide 31 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xffd8e2ff), borderRadius: BorderRadius.circular(32), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment parent'), ), const SizedBox(height: 32), ChildWidget(), ], ), ); } 30 親Widet

Slide 32

Slide 32 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xffd8e2ff), borderRadius: BorderRadius.circular(32), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment parent'), ), const SizedBox(height: 32), ChildWidget(), ], ), ); } 31 親Widet

Slide 33

Slide 33 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xffd8e2ff), borderRadius: BorderRadius.circular(32), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment parent'), ), const SizedBox(height: 32), ChildWidget(), ], ), ); } 32 親Widet

Slide 34

Slide 34 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xffd8e2ff), borderRadius: BorderRadius.circular(32), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment parent'), ), const SizedBox(height: 32), ChildWidget(), ], ), ); } 33 親Widet

Slide 35

Slide 35 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xfffdd7fa), borderRadius: BorderRadius.circular(32), ), child: Column( children: [ Text('Child counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment child'), ), ], ), ); } 34 子Widet

Slide 36

Slide 36 text

% 35 Child counter: 0 Parent counter: 0 Increment parent Increment child

Slide 37

Slide 37 text

% parent: initState parent: didChangeDependencies parent: build child: initState child: didChangeDependencies child: build 初回ビルド時 Child counter: 0 Parent counter: 0 Increment parent Increment child 36

Slide 38

Slide 38 text

% child: setState child: build Childボタン押下時 Child counter: 5 Parent counter: 0 Increment parent Increment child 37

Slide 39

Slide 39 text

% Parentボタン押下時 Child counter: 5 Parent counter: 3 Increment parent Increment child parent: setState parent: build child: didUpdateWidget child: build 38

Slide 40

Slide 40 text

% Parentボタン押下時 Child counter: 5 Parent counter: 3 Increment parent Increment child parent: setState parent: build child: didUpdateWidget child: build 親のWidgetが更新されたため呼ばれる 39

Slide 41

Slide 41 text

% parent: deactivate child: deactivate child: dispose parent: dispose Widget破棄時 Child counter: 5 Parent counter: 3 Increment parent Increment child 40

Slide 42

Slide 42 text

% 41 didUpdateWidget呼ばれないようにする

Slide 43

Slide 43 text

% Parentボタン押下時 Child counter: 5 Parent counter: 3 Increment parent Increment child parent: setState parent: build child: didUpdateWidget child: build 42 子も更新されてしまう

Slide 44

Slide 44 text

% @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: const Color(0xffd8e2ff), borderRadius: BorderRadius.circular(32), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_counter'), const SizedBox(height: 8), OutlinedButton( onPressed: () => _incrementCounter(), child: const Text('Increment parent'), ), const SizedBox(height: 32), ChildWidget(), ], ), ); } 43 親Widet

Slide 45

Slide 45 text

% ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Parent counter: $_count const SizedBox(height: 8), OutlinedButton( onPressed: () => _increment child: const Text('Incremen ), const SizedBox(height: 32), const ChildWidget(), ], ), ); } 44

Slide 46

Slide 46 text

% Parentボタン押下時 Child counter: 5 Parent counter: 4 Increment parent Increment child parent: setState parent: build 親のみRebuildされる 45

Slide 47

Slide 47 text

% Parentボタン押下時 Child counter: 5 Parent counter: 4 Increment parent Increment child parent: setState parent: build 親のみRebuildされる 46 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 48

Slide 48 text

% Parentボタン押下時 Child counter: 5 Parent counter: 4 Increment parent Increment child parent: setState parent: build 親のみRebuildされる 47 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 49

Slide 49 text

% Parentボタン押下時 Child counter: 5 Parent counter: 4 Increment parent Increment child parent: setState parent: build 親のみRebuildされる X Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 50

Slide 50 text

% • OSによる操作をFlutterのフレームワークが吸収したもの • Flutter 3.13.0で大きな変更があった • 状態の追加(hidden) • イベントの発火パターンが増えた • 状態遷移の流れが変わった • AppLifecycleListener(WidgetBindingObserver)でハンドリング 48 App lifecycle

Slide 51

Slide 51 text

% • resumed • inactive • hidden • paused • detached 49 App lifecycle state

Slide 52

Slide 52 text

% • resumed • inactive • hidden → Flutter 3.13.0から追加 • paused • detached 50 App lifecycle state https://docs. fl utter.dev/release/breaking-changes/add-applifecyclestate-hidden

Slide 53

Slide 53 text

% 51 https://github.com/ fl utter/engine/pull/40542

Slide 54

Slide 54 text

% 52 https://github.com/ fl utter/engine/pull/40542 hidden 3.13.0以前は非対応だった macOSとLinuxのために追加 された OK CANCEL

Slide 55

Slide 55 text

% 53 App Lifecycleの状態遷移図 resumed inactive hidden paused detached onInactive onHide onPause onDetach onResume onShow onRestart

Slide 56

Slide 56 text

% 54 App Lifecycleの状態遷移図 resumed inactive hidden paused detached onInactive onHide onPause onDetach onResume onShow onRestart Foreground状態 Kill状態 Background状態 --------------------- ---------------------

Slide 57

Slide 57 text

% • ライフサイクルイベント発火パターンが3.13.0の前後で異なる • OSによってイベント、発火タイミングが異なる 55 実際の挙動

Slide 58

Slide 58 text

% • アプリからOSホームへの遷移 • OSホームからアプリへの遷移 • アプリからタスク一覧への遷移 • タスク一覧からアプリへの遷移 • 画面OFF • 画面ON • アプリ終了 56 実際の挙動 • Noti fi cation, Control Centerを表示 • Noti fi cation, Control Centerを非表示 • 画面分割 • 画面分割時のフォーカス切り替え • 自分のアプリ側の画面分割を終了 • もう一方のアプリ側の画面分割を終了

Slide 59

Slide 59 text

% ~3.13.0 inactive paused アプリからOSホームへの遷移 57 3.13.0~ inactive hidden paused J04

Slide 60

Slide 60 text

% ~3.13.0 resumed OSホームからアプリへの遷移 58 3.13.0~ hidden inactive resumed J04

Slide 61

Slide 61 text

% ~3.13.0 inactive paused (その後OSホーム、他アプリにいっても変化なし) アプリからタスク一覧への遷移 59 3.13.0~ inactive hidden paused (その後OSホーム、他アプリにいっても変化なし)

Slide 62

Slide 62 text

% ~3.13.0 inactive (その後OSホーム、他アプリに遷移した場合) paused アプリからタスク一覧への遷移 60 3.13.0~ inactive (その後OSホーム、他アプリに遷移した場合) hidden paused J04

Slide 63

Slide 63 text

% ~3.13.0 resumed タスク一覧からアプリへの遷移 61 3.13.0~ hidden inactive resumed

Slide 64

Slide 64 text

% ~3.13.0 • アプリ=>タスク一覧=>アプリ resumed • OSホーム=>タスク一覧=>アプリ inactive resumed タスク一覧からアプリへの遷移 62 3.13.0~ • アプリ=>タスク一覧=>アプリ resumed • OSホーム=>タスク一覧=>アプリ hidden inactive resumed J04

Slide 65

Slide 65 text

% ~3.13.0 inactive paused 画面OFF 63 3.13.0~ inactive hidden paused J04

Slide 66

Slide 66 text

% ~3.13.0 resumed 画面ON 64 3.13.0~ hidden inactive resumed J04

Slide 67

Slide 67 text

% ~3.13.0 イベントなし 通知欄を表示 65 3.13.0~ inactive

Slide 68

Slide 68 text

% ~3.13.0 イベントなし 通知欄を非表示 66 3.13.0~ resumed

Slide 69

Slide 69 text

% ~3.13.0 • 開く inactive • 閉じる resumed 通知欄, 設定 67 3.13.0~ • 開く inactive • 閉じる resumed J04

Slide 70

Slide 70 text

% ~3.13.0 inactive paused detached アプリ終了 68 3.13.0~ inactive hidden paused detached J04

Slide 71

Slide 71 text

% ~3.13.0 (inactive) (paused) resumed inactive resumed 画面分割 (Android) 69 3.13.0~ (inactive) (hidden) (paused) hidden inactive resumed

Slide 72

Slide 72 text

% ~3.13.0 inactive resumed 画面分割 (iPad) 70 3.13.0~ inactive resumed J1BE

Slide 73

Slide 73 text

% ~3.13.0 変化なし 画面分割時のフォーカス切り替え 71 3.13.0~ • unfocused inactive • focused resumed

Slide 74

Slide 74 text

% ~3.13.0 (変化なし) 画面分割時のフォーカス切り替え 72 3.13.0~ (変化なし) J1BE

Slide 75

Slide 75 text

% ~3.13.0 inactive paused 自分のアプリ側の画面分割を終了 73 3.13.0~ inactive hidden paused

Slide 76

Slide 76 text

% ~3.13.0 inactive paused 自分のアプリ側の画面分割を終了 74 3.13.0~ inactive hidden paused J1BE

Slide 77

Slide 77 text

% ~3.13.0 フォーカスに関係なく変化なし もう一方のアプリ側の画面分割を終了 75 3.13.0~ • unfocused resumed • focused 変化なし

Slide 78

Slide 78 text

% ~3.13.0 inactive resumed もう一方のアプリ側の画面分割を終了 76 3.13.0~ inactive resumed J1BE

Slide 79

Slide 79 text

% 3.13.0~ • Focus resumed • Unfocus inactive フォーカス切り替え 77 NBD04

Slide 80

Slide 80 text

% 3.13.0~ • 覆う hidden • 表示 inactive 画面を覆う 78 NBD04

Slide 81

Slide 81 text

% 3.13.0~ • 画面OFF inactive hidden • 画面ON inactive resumed 画面ON/OFF 79 NBD04

Slide 82

Slide 82 text

% 3.13.0~ inactive hidden 最小化 80 NBD04

Slide 83

Slide 83 text

% 3.13.0~ inactive resumed 最小化からフォーカス 81 NBD04

Slide 84

Slide 84 text

% 3.13.0~ inactive hidden inactive resumed inactive ... (何度かイベント発火) resumed フルスクリーン化 82 NBD04

Slide 85

Slide 85 text

% 3.13.0~ inactive hidden inactive resumed inactive hidden inactive resumed フルスクリーン解除 83 NBD04

Slide 86

Slide 86 text

% ~3.13.0 イベントなし WEB 84 3.13.0~ イベントなし

Slide 87

Slide 87 text

% 85 Android iOS(iPhone/iPad) アプリからOSホームへの遷移 inactive->hidden->paused inactive->hidden->paused OSホームからアプリへの遷移 hid->ina->res hid->ina->res アプリからタスク一覧への遷移 ina->hid->pau ina, (to home: hid->pau) タスク一覧からアプリへの遷移 hid->ina->res res, (from home: hid->ina->res) 画面OFF ina->hid->pau ina->hid->pau 画面ON hid->ina->res hid->ina->res Noti fi cation, Control Centerを表示 inactive inactive Noti fi cation, Control Centerを非表示 resumed resumed アプリ終了 ina->hid->pau->detached ina->hid->pau->detached 画面分割 hid->ina->res ina->res 画面分割時のフォーカス切り替え inactive/resumed - 自分のアプリ側の画面分割を終了 ina->hid->pau ina->hid->pau もう一方のアプリ側の画面分割を終了 focused: -, unfocused: resumed ina->res

Slide 88

Slide 88 text

% 86 Android iOS(iPhone/iPad) アプリからOSホームへの遷移 inactive->hidden->paused inactive->hidden->paused OSホームからアプリへの遷移 hid->ina->res hid->ina->res アプリからタスク一覧への遷移 ina->hid->pau ina, (to home: hid->pau) タスク一覧からアプリへの遷移 hid->ina->res res, (from home: hid->ina->res) 画面OFF ina->hid->pau ina->hid->pau 画面ON hid->ina->res hid->ina->res Noti fi cation, Control Centerを表示 inactive inactive Noti fi cation, Control Centerを非表示 resumed resumed アプリ終了 ina->hid->pau->detached ina->hid->pau->detached 画面分割 hid->ina->res ina->res 画面分割時のフォーカス切り替え inactive/resumed - 自分のアプリ側の画面分割を終了 ina->hid->pau ina->hid->pau もう一方のアプリ側の画面分割を終了 focused: -, unfocused: resumed ina->res

Slide 89

Slide 89 text

% • 今まではWidgetsBindingObserverをWidgetに対して実装する必要があった • 3.13.0からAppLifecycleListenerが追加 87 AppLifecycleListener

Slide 90

Slide 90 text

% 88 class ExamplePage extends StatefulWidget { const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print(state); } @override Widget build(BuildContext context) { return Contaienr(); } } WidgetsBindingObserver

Slide 91

Slide 91 text

% 89 const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print(state); } @override Widget build(BuildContext context) { return Contaienr();

Slide 92

Slide 92 text

% 90 const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print(state); } @override Widget build(BuildContext context) { return Contaienr();

Slide 93

Slide 93 text

% 91 const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print(state); } @override Widget build(BuildContext context) { return Contaienr();

Slide 94

Slide 94 text

% 92 const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { print(state); } @override Widget build(BuildContext context) { return Contaienr();

Slide 95

Slide 95 text

% 93 class ExamplePage extends StatefulWidget { const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State { late final AppLifecycleListener appLifecycleListener; @override void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onShow: () => print('onShow'), onResume: () => print('onResume'), onHide: () => print('onHide'), onInactive: () => print('onInactive'), onPause: () => print('onPause'), onDetach: () => print('onDetach'), onRestart: () => print('onRestart'), onStateChange: (state) => print(state.toString()), ); } @override void dispose() { appLifecycleListener.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } AppLifecycleListener

Slide 96

Slide 96 text

% 94 class ExamplePage extends StatefulWidget { const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State { late final AppLifecycleListener appLifecycleListener; @override void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onShow: () => print('onShow'), onResume: () => print('onResume'), onHide: () => print('onHide'), onInactive: () => print('onInactive'), onPause: () => print('onPause'), onDetach: () => print('onDetach'), onRestart: () => print('onRestart'), onStateChange: (state) => print(state.toString()), ); }

Slide 97

Slide 97 text

% 95 class ExamplePage extends StatefulWidget { const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State { late final AppLifecycleListener appLifecycleListener; @override void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onShow: () => print('onShow'), onResume: () => print('onResume'), onHide: () => print('onHide'), onInactive: () => print('onInactive'), onPause: () => print('onPause'), onDetach: () => print('onDetach'), onRestart: () => print('onRestart'), onStateChange: (state) => print(state.toString()), ); }

Slide 98

Slide 98 text

% 96 class ExamplePage extends StatefulWidget { const ExamplePage({super.key}); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State { late final AppLifecycleListener appLifecycleListener; @override void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onStateChange: (state) => print(state.toString()), ); } @override void dispose() { appLifecycleListener.dispose(); super.dispose(); } @override

Slide 99

Slide 99 text

% 97 void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onShow: () => print('onShow'), onResume: () => print('onResume'), onHide: () => print('onHide'), onInactive: () => print('onInactive'), onPause: () => print('onPause'), onDetach: () => print('onDetach'), onRestart: () => print('onRestart'), onStateChange: (state) => print(state.toString()), ); } @override void dispose() { appLifecycleListener.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } }

Slide 100

Slide 100 text

% 98 @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State { late final AppLifecycleListener appLifecycleListener; @override void initState() { super.initState(); appLifecycleListener = AppLifecycleListener( onShow: () => print('onShow'), onResume: () => print('onResume'), onHide: () => print('onHide'), onInactive: () => print('onInactive'), onPause: () => print('onPause'), onDetach: () => print('onDetach'), onRestart: () => print('onRestart'), onStateChange: (state) => print(state.toString()), ); } @override void dispose() { appLifecycleListener.dispose(); super.dispose(); } AppLifecycleStateにはない

Slide 101

Slide 101 text

% AppLifecycleState inactive hidden paused アプリからOSホームへの遷移 99 AppLifecycleListener onInactive onHide onPause AppLifecycleState AppLifecycleListener onRestart onShow onResume hidden inactive resumed OSホームからアプリへの遷移

Slide 102

Slide 102 text

% AppLifecycleState inactive hidden paused アプリからOSホームへの遷移 100 onInactive onHide onPause AppLifecycleState onRestart onShow onResume hidden inactive resumed OSホームからアプリへの遷移 AppLifecycleListener AppLifecycleListener

Slide 103

Slide 103 text

% 101 inactive hidden paused アプリからOSホームへの遷移 AppLifecycleState onInactive onHide onPause AppLifecycleState onRestart onShow onResume AppLifecycleListener AppLifecycleListener hidden inactive resumed OSホームからアプリへの遷移 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 104

Slide 104 text

% 102 inactive hidden paused アプリからOSホームへの遷移 AppLifecycleState AppLifecycleListener AppLifecycleListener onInactive onHide onPause AppLifecycleState onRestart onShow onResume hidden inactive resumed OSホームからアプリへの遷移 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 105

Slide 105 text

% X inactive hidden paused アプリからOSホームへの遷移 AppLifecycleState AppLifecycleListener AppLifecycleListener onInactive onHide onPause AppLifecycleState onRestart onShow onResume hidden inactive resumed OSホームからアプリへの遷移 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 106

Slide 106 text

% • Widgetごとのライフサイクルハンドリング • アプリ全体のライフサイクルハンドリング • 画面遷移はハンドリングできない => Navigator history stackを監視する必要がある 103 ここまででできること

Slide 107

Slide 107 text

% • 現在主流のものは主に3パターン • Navigator (通称Navigator1.0) • Router (通称Navigator2.0) • ライブラリーを使う(サードパーティ含む) • fl utter.dev/go_router • codeness.ly/auto_route • tom.gilder.dev/routemaster • etc... 104 Navigator

Slide 108

Slide 108 text

% • 現在主流のものは主に3パターン • Navigator (通称Navigator1.0) • Router (通称Navigator2.0) • ライブラリーを使う(サードパーティ含む) • fl utter.dev/go_router • codeness.ly/auto_route • tom.gilder.dev/routemaster • etc... 105 Navigator https://docs. fl utter.dev/ui/navigation

Slide 109

Slide 109 text

% 106 Page1 Page2 Page3 Page1 Page2 Page3 Page4 Page1 Page2 Page3 Push Page4 Pop Page4

Slide 110

Slide 110 text

% 107 Page1 Page2 Page3 Page1 Page2 Page3 Page4 Page1 Page2 Page3 Navigator Router (GoRouter etc...) Widget

Slide 111

Slide 111 text

% • Navigator history stackの変化を監視 • didPush • didPop • didRemove • didReplace • (didStartUserGesture) • (didStopUserGesture) 108 NavigatorObserver

Slide 112

Slide 112 text

% 109 class NavigatorObserver { NavigatorState? get navigator => _navigators[this]; static final Expando _navigators = Expando(); void didPush(Route route, Route? previousRoute) { } void didPop(Route route, Route? previousRoute) { } void didRemove(Route route, Route? previousRoute) { } void didReplace({ Route? newRoute, Route? oldRoute }) { } void didStartUserGesture(Route route, Route? previousRoute) { } void didStopUserGesture() { } }

Slide 113

Slide 113 text

% • RouteObserverと組み合わせて利用する • 実装されたWidgetのNavigationに変更があると通知される • didPopNext • didPush • didPop • didPushNext 110 RouteAware

Slide 114

Slide 114 text

% 111 abstract mixin class RouteAware { void didPopNext() { } void didPush() { } void didPop() { } void didPushNext() { } }

Slide 115

Slide 115 text

% 112 abstract mixin class RouteAware { void didPopNext() { } void didPush() { } void didPop() { } void didPushNext() { } } Page1 Page2 対象WidgetがPushされたタイミング

Slide 116

Slide 116 text

% 113 abstract mixin class RouteAware { void didPopNext() { } void didPush() { } void didPop() { } void didPushNext() { } } 対象Widgetの上に別のWidgetが Pushされたタイミング Page1 Page2 Page3

Slide 117

Slide 117 text

% 114 abstract mixin class RouteAware { void didPopNext() { } void didPush() { } void didPop() { } void didPushNext() { } } 対象Widgetの上にある別のWidgetが Popされたタイミング Page1 Page2 Page3

Slide 118

Slide 118 text

% 115 abstract mixin class RouteAware { void didPopNext() { } void didPush() { } void didPop() { } void didPushNext() { } } 対象WidgetがPopされたタイミング Page1 Page2

Slide 119

Slide 119 text

% 116 class RouteObserver> extends NavigatorObserver { @override void didPop(Route route, Route? previousRoute) { ... } @override void didPush(Route route, Route? previousRoute) { ... } ... }

Slide 120

Slide 120 text

% 117 final RouteObserver routeObserver = RouteObserver();

Slide 121

Slide 121 text

% 118 final RouteObserver routeObserver = RouteObserver(); Navigator( pages: [ ... ], onPopPage: (route, result) { ... } ); MaterialApp( routes: { ... }, ); GoRouter( initialLocation: '/', routes: [ ... ], ); Navigator1.0 Navigator2.0(Router) GoRouter

Slide 122

Slide 122 text

% 119 final RouteObserver routeObserver = RouteObserver(); Navigator( observers: [ routeObserver, ], pages: [ ... ], onPopPage: (route, result) { ... } ); MaterialApp( navigatorObservers: [ routeObserver, ], routes: { ... }, ); GoRouter( observers: [ routeObserver, ], initialLocation: '/', routes: [ ... ], ); Navigator1.0 GoRouter Navigator2.0(Router)

Slide 123

Slide 123 text

% 120 class ExamplePage extends StatefulWidget { const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } @override void didPush() { } @override void didPop() { } @override void didPushNext() { } @override void didPopNext() { } @override Widget build(BuildContext context) { return Container(); } }

Slide 124

Slide 124 text

% class ExamplePage extends StatefulWidget { const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override 121 @override void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } }

Slide 125

Slide 125 text

% 122 void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } } const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); }

Slide 126

Slide 126 text

% 123 @override void didPush() { } @override void didPop() { } @override void didPushNext() { } @override void didPopNext() { } @override Widget build(BuildContext context) { return Container(); } } extends StatefulWidget { e({ super.key }); dget> createState() => _ExamplePageState(); State extends State with RouteAware { pendencies() { eDependencies(); subscribe( f(context)! as PageRoute,

Slide 127

Slide 127 text

% 124 void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } } const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); }

Slide 128

Slide 128 text

% void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } } const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } 125 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 129

Slide 129 text

% void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } } const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } 126 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 130

Slide 130 text

% void didPush() } @override void didPop() } @override void didPushNe } @override void didPopNex } @override Widget build(B return Conta } } const ExamplePage({ super.key }); @override State createState() => _ExamplePageState(); } class _ExamplePageState extends State with RouteAware { @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe( this, ModalRoute.of(context)! as PageRoute, ); } @override void dispose() { routeObserver.unsubscribe(this); super.dispose(); } X Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 131

Slide 131 text

% 127 Handling Flutter lifecycle • 従来のAndroid, iOSのようなライフサイクルイベントは Flutterには用意されていない • Android: onResume, onPause, etc. iOS: viewDidAppear, viewDidDesappear, etc. • 画面遷移 • 端末の画面ON/OFF • アプリ自体のバックグラウンドへの移行イベント

Slide 132

Slide 132 text

% 128 Handling Flutter lifecycle • 従来のAndroid, iOSのようなライフサイクルイベントは Flutterには用意されていない • Android: onResume, onPause, etc. iOS: viewDidAppear, viewDidDesappear, etc. • 画面遷移 • 端末の画面ON/OFF • アプリ自体のバックグラウンドへの移行イベント Handling lifecycle AppLifecycleListener (WidgetBindingObserver)と NavigationObserverを 組み合わせる必要がある OK CANCEL

Slide 133

Slide 133 text

% 129 sealed class LifecycleEvent { const LifecycleEvent(this.isSystemEvent); final bool isSystemEvent; } class LifecycleEventResumed extends LifecycleEvent { const LifecycleEventResumed(super.isSystemEvent); @override String toString() => 'LifecycleEventResumed(isSystemEvent: $isSystemEvent)'; } class LifecycleEventPaused extends LifecycleEvent { const LifecycleEventPaused(super.isSystemEvent); @override String toString() => 'LifecycleEventPaused(isSystemEvent: $isSystemEvent)'; } typedef LifecycleObserverCallback = void Function(LifecycleEvent event);

Slide 134

Slide 134 text

% 130 sealed class LifecycleEvent { const LifecycleEvent(this.isSystemEvent); final bool isSystemEvent; } class LifecycleEventResumed extends LifecycleEvent { const LifecycleEventResumed(super.isSystemEvent); @override String toString() => 'LifecycleEventResumed(isSystemEvent: $isSystemEvent)'; } class LifecycleEventPaused extends LifecycleEvent { const LifecycleEventPaused(super.isSystemEvent); @override String toString() => 'LifecycleEventPaused(isSystemEvent: $isSystemEvent)'; }

Slide 135

Slide 135 text

% 131 sealed class LifecycleEvent { const LifecycleEvent(this.isSystemEvent); final bool isSystemEvent; } class LifecycleEventResumed extends LifecycleEvent { const LifecycleEventResumed(super.isSystemEvent); @override String toString() => 'LifecycleEventResumed(isSystemEvent: $isSystemEvent)'; } class LifecycleEventPaused extends LifecycleEvent { const LifecycleEventPaused(super.isSystemEvent); @override String toString() => 'LifecycleEventPaused(isSystemEvent: $isSystemEvent)'; } Navigation or 画面ON, OFFなどのイベントを 判別するため

Slide 136

Slide 136 text

% 132 final bool isSystemEvent; } class LifecycleEventResumed extends LifecycleEvent { const LifecycleEventResumed(super.isSystemEvent); @override String toString() => 'LifecycleEventResumed(isSystemEvent: $isSystemEvent)'; } class LifecycleEventPaused extends LifecycleEvent { const LifecycleEventPaused(super.isSystemEvent); @override String toString() => 'LifecycleEventPaused(isSystemEvent: $isSystemEvent)'; } typedef LifecycleObserverCallback = void Function(LifecycleEvent event);

Slide 137

Slide 137 text

% 133 class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { callback(const LifecycleEventResumed(false)); } @override void didPop() { callback(const LifecycleEventPaused(false)); _appLifecycleListener.dispose(); } @override void didPushNext() { callback(const LifecycleEventPaused(false)); } @override void didPopNext() { callback(const LifecycleEventResumed(false)); } }

Slide 138

Slide 138 text

% 134 class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() {

Slide 139

Slide 139 text

% class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { 135

Slide 140

Slide 140 text

% class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { 136

Slide 141

Slide 141 text

% class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { 137

Slide 142

Slide 142 text

% class AppLifecycleObserver with RouteAware { AppLifecycleObserver({ required this.routeObserver, required ModalRoute route, required this.callback, }) { _appLifecycleListener = AppLifecycleListener( onResume: () { callback(const LifecycleEventResumed(true)); }, onPause: () { callback(const LifecycleEventPaused(true)); }, ); routeObserver.subscribe(this, route); } late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { 138

Slide 143

Slide 143 text

% 139 late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { callback(const LifecycleEventResumed(false)); } @override void didPop() { callback(const LifecycleEventPaused(false)); _appLifecycleListener.dispose(); } @override void didPushNext() { callback(const LifecycleEventPaused(false)); } @override void didPopNext() { callback(const LifecycleEventResumed(false)); } }

Slide 144

Slide 144 text

% late final AppLifecycleListener _appLifecycleListener; final RouteObserver routeObserver; final LifecycleObserverCallback callback; @override void didPush() { callback(const LifecycleEventResumed(false)); } @override void didPop() { callback(const LifecycleEventPaused(false)); _appLifecycleListener.dispose(); } @override void didPushNext() { callback(const LifecycleEventPaused(false)); } @override void didPopNext() { callback(const LifecycleEventResumed(false)); } } 140

Slide 145

Slide 145 text

% 141 final RouteObserver routeObserver = RouteObserver(); Navigator( observers: [ routeObserver, ], pages: [ ... ], onPopPage: (route, result) { ... } ); MaterialApp( navigatorObservers: [ routeObserver, ], routes: { ... }, ); GoRouter( observers: [ routeObserver, ], initialLocation: '/', routes: [ ... ], ); Navigator1.0 GoRouter Navigator2.0(Router) 利用側

Slide 146

Slide 146 text

% 142 利用側 class Page extends StatefulWidget { const Page({super.key}); @override State createState() => _PageState(); } class _PageState extends State { bool _initialized = false; @override void didChangeDependencies() { super.didChangeDependencies(); if(!_initialized) { AppLifecycleObserver( routeObserver: routeObserver, route: ModalRoute.of(context)!, callback: (event) { }, ); _initialized = true; } } @override Widget build(BuildContext context) { return Container(); } }

Slide 147

Slide 147 text

% 143 t { ate() => eState(); class _PageState extends State { bool _initialized = false; @override void didChangeDependencies() { super.didChangeDependencies(); if(!_initialized) { AppLifecycleObserver( routeObserver: routeObserver, route: ModalRoute.of(context)!, callback: (event) { }, ); _initialized = true; } }

Slide 148

Slide 148 text

% 144 t { ate() => eState(); class _PageState extends State { bool _initialized = false; @override void didChangeDependencies() { super.didChangeDependencies(); if(!_initialized) { AppLifecycleObserver( routeObserver: routeObserver, route: ModalRoute.of(context)!, callback: (event) { }, ); _initialized = true; } }

Slide 149

Slide 149 text

% • 問題点 • 各Widgetでそれぞれ書くのは冗長 • Contextが必要 145 Handling Flutter lifecycle

Slide 150

Slide 150 text

% • 問題点 • 各Widgetでそれぞれ書くのは冗長 • Contextが必要 146 Handling Flutter lifecycle PageName(String)をKeyにしたLifecycleObserverを作ってみる

Slide 151

Slide 151 text

% • 問題点 • 各Widgetでそれぞれ書くのは冗長 • Contextが必要 147 Handling Flutter lifecycle PageName(String)をKeyにしたLifecycleObserverを作ってみる 自作LifecycleObserverは参考程度に

Slide 152

Slide 152 text

% 148 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } @override void didPush(Route route, Route? previousRoute) { _routeStack.add(route); _emitLifecycleEvent(previousRoute, const LifecycleEventPaused(false)); _emitLifecycleEvent(route, const LifecycleEventResumed(false)); } @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didReplace({Route? newRoute, Route? oldRoute}) { final index = _routeStack.indexWhere((element) => element == oldRoute); if (_routeStack.last == oldRoute) { _emitLifecycleEvent(oldRoute, const LifecycleEventPaused(false)); } _routeStack.remove(oldRoute); if (index >= 0 && newRoute != null) { _routeStack.insert(index, newRoute); } if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); if (_routeStack.isNotEmpty && _routeStack.last.settings.name == key) { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(false)); } } void unsubscribe(String key) { _subscribers.remove(key); } void _emitLifecycleEvent(Route? route, LifecycleEvent event) { final key = route?.settings.name; if (key == null) { return; } final subscribers = _subscribers[key]; if (subscribers == null) { return; } for (final subscriber in subscribers) { subscriber.call(event); } } }

Slide 153

Slide 153 text

% 149 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); @over void fin if _ } _ro if _ } if _ } } void _ap _su } void fin

Slide 154

Slide 154 text

% 150 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); @over void fin if _ } _ro if _ } if _ } } void _ap _su } void fin Navigation stack historyを管理

Slide 155

Slide 155 text

% 151 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); @over void fin if _ } _ro if _ } if _ } } void _ap _su } void fin StringのKeyでListenerの管理 Keyには route.nameを使う

Slide 156

Slide 156 text

% 152 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); @over void fin if _ } _ro if _ } if _ } } void _ap _su } void fin AppLifecycleListenerは同じ

Slide 157

Slide 157 text

% 153 class AppLifecycleObserver extends NavigatorObserver { AppLifecycleObserver() { _appLifecycleListener = AppLifecycleListener( onResume: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(true)); }, onPause: () { _emitLifecycleEvent(_routeStack.last, const LifecycleEventPaused(true)); }, ); } late final AppLifecycleListener _appLifecycleListener; final Map> _subscribers = >{}; final List> _routeStack = []; @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); @over void fin if _ } _ro if _ } if _ } } void _ap _su } void fin Navigation route の一番上にあるRouteに通知

Slide 158

Slide 158 text

% 154 e)); )); lse)); _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); if (_routeStack.isNotEmpty && _routeStack.last.settings.name == key) { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(false)); } } void unsubscribe(String key) { _subscribers.remove(key); } void _emitLifecycleEvent(Route? route, LifecycleEvent event) { final key = route?.settings.name; if (key == null) { return; } final subscribers = _subscribers[key]; if (subscribers == null) { return; } for (final subscriber in subscribers) { subscriber.call(event); } } }

Slide 159

Slide 159 text

% 155 @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } @override void didPush(Route route, Route? previousRoute) { _routeStack.add(route); _emitLifecycleEvent(previousRoute, const LifecycleEventPaused(false)); _emitLifecycleEvent(route, const LifecycleEventResumed(false)); } @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } } void fin sub if _ } } void _su } void fin if r } fin if r } for

Slide 160

Slide 160 text

% 156 @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } @override void didPush(Route route, Route? previousRoute) { _routeStack.add(route); _emitLifecycleEvent(previousRoute, const LifecycleEventPaused(false)); _emitLifecycleEvent(route, const LifecycleEventResumed(false)); } @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } } void fin sub if _ } } void _su } void fin if r } fin if r } for

Slide 161

Slide 161 text

% 157 @override void didPop(Route route, Route? previousRoute) { _routeStack.remove(route); _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } @override void didPush(Route route, Route? previousRoute) { _routeStack.add(route); _emitLifecycleEvent(previousRoute, const LifecycleEventPaused(false)); _emitLifecycleEvent(route, const LifecycleEventResumed(false)); } @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } } void fin sub if _ } } void _su } void fin if r } fin if r } for

Slide 162

Slide 162 text

% 158 true)); rue)); @override void didReplace({Route? newRoute, Route? oldRoute}) { final index = _routeStack.indexWhere((element) => element == oldRoute); if (_routeStack.last == oldRoute) { _emitLifecycleEvent(oldRoute, const LifecycleEventPaused(false)); } _routeStack.remove(oldRoute); if (index >= 0 && newRoute != null) { _routeStack.insert(index, newRoute); } if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback);

Slide 163

Slide 163 text

% 159 true)); rue)); @override void didReplace({Route? newRoute, Route? oldRoute}) { final index = _routeStack.indexWhere((element) => element == oldRoute); if (_routeStack.last == oldRoute) { _emitLifecycleEvent(oldRoute, const LifecycleEventPaused(false)); } _routeStack.remove(oldRoute); if (index >= 0 && newRoute != null) { _routeStack.insert(index, newRoute); } if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); Page1 Page2 Page3 Page1 Page4 Page3 Page1 Page2 Page3 Page1 Page2 Page4

Slide 164

Slide 164 text

% 160 true)); rue)); @override void didReplace({Route? newRoute, Route? oldRoute}) { final index = _routeStack.indexWhere((element) => element == oldRoute); if (_routeStack.last == oldRoute) { _emitLifecycleEvent(oldRoute, const LifecycleEventPaused(false)); } _routeStack.remove(oldRoute); if (index >= 0 && newRoute != null) { _routeStack.insert(index, newRoute); } if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback);

Slide 165

Slide 165 text

% 161 ; if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); if (_routeStack.isNotEmpty && _routeStack.last.settings.name == key) { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(false)); } } void unsubscribe(String key) { _subscribers.remove(key); } void _emitLifecycleEvent(Route? route, LifecycleEvent event) { final key = route?.settings.name; if (key == null) {

Slide 166

Slide 166 text

% 162 ; if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe(String key, LifecycleObserverCallback callback) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); if (_routeStack.isNotEmpty && _routeStack.last.settings.name == key) { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(false)); } } void unsubscribe(String key) { _subscribers.remove(key); } void _emitLifecycleEvent(Route? route, LifecycleEvent event) { final key = route?.settings.name; if (key == null) {

Slide 167

Slide 167 text

% 163 ; if (_routeStack.last == newRoute) { _emitLifecycleEvent(newRoute, const LifecycleEventResumed(false)); } } void dispose() { _appLifecycleListener.dispose(); _subscribers.clear(); } void subscribe( ɹ String key, LifecycleObserverCallback callback, { bool notifyImmediately = true, }) { final Set subscribers = _subscribers.putIfAbsent(key, () => {}); subscribers.add(callback); if (notifyImmediately && _routeStack.isNotEmpty && _routeStack.last.settings.name == key) { _emitLifecycleEvent(_routeStack.last, const LifecycleEventResumed(false)); } } void unsubscribe(String key) { _subscribers.remove(key); すぐに通知されてしまうので、 適宜フラグで送らないようにするのも良い

Slide 168

Slide 168 text

% 164 final goRouter = GoRouter( initialLocation: '/', observers: [ appLifecycleObserver, ], routes: [ GoRoute( path: '/', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: '/', child: const MyHomePage(), ); }, ), GoRoute( path: '/page1', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: 'page1', child: const Page1(), ); }, ), GoRoute( path: '/page2', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: 'page2', child: const Page2(), ); }, ), ], ); 利用側

Slide 169

Slide 169 text

% 165 final goRouter = GoRouter( initialLocation: '/', observers: [ appLifecycleObserver, ], routes: [ GoRoute( path: '/', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: '/', child: const MyHomePage(), ); }, ), GoRoute( path: '/page1', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: 'page1', child: const Page1(), ); }, ), GoRoute( path: '/page2', pageBuilder: (context, state) { return MaterialPage( key: state.pageKey, name: 'page2', child: const Page2(), ); }, ), ], ); 利用側 MaterialPage.nameにnameをセット

Slide 170

Slide 170 text

% 166 利用側 class _PageState extends State { bool _initialized = false; @override void didChangeDependencies() { super.didChangeDependencies(); if(!_initialized) { appLifecycleObserver.subscribe('pageName', (event) { switch (event) { case LifecycleEventResumed(): print('onResume: ${event.isSystemEvent}'); case LifecycleEventPaused(): print('onPause: ${event.isSystemEvent}'); } }); _initialized = true; } } @override Widget build(BuildContext context) { return Container(); } }

Slide 171

Slide 171 text

% 167 利用側 class _PageState extends State { bool _initialized = false; @override void didChangeDependencies() { super.didChangeDependencies(); if(!_initialized) { appLifecycleObserver.subscribe('pageName', (event) { switch (event) { case LifecycleEventResumed(): print('onResume: ${event.isSystemEvent}'); case LifecycleEventPaused(): print('onPause: ${event.isSystemEvent}'); } }); _initialized = true; } } @override Widget build(BuildContext context) { return Container(); } }

Slide 172

Slide 172 text

% 168 利用側 class _PageState extends State { bool _initialized = false; @override void initState() { super.initState(); if(!_initialized) { appLifecycleObserver.subscribe('pageName', (event) { switch (event) { case LifecycleEventResumed(): print('onResume: ${event.isSystemEvent}'); case LifecycleEventPaused(): print('onPause: ${event.isSystemEvent}'); } }); _initialized = true; } } @override Widget build(BuildContext context) { return Container(); } } Context不要なのでinitStateでも呼べる

Slide 173

Slide 173 text

% おまけ 169

Slide 174

Slide 174 text

% 170 Riverpodで使うなら

Slide 175

Slide 175 text

% 171 @Riverpod(keepAlive: true) AppLifecycleObserver appLifecycleObserver( AppLifecycleObserverRef ref, ) => AppLifecycleObserver();

Slide 176

Slide 176 text

% 172 @riverpod Stream appLifecycleStream( AppLifecycleStreamRef ref, String key, ) async* { StreamController eventController = StreamController(); final provider = ref.watch(appLifecycleObserverProvider); provider.subscribe(key, (LifecycleEvent event) { eventController.add(event); }); ref.onDispose(() { provider.unsubscribe(key); }); yield* eventController.stream; }

Slide 177

Slide 177 text

% Stream appLifecycleStream( AppLifecycleStreamRef ref, String key, ) async* { StreamController eventController = StreamControll final provider = ref.watch(appLifecycleObserverProvider); provider.subscribe(key, (LifecycleEvent event) { eventController.add(event); }); ref.onDispose(() { provider.unsubscribe(key); }); yield* eventController.stream; } 173 appLifecycleObserverは内部で参照

Slide 178

Slide 178 text

% 174 Stream appLifecycleStream( AppLifecycleStreamRef ref, String key, ) async* { StreamController eventController = StreamControll final provider = ref.watch(appLifecycleObserverProvider); provider.subscribe(key, (LifecycleEvent event) { eventController.add(event); }); ref.onDispose(() { provider.unsubscribe(key); }); yield* eventController.stream; } 渡されたKey(Route name)でsubscribe

Slide 179

Slide 179 text

% 175 Stream appLifecycleStream( AppLifecycleStreamRef ref, String key, ) async* { StreamController eventController = StreamControll final provider = ref.watch(appLifecycleObserverProvider); provider.subscribe(key, (LifecycleEvent event) { eventController.add(event); }); ref.onDispose(() { provider.unsubscribe(key); }); yield* eventController.stream; } Disposeで自動unsubscribe

Slide 180

Slide 180 text

% 176 利用側 final event = ref.watch(appLifecycleStreamProvider('page1')); switch(event) { case AsyncData(:final value): print(value); }

Slide 181

Slide 181 text

% 177 goRouter.routerDelegate.currentConfiguration.routes; GoRouter内で管理しているNavigation historyを取得できる

Slide 182

Slide 182 text

% 178 @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didRemove(Route route, Route? previousRoute) { final top = goRouter.routerDelegate.currentConfiguration.routes.lastOrNull; if (top != null && (top as GoRoute).name == route.settings.name) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } } 自分で管理 GoRouterを参照

Slide 183

Slide 183 text

% 179 @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didRemove(Route route, Route? previousRoute) { final top = goRouter.routerDelegate.currentConfiguration.routes.lastOrNull; if (top != null && (top as GoRoute).name == route.settings.name) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } } 自分で管理 GoRouterを参照

Slide 184

Slide 184 text

% 180 @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didRemove(Route route, Route? previousRoute) { final top = goRouter.routerDelegate.currentConfiguration.routes.lastOrNull; if (top != null && (top as GoRoute).name == route.settings.name) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } } 自分で管理 GoRouterを参照 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 185

Slide 185 text

% 181 @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didRemove(Route route, Route? previousRoute) { final top = goRouter.routerDelegate.currentConfiguration.routes.lastOrNull; if (top != null && (top as GoRoute).name == route.settings.name) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } } 自分で管理 GoRouterを参照 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 186

Slide 186 text

% X @override void didRemove(Route route, Route? previousRoute) { if (_routeStack.last == route) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } _routeStack.remove(route); } @override void didRemove(Route route, Route? previousRoute) { final top = goRouter.routerDelegate.currentConfiguration.routes.lastOrNull; if (top != null && (top as GoRoute).name == route.settings.name) { _emitLifecycleEvent(route, const LifecycleEventPaused(false)); _emitLifecycleEvent(previousRoute, const LifecycleEventResumed(false)); } } 自分で管理 GoRouterを参照 Master of Flutter lifecycle Introduction Widget lifecycle App lifecycle Navigation & Routing Handling Flutter lifecycle Summary

Slide 187

Slide 187 text

% • Widget, App, Navigationそれぞれのライフサイクルを学んだ • Flutter3.13.0からイベントの発火タイミングが変わったので注意が必要 • FlutterはEverything is a widgetになっているため、 画面遷移のライフサイクルイベントはNavigationを考慮する必要がある • それぞれを組み合わせて初めてFlutterのライフサイクルにあった処理ができる • 今日のサンプルコード https://github.com/AAkira/ fl utter_lifecycle_example 182 Summary

Slide 188

Slide 188 text

Thank you! @_a_akira We are hiring! https://jobs.m3.com/engineer/