Slide 1

Slide 1 text

Daichi Furiya Flutter ΞϓϦΞʔΩςΫνϟྫ #gdg_yamanashi

Slide 2

Slide 2 text

About me Daichi Furiya (߱໼ େ஍) Google Developers Expert CyberAgent, Inc. @wasabeef_jp wasabeef    • ࢁསࢢग़਎ (ؠԼԹઘͷۙ͘)

Slide 3

Slide 3 text

Agenda • Flutter ͷݱࡏ஍ • ΞϓϦΞʔΩςΫνϟྫ

Slide 4

Slide 4 text

Flutter 2023 ೥ͷ Flutter/Dart Ξοϓσʔτ 1 ݄ 5 ݄ 8 ݄ 11 ݄ Flutter 3.7 Flutter 3.13 Flutter 3.10 Flutter 3.16 Dart 2.19 3.0 alpha Dart 3 Dart 3.2 Dart 3.1 Dart

Slide 5

Slide 5 text

Flutter 2024 ೥ͷॳΊͷΞοϓσʔτ 2 ݄ 5݄ 8 ݄ 11 ݄ Flutter 3.16 Dart Dart 3.3.0

Slide 6

Slide 6 text

Apps made with Flutter 500k+ apps created 2m+ developers 48k+ Flutter & Dart packages 1k+ Every day, new apps

Slide 7

Slide 7 text

Flutter apps in production - Japan

Slide 8

Slide 8 text

ΞʔΩςΫνϟ ΫϥΠΞϯτΞϓϦͷΞʔΩςΫνϟΛߟ͑Δ ্ͰҰ൪ॏཁʹߟ͍͑ͯΔ͜ͱ͸ঢ়ଶ؅ཧΛͲ ͏ѻ͏͔ͩͱࢥ͍ͬͯ·͢ɻ ※ جຊతʹΞʔΩςΫνϟ͸ɺੈͷதతʹ͸΋ͬ ͱ͍͍ͱݴΘΕΔ΋ͷ͕͋ͬͨͱͯ͠΋νʔϜ ͱͯ͠߹ҙ͕औΕ͍ͯΔঢ়ଶͰ͋Ε͹ਖ਼ղͰ͢ɻ

Slide 9

Slide 9 text

ΞʔΩςΫνϟ MVC Redux

Slide 10

Slide 10 text

Web ͷੈքΛࢀߟʹ͢Δ Flutter ͷੈքͰ͸ɺະͩʹσϑΝΫτελϯμʔυͷΑ͏ͳΞʔΩςΫνϟ͸ແ͍ͷͰྺ ࢙΋௕͍΢Σϒͷੈք͔Βߟ͑ํΛ༌ೖͯ͠ΈΔͷ͸Ͳ͏͔

Slide 11

Slide 11 text

TanStack Query/SWR import useSWR from 'swr' function Profile() { const { data, error, isLoading } = useSWR('/api/user', fetcher) if (error) return
failed to load if (isLoading) return
loading ... return
hello {data.name}! } SWR ͷྫ ӈͷΑ͏ʹ UI ίϯϙʔωϯτͰ௚઀ αʔόϦΫΤετΛʢΩϟογϡͷ֬ ೝʣͯ͠ UI ͱͯ͠දࣔ͢ΔྫͰ͢ɻ ͜ΕʹΑΓίϯϙʔωϯτͷϙʔλϏ ϦςΟੑΛ্͛ͯऔΓճ͠Λ͠΍͘͢ ͢ΔͨΊͷํ๏͕૿͑ͯདྷ͍ͯ·͢ɻ

Slide 12

Slide 12 text

