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

Flutter Hooks を使ったアプリ開発 / App Development with the Flutter Hooks

Flutter Hooks を使ったアプリ開発 / App Development with the Flutter Hooks

CyberAgent Developer Conference 2022

Daichi Furiya (Wasabeef)

March 23, 2022
Tweet

More Decks by Daichi Furiya (Wasabeef)

Other Decks in Programming

Transcript

  1. class CountPage extends StatefulWidget { @override State createState() => _CountState();

    } class _CountState extends State<CountPage> { int count = 0; @override void initState() { / ** ॳظԽ **/ } @override void dispose() { /** ഁغ **/ } void setCount(int value) => setState(() = > count = value); @override Widget build(BuildContext context) { return TextButton( child: Text("Count is $count"), onPressed: () => setCount(count + 1), ); } } w γεςϜͷঢ়ଶͳͲʹΑͬͯಈతʹ มԽΛ͢Δখ͞ͳϥΠϑαΠΫϧΛ ΋ͬͨ΢ΟδΣοτ w *NBHFɺ5FYU'PSN'JFME΍)FSPͳ Ͳ͕4UBUFGVM8JEHFUͷαϒΫϥε 4UBUFGVM8JEHFUͷઆ໌
  2. class CountPage extends StatefulWidget { @override State createState() => _CountState();

    } class _CountState extends State<CountPage> { int count = 0; @override void initState() { / ** ॳظԽ **/ } @override void dispose() { /** ഁغ **/ } void setCount(int value) => setState(() = > count = value); @override Widget build(BuildContext context) { return TextButton( child: Text("Count is $count"), onPressed: () => setCount(count + 1), ); } } 4UBUFGVM8JEHFUΛ࢖͏ʹ͸جຊతʹೋͭͷ ΫϥεΛ࡞Δඞཁ͕͋Δ w 4UBUFGVM8JEHFUΛܧঝͨ͠$PVOU1BHF w 4UBUF5Λܧঝͨ͠@$PVOU4UBUF 4UBUFGVM8JEHFUͷઆ໌
  3. class CountPage extends StatefulWidget { @override State createState() => _CountState();

    } class _CountState extends State<CountPage> { int count = 0; @override void initState() { / ** ॳظԽ **/ } @override void dispose() { /** ഁغ **/ } void setCount(int value) => setState(() = > count = value); @override Widget build(BuildContext context) { return TextButton( child: Text("Count is $count"), onPressed: () => setCount(count + 1), ); } } 4UBUFGVM8JEHFUͷઆ໌ 4UBUFGVM8JEHFUΛ࢖͏ʹ͸جຊతʹೋͭͷ ΫϥεΛ࡞Δඞཁ͕͋Δ w 4UBUFGVM8JEHFUΛܧঝͨ͠$PVOU1BHF w 4UBUF5Λܧঝͨ͠@$PVOU4UBUF
  4. class CountPage extends StatefulWidget { @override State createState() => _CountState();

    } class _CountState extends State<CountPage> { int count = 0; @override void initState() { / ** ॳظԽ **/ } @override void dispose() { /** ഁغ **/ } void setCount(int value) => setState(() = > count = value); @override Widget build(BuildContext context) { return TextButton( child: Text("Count is $count"), onPressed: () => setCount(count + 1), ); } } 4UBUFGVM8JEHFUͷઆ໌ JOJU4UBUFɿॳظԽͷॲཧ EJTQPTFɿഁغͷॲཧ TFU4UBUFɿݺͿ͜ͱͰঢ়ଶ͕มԽͨ͜͠ͱ Λ௨஌ͯ͠ϦϏϧυ͕૸Δ
  5. 'MVUUFS)PPLTͰԿ͕ղܾͰ͖Δ͔ ग़యɿIUUQTHJUIVCDPNSSPVTTFM(JU fl VUUFS@IPPLT ग़యɿIUUQTSFBDUKTPSHEPDTIPPLTJOUSPIUNM w એݴత6*ͰΫϥΠΞϯτΛ࣮૷্͍ͯ͘͠ͰɺϩδοΫͱ ΢ΟδΣοτʢίϯϙʔωϯτʣͷ෼཭ΛϝϯςφϯεੑΛอ ͪͭͭҡ࣋͢Δͷ͕೉͘͠ɺͦΕΛղܾ͢ΔػೳΛఏڙ͢Δ w

    ίϯϙʔωϯτΛͪΌΜͱؔ਺ͱͯ͠ѻ͏ͨΊ w ΫϥεͷఆٛΛݮΒ͢ʢγϯλοΫεγϡΨʔʣͨΊ w 'MVUUFSͷ΢ΟδΣοτ͸සൟʹ࠶ඳը͕࣮ߦ͞ΕΔͨΊॏ͍ ԋࢉ΍ෳࡶͳॲཧΛ͚͞ΕΔΑ͏ͳػೳΛఏڙ͢Δ w ಉ͡ೖྗσʔλͷ৔߹ʹ͸݁ՌΛΩϟογϡ͢Δ
  6. VTF4UBUFΛཧղ͢Δ class CountPage extends HookWidget { @override Widget build(BuildContext context)

    { final count = useState(0); return TextButton( onPressed: () { count.value ++ ; }, child: Text("Count is ${count.value}"), ); } } class CountPage extends StatefulWidget { @override State createState() => _CountState(); } class _CountState extends State<CountPage> { int count = 0; void setCount(int value) => setState(() = > count = value); @override Widget build(BuildContext context) { return TextButton( child: Text("Count is $count"), onPressed: () => setCount(count + 1), ); } } 4UBUFGVM8JEHFU VTF4UBUF େ͖ͳҧ͍͸4UBUFΛܧঝͨ͠Ϋϥε͕ඞ ཁͩͬͨ΋ͷ͕ͳ͘ͳΓVTF4UBUF 5 ʹॳ ظ஋Λ༩͑Δ͚ͩʹͳ͍ͬͯΔ
  7. VTF4UBUFΛཧղ͢Δ class CountPage extends HookWidget { @override Widget build(BuildContext context)

    { final count = useState(0); return TextButton( onPressed: () { count.value + + ; }, child: Text("Count is ${count.value}"), ); } }
  8. VTF& ff FDUΛཧղ͢Δ ؆୯ͳ΢ΟδΣοτͷϥΠϑαΠΫϧͰॲཧ͕ඞཁͩͬͨ ৔߹ʹ͸VTF&GGFDUΛ࢖͏ 
 ʢ΢ΟδΣοτੜ੒࣌ʹॳظԽ΍ഁغΛ͍ͨ͠৔߹ͳͲʣ class _CountState extends

    State<CountPage> { ɹ // . .. @override void initState() { / ** ॳظԽ **/ } @override void dispose() { /** ഁغ **/ } } ɹ // . .. } 4UBUFGVM8JEHFUͷJOJU4UBUFEJQPTF૬౰
  9. VTF& ff FDUΛཧղ͢Δ class CountPage extends HookWidget { @override Widget

    build(BuildContext context) { useEffect(() => { / / ॳظԽ final subscription = source.subscribe(id); return () => { // ഁغ subscription.unsubscribe(id); }; }, [id]); return // Widget . .. } } 8JEHFUͷCVJME಺ͰVTF&GGFDUͷୈҰҾ ਺ʹΫϩʔδϟΛ౉͠ɺͦͷΫϩʔδϟͷ SFUVSO࣌ʹഁغ࣌ͷॲཧΛॻ͘ VTF&GGFDUͷୈೋҾ਺ʹ͸ॳظԽΛϏϧυ࣌ ʹຖճ࣮ߦ͞Εͳ͍Α͏ʹΩʔͱͳΔΦϒ δΣΫτΛ౉͢ඞཁ͕͋Δ
  10. VTF& ff FDUͷୈೋҾ਺͸ॏཁ class CountPage extends HookWidget { @override Widget

    build(BuildContext context) { useEffect(() => { / / ᶃ ID ͕มߋ͞ΕΔͨͼʹݺͼग़͠ }, [id]); useEffect(() => { / / ᶄ ຖճݺͼग़͠ }); useEffect(() => { / / ᶅ ࠷ॳͷҰճ͚ͩݺͼग़͠ }, const []); return // Widget . .. } }
  11. VTF"OJNBUJPO$POUSPMMFSΛར༻ͯ͠ΈΔ class Example extends StatefulWidget { final Duration duration; const

    Example({Key? key, required this.duration}) : super(key: key); @override _ExampleState createState() => _ExampleState(); } class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller!.duration = widget.duration; } } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } 4UBUFGVM8JEHFU 'MVUUFSͰΞχϝʔγϣϯΛ4UBUFGVM8JEHFU Ͱ࣮૷͠Α͏ͱ͢Δͱঢ়ଶ؅ཧͷίʔυΛΫ ϥεʹ͚ͩͰ͜Ε͚ͩඞཁʹͳΓͦͷଟ͕͘ ࢖͍ճ͠Ͱ͖ͳ͍ίʔυʹͳΔ ˞৑௕ͳίʔυΛݮΒ͢ͱ͍͏໨త͚ͩͰ͋ Ε͹%BSUʹ΋.JYJO͕͋ΔͷͰͦΕͰڞ௨ Խ͢Δ͜ͱ΋Ͱ͖Δ
  12. VTF"OJNBUJPO$POUSPMMFSΛར༻ͯ͠ΈΔ class Example extends HookWidget { const Example({required this.duration}); final

    Duration duration; @override Widget build(BuildContext context) { final controller = useAnimationController(duration: duration); return Container(); } } class Example extends StatefulWidget { final Duration duration; const Example({Key? key, required this.duration}) : super(key: key); @override _ExampleState createState() => _ExampleState(); } class _ExampleState extends State<Example> with SingleTickerProviderStateMixin { AnimationController? _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: widget.duration); } @override void didUpdateWidget(Example oldWidget) { super.didUpdateWidget(oldWidget); if (widget.duration != oldWidget.duration) { _controller!.duration = widget.duration; } } @override void dispose() { _controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); } } 4UBUFGVM8JEHFU VTF"OJNBUJPO$POUSPMMFS VTF"OJNBUJPO$POUSPMMFSΛ࢖͏͜ͱͰ৑௕ ͳίʔυྔ͸ݮΒ͢͜ͱ͕Ͱ͖ɺར༻ଆͰͷ ίετΛԼ͛Δ͜ͱ͕Ͱ͖Δ
  13. ΧελϜϑοΫΛ࡞੒͢Δ w ؔ਺໊ʹ͸VTF999ͱ͍͏໊લΛ͚ͭΔ w ࠷ॳ͸VTF4UBUF΍VTF& ff FDUΛ૊Έ߹Θ ࣮ͤͯ૷ͯ͠ΈΔ VoidCallback useUpdate()

    { final attempt = useState(0); return () => attempt.value ++ ; } class Sample extends HookWidget { @override Widget build(BuildContext context) { final update = useUpdate(); return Column( children: [ ElevatedButton( onPressed: () => update(), child: const Text("Update"), ), ] ); } } VTF6QEBUF 4BNQMF
  14. VTF& ff FDU0ODFΛར༻ͯ͠ΈΔ w fl VUUFS@VTFʹ͓͍ͯ࠷΋୯७ͳ࣮૷ w VTF& ff FDUͷୈೋҾ਺Λলུ͢ΔͨΊͷΧελϜϑοΫ

    useEffect(() => { // ࠷ॳͷҰճ͚ͩݺͼग़͠ }, const []); void useEffectOnce(Dispose? Function() effect) { return useEffect(effect, const []); } VTF&GGFDU VTF&GGFDU0ODFͷ࣮૷
  15. VTF& ff FDU0ODFΛར༻ͯ͠ΈΔ w fl VUUFS@VTFʹ͓͍ͯ࠷΋୯७ͳ࣮૷ w VTF& ff FDUͷୈೋҾ਺Λলུ͢ΔͨΊͷΧελϜϑοΫ

    useEffect(() => { // ࠷ॳͷҰճ͚ͩݺͼग़͠ }, const []); void useEffectOnce(Dispose? Function() effect) { return useEffect(effect, const []); } VTF&GGFDU VTF&GGFDU0ODFͷ࣮૷
  16. w ΞϓϦཁ݅ͰԿඵޙʹॲཧΛ࣮ߦ͍ͤͨ͞৔߹ w VTF5JNFPVUVTF5JNFPVU'OΛ࢖͏͜ͱͰࢦఆͷ࣌ؒܦա ޙʹϦϏϧυ΍ΫϩʔδϟΛ࣮ߦ͢Δ͜ͱ͕Ͱ͖Δ class Sample extends HookWidget {

    @override Widget build(BuildContext context) { final timeout = useTimeout(const Duration(seconds: 3)); return Column( children: [ Text("isReady ?: ${timeout.isReady}"), ElevatedButton( onPressed: () => timeout.reset(), child: const Text("Reset"), ), ], ); } } VTF5JNFPVUΛར༻ͯ͠ΈΔ
  17. w ಺෦Ͱ͸DPOOFDUJWJUZ@QMVTͱ͍͏ϥΠϒϥϦΛ࢖ͬͯ୺ ຤ͷωοτϫʔΫঢ়گΛऔಘ͍ͯ͠Δ w ࣗ෼Ͱ࣮૷͢Δͱඇಉظॲཧͷ੍ޚͳͲ͕ඞཁ͕ͩɺ͜ ͷΧελϜϑοΫͰ͸ར༻ଆ͸γϯϓϧʹ͍ͯ͠Δ class Sample extends HookWidget

    { @override Widget build(BuildContext context) { final networkState = useNetworkState(); return Column( children: [ Text( "Network: ${networkState.connectivityResult}"), ], ); } } VTF/FUXPSL4UBUFΛར༻ͯ͠ΈΔ
  18. ϥΠϒϥϦΛར༻͠ͳ͍৔߹ testWidgets('usePrevious', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) {

    usePrevious(42); return const SizedBox(); }), ); await tester.pumpWidget( HookBuilder(builder: (context) { usePrevious(21); return const SizedBox(); }), ); final element = tester.element(find.byType(HookBuilder)); expect( element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ usePrevious: 42\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); VTF1SFWJPVT͸'MVUUFS)PPLTʹඪ ४Ͱؚ·Ε͍ͯΔ༩͑ͨ஋ͷҰͭલͷ ঢ়ଶΛอ࣋͢ΔΧελϜϑοΫ ͜ͷϑοΫΛςετॻ͘৔߹ʹ͸࠷௿ Ͱ΋͜Ε͚ͩͷίʔυྔͰ͢
  19. testWidgets('usePrevious', (tester) async { / / ... await tester.pumpWidget( HookBuilder(builder:

    (context) { usePrevious(42); return const SizedBox(); }), ); / / ... }); ϥΠϒϥϦΛར༻͠ͳ͍৔߹ গ͠ࡉ͔͘આ໌͍ͯ͘͠ͱɺ QVNQ8JEHFUͱ͍͏ςετ༻΢Ο δΣοτͷঢ়ଶΛ؅ཧ͍ͯ͠Δؔ਺ʹ )PPL#VJMEFSΛ౉ͯ͠ɺͦͷதͰΧε λϜϑοΫΛ࢖͏ඞཁ͕͋Δ
  20. testWidgets('usePrevious', (tester) async { / / ... await tester.pumpWidget( HookBuilder(builder:

    (context) { usePrevious(21); return const SizedBox(); }), ); / / ... }); ϥΠϒϥϦΛར༻͠ͳ͍৔߹ ΋͏Ұ౓ߋ৽͠ͳ͍ͱ͍͚ͳ͍ͷͰɺ ઌ΄Ͳ͸͕ͩͬͨࠓճ͸Λ౉ ͍ͯ͠Δ
  21. testWidgets('usePrevious', (tester) async { // .. . final element =

    tester.element(find.byType(HookBuilder)); expect( element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ usePrevious: 42\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); // .. . }); VTF1SFWJPVTͱ͍͏Ұͭલͷ஋Λอ࣋ ͢ΔͷͰ࠷ॳʹ౉͕ͨ͠ݱࡏอ࣋ ͍ͯ͠Δ஋ͱͳΔ ͜ΕΛνΣοΫ͢ΔͨΊʹςετ΢Ο δΣοτͷϊʔυΛจࣈྻ͔Βϋο γϡίʔυͰൺֱ͍ͯ͠Δ ϥΠϒϥϦΛར༻͠ͳ͍৔߹
  22. testWidgets('usePrevious', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { usePrevious(42);

    return const SizedBox(); }), ); await tester.pumpWidget( HookBuilder(builder: (context) { usePrevious(21); return const SizedBox(); }), ); final element = tester.element(find.byType(HookBuilder)); expect( element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ usePrevious: 42\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); ϥΠϒϥϦΛར༻͠ͳ͍৔߹
  23. testWidgets('usePrevious', (tester) async { final result = await buildHook( (value)

    = > usePrevious(value), initialProps: 42, ); await result.rebuild(21); expect(result.current, 42); }); testWidgets('usePrevious', (tester) async { await tester.pumpWidget( HookBuilder(builder: (context) { usePrevious(42); return const SizedBox(); }), ); await tester.pumpWidget( HookBuilder(builder: (context) { usePrevious(21); return const SizedBox(); }), ); final element = tester.element(find.byType(HookBuilder)); expect( element .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) .toStringDeep(), equalsIgnoringHashCodes( 'HookBuilder\n' ' │ usePrevious: 42\n' ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', ), ); }); ར༻͠ͳ͍ ར༻͢Δ ΧελϜϑοΫͷঢ়ଶΛߋ৽͢ΔͨΊʹ UFTUFSQVNQ8JEHFUʹ)PPL#VJMEFSΛຖ ճ౉͍ͯͨ͠΋ͷΛলུͰ͖ɺFYQFDU࣌ͷ ঢ়ଶऔಘ΋γϯϓϧʹॻ͚Δ ϥΠϒϥϦΛར༻͢Δ৔߹
  24. testWidgets('usePrevious', (tester) async { final result = await buildHook( (value)

    = > usePrevious(value), initialProps: 42, ); await result.rebuild(21); expect(result.current, 42); }); 'MVUUFS)PPLT5FTUJOH-JCSBSZΛར༻ͨ͠৔ ߹͸ଟ͘ͷ৑௕ίʔυΛ࡟ݮ͢Δ͜ͱ͕Ͱ͖Δ CVJME)PPLɿͷΫϩʔδϟʹΧελϜϑοΫͷ ݺͼग़͠Λॻ͖ɺୈೋҾ਺ʹॳظ஋Λ༩͑Δ͜ ͱ͕Ͱ͖Δ SFCVJMEɿCVJME)PPLͰੜ੒͞ΕͨΫϥεͰ஋ Λߋ৽͠ϦϏϧυͰ͖Δ DVSSFOUɿอ͍࣋ͯ͠Δ஋ΛऔಘͰ͖Δ ϥΠϒϥϦΛར༻͢Δ৔߹
  25. final variable1 = callSomething(); final variable2 = callSomething(); // ෆඞཁ·ͨ͸ෆ଍͍ͯ͠ΔΩʔ

    useEffect(() { print(variable1); }, [variable2]); // ϑοΫΛ৚݅෼ذͷதʹೖΕͳ͍ if (flag) { final variable = useState('hello'); } ϑοΫʹ͸͍͔ͭ͘ͷϧʔϧ͕͋Γ·͢ ྫ w ΧελϜϑοΫͷؔ਺໊ʹ͸VTF999 ͱ͍͏໊લΛ͚ͭΔ ɾϑοΫΛ৚݅෼ذʹೖΕͳ͍ ͜ͷ1MVHJOΛ࢖͏͜ͱͰόάΛ๷͙ͱڞ ʹϨϏϡʔͷίετͳͲͷԼ͛Δ fl VUUFS@IPPLT@MJOU@QMVHJOΛར༻ͯ͠ΈΔ