ঢ়ଶ؅ཧ άϩʔόϧεςʔτ جຊతʹ͸αʔόΛάϩʔόϧ εςʔτͱͯ͠ଊ͍͑ͯΔͷͰ ϦΫΤετσʔλͷΩϟογϡ ͕΄ͱΜͲղܾͯ͘͠ΕΔ͸ͣ Ͱ͢ɻ ͨͩ͠ɺෳ਺ͷεΫϦʔϯͳͲͰ ࢖ΘΕΔΑ͏ͳೝূτʔΫϯͳͲ ʹ͸ Riverpod Ͱ؅ཧ͍ͯ͠·͢ɻ ϩʔΧϧεςʔτ εΫϦʔϯɺίϯϙʔωϯτ಺ Ͱ׬݁͢Δσʔλͷ؅ཧํ๏Ͱ ͢ɻΑ͋͘ΔྫͰ UI ʹදࣔ͢ ΔϩʔςΟϯάͷঢ়ଶͩͬͨ ΓɺϘλϯͷ༗ޮແޮͷ੾Γସ ͑༻ͩͬͨΓ͢Δ΋ͷ͸ Flutter Hooks Ͱ؅ཧ͍ͯ͠·͢ɻ αʔόϦΫΤετͱΩϟογϡ GraphQL Flutter Λར༻͍ͯ͠ ·͢ɻGraphQL Flutter ͸Ϩε ϙϯεσʔλͷΩϟογϡ΋͠ ͯ͘Ε·͢ɻ React Hooks ࢀߟ Recoil ࢀߟ TanStack/SWR ࢀߟ

Slide 13

Slide 13 text

ϩʔΧϧεςʔτ Flutter Hooks

Slide 14

Slide 14 text

ϩʔΧϧεςʔτ Flutter Hooks ྫͷΑ͏ʹ͜ͷ΢ΟδΣοτ಺Ͱ ׬݁͢ΔΑ͏ͳσʔλͷ৔߹͸ Flutter Hooks ͷ useState(...) Λར༻ ͯ͠؅ཧ͍ͯ͠·͢ɻ ͦΕҎ֎ʹ΋ useContext ΍ useCallback ͳͲ΋Α͘ར༻͍ͯ͠ ·͢ɻ @override Widget build(BuildContext context) { final isLoading = useState(false); // ... Կ͔ return Scaffold( body: Stack( children: [ const Text('Body'), if (isLoading.value) const Center( child: CircularProgressIndicator(), ), ], ), ); }

Slide 15

Slide 15 text

άϩʔόϧεςʔτ Riverpod

Slide 16

Slide 16 text

άϩʔόϧεςʔτ جຊతʹ͸αʔόΛάϩʔόϧεςʔ τͱͯ͠ଊ͍͑ͯΔͷͰϦΫΤετ σʔλͷΩϟογϡ͕΄ͱΜͲղܾ͠ ͯ͘ΕΔ͸ͣͰ͢ɻ ͨͩ͠ɺෳ਺ͷεΫϦʔϯͳͲͰ࢖ΘΕ ΔΑ͏ͳೝূτʔΫϯͳͲʹ͸ Riverpod Ͱ؅ཧ͍ͯ͠·͢ɻ part 'id_token_state.g.dart'; typedef IdToken = String; @Riverpod(keepAlive: true, dependencies: [firebaseAuth]) class IdTokenState extends _$IdTokenState { @override IdToken build() { return ''; } / / ॳظ஋͸ۭ Future fetch() async { final user = ref.watch(firebaseAuthProvider).currentUser; if (user == null) return; update(await user.getIdToken()); } void update(String token) { if (state != token) state = token; } } @override Widget build(BuildContext context) { final idToken = ref.watch(idTokenStateProvider); // ... Կ͔ }

Slide 17

Slide 17 text

αʔόϦΫΤετͱΩϟογϡ GraphQL

Slide 18

Slide 18 text

αʔόϦΫΤετͱΩϟογϡ αʔόϦΫΤετͱΩϟογϡ͸ GraphQL Flutter ͕୲ͬͯ΋Β͍ͬͯ· ͢ɻαʔόϦΫΤετʹඞཁͳ৘ใ ʢFirebase Auth ͷ Id Token ͳͲʣҎ֎ ͸جຊతʹάϩʔόϧεςʔτͱͯ͠͸ ࣋ͨͳ͍Α͏ʹ͍ͯ͠·͢ɻ ར༻ϥΠϒϥϦɿgraphql_flutterɺgraphql_codegen

Slide 19

Slide 19 text

GraphQL ͷઃܭʹ͍ͭͯ͸ Fragment Colocation Λجຊઃܭͱ͓ͯ͠ΓɺͦΕͧ ΕͷίϯϙʔωϯτͰඞཁͳσʔλ͸ Fragment ͱͯͦ͠ΕͧΕͰίϯϙʔωϯ τͱಉ༷ͷ৔ॴͰ .graphql ϑΝΠϧΛఆٛ ͍ͯ͠·͢ɻ Query ͸ը໘͔Βݺͼ·͢ɻ Home Screen User Component Feed Component query Home { user { ... UserParts } feed { ... FeedParts } } # Ϣʔβʔ৘ใ fragment UserParts on User { id } mutation CreateAccount { signUp { user { id } } } # ϑΟʔυ৘ใ fragment FeedParts on Feed { id title body }

Slide 20

Slide 20 text

GraphQL Flutter ʹ͸ Flutter Hooks Λར༻ ͨ͠ػೳΛఏڙ͍ͯ͠ΔͷͰɺΑΓ TanStack Query ͷΑ͏ͳ͜ͱΛΠϝʔδ Ͱ͖Δͱࢥ͍·͢ɻ final readRespositoriesResult = useQuery( QueryOptions( document: gql(readRepositories), variables: { 'nRepositories': 50 }, pollInterval: const Duration(seconds: 10), ), ); final result = readRespositoriesResult.result; if (result.hasException) { return Text(result.exception.toString()); } if (result.isLoading) { return const Text('Loading'); } List? repositories = result.data?['viewer']?['repositories']?['nodes']; if (repositories == null) { return const Text('No repositories'); } return ListView.builder( itemCount: repositories.length, itemBuilder: (context, index) { final repository = repositories[index]; return Text(repository['name'] ?? ''); });

Slide 21

Slide 21 text

σΟϨΫτϦߏ੒ શମͷσΟϨΫτϦߏ଄͸͜͏͍͏ܗͰ ఆ͍ٛͯ͠·͢ɻ࡞ΔʹͭΕͯۤ͘͠ͳͬ ͍ͯ͘Օॴ͸ਵ࣌ߟ͑௚͍ͯ͠·͢ɻ lib/ ├── data/ │ ├── network/ │ └── system/ ├── foundation/ │ ├── extension/ # ֦ுؔ਺ │ └── firebase/ │ ├── auth/ # Firebase Auth ؔ࿈ │ └── messaging/ # Firebase Messaging ؔ࿈ ├── gen/ # FlutterGen ͳͲҰ෦ͷࣗಈੜ੒ϑΝΠϧ ├── l10n/ # Localization ؔ࿈ ├── route/ # auto_route ؔ࿈ ├── state/ # άϩʔόϧͷঢ়ଶ؅ཧؔ࿈ ├── ui/ # UI ؔ࿈ͷϞδϡʔϧʢGraphQL ͸֤ը໘Ͱఆٛʣ │ ├── screen/ │ │ └── home/ │ │ ├── home_screen.dart # ը໘ │ │ ├── home_screen_e2e.yaml # Maestro ͷϑΝΠϧ │ │ ├── home_screen_vrt.dart # Visual Regression Test ͷϑΝΠϧ │ │ ├── hook/ # ը໘ݻ༗ͷ Hooks │ │ │ └── use_update_home.dart │ │ ├── component/ # ը໘ݻ༗ͷίϯϙʔωϯτ │ │ │ ├── home_card.dart │ │ │ └── home_card_fragment.dart # ίϯϙʔωϯτͷ GraphQL ϑΝΠϧ │ │ └── top │ │ ├── home_top_screen_e2e.yaml │ │ └── home_top_screen.dart │ ├── theme/ # άϩʔόϧςʔϚઃఆ │ │ ├── app_theme.dart │ │ └── app_text.dart │ ├── hook/ # ൚༻తͳ Hooks │ │ ├── use_debounce.dart │ │ ├── use_debounce_test.dart │ │ ├── use_sign_in.dart │ │ ├── use_sign_in_test.dart │ │ ├── use_sign_out.dart │ │ └── use_sign_out_test.dart │ └── component/ # ൚༻ UI ίϯϙʔωϯτ │ ├── fab/ │ └── text/ └── use_case/

Slide 22

Slide 22 text

Thank you ɹɹ wasabeef_jp ɹɹ wasabeef