Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
flutterkaigi_2024.pdf
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
Kyohei Ito
November 21, 2024
Programming
2k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
flutterkaigi_2024.pdf
Kyohei Ito
November 21, 2024
More Decks by Kyohei Ito
See All by Kyohei Ito
flutter_kaigi_2025.pdf
kyoheig3
2
1.1k
layerx_20241129.pdf
kyoheig3
2
540
flutter_kaigi_2021.pdf
kyoheig3
0
1.2k
flutter_kmm_1.pdf
kyoheig3
1
1.2k
ca.swift_10.pdf
kyoheig3
0
720
iosdc_2018.pdf
kyoheig3
2
3.2k
orecon_vol1.pdf
kyoheig3
4
1.8k
iosdc_2017.pdf
kyoheig3
4
950
ca.swift_2.pdf
kyoheig3
9
1.4k
Other Decks in Programming
See All in Programming
コードレビューのボトルネックを"する側"と"される側"の両面から解消する
yub0n
2
1.3k
C#の現在地 進化の歴史と、AI時代の.NET Everywhere
neuecc
5
4.8k
ハーネス設計入門 〜 基礎知識の整理から実務へのステップアップ 〜
kinopeee
19
20k
AHC070解法紹介
eijirou
0
150
AI Agent時代のリアーキテクチャ戦略と実践
hokaccha
9
5.3k
The Rails Doctrine Decade
koic
2
450
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
280
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
430
世界の中心で、AI(App Intents)をさけぶ ー App Intents中心設計の実践ガイド
touyou
0
750
iOSDC2026登壇資料.pdf
riofujimon
0
200
Androidだけじゃない、DroidKaigiで広がった私の技術スタック
akkeylab
0
110
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.2k
Featured
See All Featured
Docker and Python
trallard
47
4.2k
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
550
Bash Introduction
62gerente
615
220k
エンジニアに許された特別な時間の終わり
watany
109
250k
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
350
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
2
290
Context Engineering - Making Every Token Count
addyosmani
9
1.2k
[RailsConf 2023] Rails as a piece of cake
palkan
59
7k
Build your cross-platform service in a week with App Engine
jlugia
234
19k
Are puppies a ranking factor?
jonoalderson
2
4k
Thoughts on Productivity
jonyablonski
76
5.4k
Transcript
࣮ફతύοέʔδઓུ FlutterKaigi 2024
About Me ҏ౻ ګฏ גࣜձࣾαΠόʔΤʔδΣϯτ Github: KyoheiG3 X: KyoheiG3
ΞδΣϯμ •֓ཁ •ύοέʔδׂͷ࣮ྫ •ύοέʔδͷׂ •ςετʹ͍ͭͯ
ΞδΣϯμ •֓ཁ •ύοέʔδׂͷ࣮ྫ •ύοέʔδͷׂ •ςετʹ͍ͭͯ
ύοέʔδׂͷύλʔϯྫ • ΞϓϦέʔγϣϯͱ cli ͳͲͷπʔϧ • ௨৴෦ͳͲͷ෦୯Ґ • ෳͷΞϓϦέʔγϣϯͱڞ௨ػೳ •
ػೳ୯Ґ • ϨΠϠʔ • ͚ͳ͍
ΞϓϦΛී௨ʹ։ൃ͢Δͱ ͜ͷܗʹͳΔ App ͚ͳ͍
෦୯ҐͰ͚Δ Γग़͞Εͨ෦ͷ୯ମς ετ͕Γ͘͢ͳΔ Network Location DB App
ڞ௨ػೳͷΓग़͠ Core ػೳΛڞ༗͢ΔΞϓϦ ͕૿͑Δͱෆཁͳػೳ͕૿ ͑Δ Core App
ػೳ୯ҐͰ͚Δ ଞͷػೳͷӨڹΛ͋·Γ ߟ͑ͳ͍͍ͯ͘໘ɺػೳ ಉ࢜ͷ࿈ܞ͕͍͠ App Core Noti fi cation Purchase
Login
ϨΠϠʔͰ͚Δ ֎ଆͷϨΠϠʔଆͷϨ ΠϠʔʹґଘ͍ͯ͠Δ Presentation App Application Infra Domain
ΫϦʔϯΞʔΩςΫνϟ ը૾ग़లɿThe Clean Code Blog by Robert C. Martin (Uncle
Bob) | The Clean Architecture
ΫϦʔϯΞʔΩςΫνϟ ґଘੑٯస App UI Driver Presenter Gateway Entity UseCase
• Ͳ͜ͰԿΛॲཧ͢Δ͔͕໌֬ʹͳ࣮ͬͯʹڧ੍ྗ͕ੜ·ΕΔ • DI ͕ඞਢͳͷͰࣗͣͱϢχοτςετͷΈ͕͏ • ςετͷࡍʹෆཁͳύοέʔδΛഉআͰ͖Δ • ύοέʔδͷॲཧΛฒྻʹߦ͑ΔΑ͏ʹͳΔ ׂ࣌ͷϝϦοτ
ׂ࣌ͷσϝϦοτ • ׳ΕΔ·Ͱࠞཚ͕ͪ͠ • ΠϯλʔϑΣʔεͳͲͷهड़͕૿͑Δ • ίʔυδϟϯϓ͕໘ʹͳΔ
ΞδΣϯμ •֓ཁ •ύοέʔδׂͷ࣮ྫ •ύοέʔδͷׂ •ςετʹ͍ͭͯ
None
riverpod • ΞϓϦશମͷঢ়ଶཧ • ґଘͷೖ͕༰қʢDIʣ • ϥϯλΠϜͰͳͯ͘ίϯύΠϧλΠϜͰܕͷΤϥʔ͕͔Δʢܕ҆શʣ
Ϣʔβ໊Λදࣔ͢Δ͚ͩͷ ΞϓϦΛ࣮ͯ͠ΈΔ • Repository ܦ༝Ͱσʔλ औಘ • UseCase ͰσʔλΛՃ •
UI ʹදࣔ
App ύοέʔδͳͲಛʹߟ͑ͣʹ࡞Δ
app/lib/src/repository/user.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.g.dart'; @riverpod UserRepository userRepository(UserRepositoryRef ref) =>
UserRepository(ref); class UserRepository { const UserRepository(this.ref); final UserRepositoryRef ref; Future<User?> getUser() async { final response = await ref.read(dioProvider).user.fetch(); return User(response.id, response.name); } }
app/lib/src/state/user.dart import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../repository/user.dart'; part 'user.g.dart'; @riverpod class UserStateNotifier
extends _$UserStateNotifier { @override Future<UserState> build() async { final user = await ref.watch(userRepositoryProvider).getUser(); return UserState( user: user, loggedIn: user != null, ); } }
app/lib/src/widget/user_name.dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../state/user.dart'; class UserName extends
ConsumerWidget { const UserName({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final userState = ref.watch(userStateNotifierProvider); return userState.when( data: (state) { return Text('User name: ${state.user?.name}'); }, error: (error, _) { return Text('Error: $error'); }, loading: () { return const CircularProgressIndicator(); }, ); } }
ΞϓϦ࣮ߦ UserRepository ͕ฦͨ͠ Ϣʔβ໊͕දࣔ͞ΕΔ
App App ϨΠϠʔ͝ͱʹύοέʔδΛ͚Δ
ϨΠϠʔͷྫ • ΠϯϑϥϨΠϠʔ • DB API ʹΞΫηεͨ͠Γ͢Δ • Repository
ͳͲ • ΞϓϦέʔγϣϯϨΠϠʔ • ϏδωεϩδοΫ͕ू͞ΕΔ • UseCase ͳͲ
ΫϦʔϯΞʔΩςΫνϟ ͜ͷ͋ͨΓΛࢀߟʹ ύοέʔδΛ໋໊
ύοέʔδͷྫ • ΠϯϑϥϨΠϠʔ • Gateway ύοέʔδΛ࡞ • UserRepository ΛҠಈ •
ΞϓϦέʔγϣϯϨΠϠʔ • UseCase ύοέʔδΛ࡞ • UserStateNoti fi er ΛҠಈ
Gateway ύοέʔδͷ࡞ $ flutter create -t package gateway $ mkdir
-p gateway/lib/src/repository $ mv app/lib/src/repository/user.dart gateway/lib/src/repository/user.dart
Gateway ύοέʔδͷ࡞ name: gateway description: Gateway for infrastructure Layer environment:
sdk: '>=3.3.0 <4.0.0' dependencies: riverpod_annotation: dev_dependencies: build_runner: riverpod_generator:
Gateway ύοέʔδͷ࡞ export 'src/repository/user.dart';
UseCase ύοέʔδͷ࡞ $ flutter create -t package usecase $ mkdir
-p usecase/lib/src/state $ mv app/lib/src/state/user.dart usecase/lib/src/state/user.dart
UseCase ύοέʔδͷ࡞ name: usecase description: UseCase for domain Layer environment:
sdk: '>=3.3.0 <4.0.0' dependencies: riverpod_annotation: gateway: path: ../gateway dev_dependencies: build_runner: riverpod_generator: HBUFXBZύοέʔδΛ ґଘͱͯ͠Ճ͢Δ
UseCase ύοέʔδͷ࡞ import 'package:riverpod_annotation/riverpod_annotation.dart'; // import '../repository/user.dart'; import 'package:gateway/gateway.dart'; part
'user.g.dart'; @riverpod class UserStateNotifier extends _$UserStateNotifier { @override FutureOr<UserState> build() async { final user = await ref.watch(userRepositoryProvider).getUser(); return UserState( user: user, loggedIn: user != null, ); } } HBUFXBZύοέʔδΛ JNQPSU͢Δ HBUFXBZύοέʔδͷ 3FQPTJUPSZΛࢀরͰ͖Δ
UseCase ύοέʔδͷ࡞ export 'src/state/user.dart';
ΞϓϦຊମ name: app description: Application environment: sdk: '>=3.3.0 <4.0.0' flutter:
'>=3.19.0' dependencies: flutter: sdk: flutter flutter_riverpod: usecase: path: ../usecase dev_dependencies: flutter_test: sdk: flutter 6TF$BTFύοέʔδΛ ґଘͱͯ͠Ճ͢Δ
ΞϓϦຊମ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // import '../state/user.dart'; import 'package:usecase/usecase.dart';
class UserName extends ConsumerWidget { const UserName({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final userState = ref.watch(userStateNotifierProvider); return userState.when( data: (state) { return Text('User name: ${state.user?.name}'); }, error: (error, _) { return Text('Error: $error'); }, loading: () { return const CircularProgressIndicator(); }, ); } VTFDBTFύοέʔδΛ JNQPSU͢Δ VTFDBTFύοέʔδͷ 4UBUFΛࢀরͰ͖Δ
App Gateway UseCase UseCase Gateway App ґଘ͚ͩΛٯసͤ͞Δ
UseCase ύοέʔδͷґଘ name: usecase description: UseCase for domain Layer environment:
sdk: '>=3.3.0 <4.0.0' dependencies: riverpod_annotation: # gateway: # path: ../gateway dev_dependencies: build_runner: riverpod_generator: HBUFXBZύοέʔδΛ ґଘ͔Βআ͢Δ
UseCase ύοέʔδͷґଘ import 'package:riverpod_annotation/riverpod_annotation.dart'; // import '../repository/user.dart'; import 'package:gateway/gateway.dart'; part
'user.g.dart'; @riverpod class UserStateNotifier extends _$UserStateNotifier { @override FutureOr<UserState> build() async { final user = await ref.watch(userRepositoryProvider).getUser(); return UserState( user: user, loggedIn: user != null, ); } } HBUFXBZύοέʔδ͕ JNQPSUͰ͖ͳ͘ͳΔ HBUFXBZύοέʔδͷ 3FQPTJUPSZΛࢀরͰ͖ͳ͍
UseCase ύοέʔδͷґଘ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.g.dart'; @riverpod UserRepository userRepository(_) =>
throw UnimplementedError(); abstract interface class UserRepository { Future<User?> getUser(); } ͻͱ·ͣ&SSPSΛUISPX ͓ͯ͘͠
UseCase ύοέʔδͷґଘ import 'package:riverpod_annotation/riverpod_annotation.dart'; // import 'package:gateway/gateway.dart'; import '../repository/user.dart'; part
'user.g.dart'; @riverpod class UserStateNotifier extends _$UserStateNotifier { @override FutureOr<UserState> build() async { final user = await ref.watch(userRepositoryProvider).getUser(); return UserState( user: user, loggedIn: user != null, ); } } VTFDBTFͷϑΝΠϧΛ JNQPSU͢Δ VTFDBTFύοέʔδͷ 3FQPTJUPSZΛࢀরͰ͖Δ
ΞϓϦ࣮ߦ UseCase ͷ UserRepository ͷΠϯλʔ ϑΣʔε͕ throw ͨ͠Τ ϥʔ͕දࣔ͞ΕΔ
ΞϓϦ࣮ߦ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user.g.dart'; @riverpod UserRepository userRepository(_) => throw
UnimplementedError(); abstract interface class UserRepository { Future<User?> getUser(); } વ͍͕ͭ͜දࣔ͞ΕΔʂ
riverpod ʹΑΔґଘղܾ • overrideWith ʢಈతɺ ref ͋Γʣ • overrideWithValue ʢ੩తɺ
ref ͳ͠ʣ • ProviderScope Ͱ্ॻ͖ͷઃఆ
Gateway ύοέʔδͷೖ name: gateway description: Gateway for infrastructure Layer environment:
sdk: '>=3.3.0 <4.0.0' dependencies: riverpod_annotation: usecase: path: ../usecase dev_dependencies: build_runner: riverpod_generator: VTFDBTFύοέʔδΛ ґଘʹՃ͢Δ
Gateway ύοέʔδͷೖ import 'package:usecase/usecase.dart'; final userRepositoryOverride = userRepositoryProvider.overrideWith(UserRepositoryImpl.new); class UserRepositoryImpl
implements UserRepository { const UserRepositoryImpl(this.ref); final UserRepositoryRef ref; Future<User?> getUser() async { final response = await ref.read(dioProvider).user.fetch(); return User(response.id, response.name); } } VTFDBTFύοέʔδͷ 3FQPTJUPSZΛ্ॻ͖͢Δ VTFDBTFύοέʔδͷ 3FQPTJUPSZΛܧঝ͢Δ
Gateway ύοέʔδͷೖ name: app description: Application environment: sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.19.0' dependencies: flutter: sdk: flutter flutter_riverpod: gateway: path: ../gateway usecase: path: ../usecase dev_dependencies: flutter_test: sdk: flutter HBUFXBZύοέʔδΛ ґଘͱͯ͠Ճ͢Δ
Gateway ύοέʔδͷೖ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gateway/gateway.dart'; class UserPage
extends StatelessWidget { const UserPage({super.key}); @override Widget build(BuildContext context) { return ProviderScope( overrides: [ userRepositoryOverride, ], child: const UserName(), ); } } HBUFXBZύοέʔδΛ JNQPSU͢Δ 3FQPTJUPSZΛ্ॻ͖͢Δ είʔϓΛઃఆ͢Δ
ΞϓϦ࣮ߦ Gateway ͷ UserRepository ͕ฦͨ͠ Ϣʔβ໊͕දࣔ͞ΕΔΑ͏ ʹͳΔ
Gateway σʔλͷྲྀΕ App UseCase
UseCase Gateway App ґଘͷํ
ύοέʔδߏ App Gateway UseCase
ґଘΛٯసͤ͞ΔͨΊʹΔ͜ͱ • ٯస͍ͤͨ͞ґଘॲཧͷΠϯλʔϑΣʔεΛఆٛ → UseCase • ఆٛͨ͠ΠϯλʔϑΣʔεΛܧঝͯ͠ґଘॲཧΛ࣮ → Gateway •
࣮ͨ͠ґଘॲཧΛೖ → App
ProviderScope ͷઃఆͷ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gateway/gateway.dart'; class App
extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return ProviderScope( overrides: [ artistRepositoryOverride, authRepositoryOverride, notificationRepositoryOverride, searchRepositoryOverride, userRepositoryOverride, userProfileRepositoryOverride, ], child: const Page(), ); } } ্ॻ͖ͷ͚ͩมΛઃఆ ͢Δඞཁ͕͋Δ
https://github.com/KyoheiG3/ override_pod override_pod
override_pod • ύοέʔδͷ Override มΛूΊͯྻͷมΛ࡞ • ΞϊςʔγϣϯͱδΣωϨʔλͷ 2ͭͷύοέʔδ
override_pod name: gateway description: Gateway for infrastructure Layer environment: sdk:
'>=3.3.0 <4.0.0' dependencies: riverpod_annotation: usecase: path: ../usecase override_pod_annotation: dev_dependencies: build_runner: riverpod_generator: override_pod_generator: BOOPUBUJPOΛՃ͢Δ HFOFSBUPSΛՃ͢Δ
Before import 'package:usecase/usecase.dart'; final userRepositoryOverride = userRepositoryProvider.overrideWith(UserRepositoryImpl.new); class UserRepositoryImpl implements
UserRepository { const UserRepositoryImpl(this.ref); final UserRepositoryRef ref; Future<User?> getUser() async { final response = await ref.read(dioProvider).user.fetch(); return User(response.id, response.name); } }
After import 'package:override_pod_annotation/override_pod_annotation.dart'; import 'package:usecase/usecase.dart'; @overridePod final pod = userRepositoryProvider.overrideWith(UserRepositoryImpl.new);
class UserRepositoryImpl implements UserRepository { const UserRepositoryImpl(this.ref); final UserRepositoryRef ref; Future<User?> getUser() async { final response = await ref.read(dioProvider).user.fetch(); return User(response.id, response.name); } } 0WFSSJEFͷม໊ࣗ༝ !PWFSSJEF1PEΛ͚Δ Πϯϙʔτ͢Δ
Before import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gateway/gateway.dart'; class App extends
StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return ProviderScope( overrides: [ artistRepositoryOverride, authRepositoryOverride, notificationRepositoryOverride, searchRepositoryOverride, userRepositoryOverride, userProfileRepositoryOverride, ], child: const Page(), ); } } શͯͷมΛઃఆ͍ͯͨ͠
After import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gateway/gateway.dart'; class App extends
StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return ProviderScope( overrides: gatewayPodOverrides, child: const Page(), ); } } ग़དྷ্͕ͬͨมΛ ઃఆ͢Δ͚ͩ
ΞδΣϯμ •֓ཁ •ύοέʔδׂͷ࣮ྫ •ύοέʔδͷׂ •ςετʹ͍ͭͯ
App Gateway UseCase App UI Driver Presenter Gateway Entity UseCase
ׂ͝ͱʹύοέʔδΛ͚Δ
• ΦϒδΣΫτ • ڞ௨ͷࣝΛදͨ͠ͷ Entity
Entity class User { const User({ required this.id, required this.name,
required this.createdAt, }); final int id; final String name; final String createdAt; }
• Repository ͷΠϯλʔϑΣʔεΛఆٛ • ෳͷ Repository ͷॲཧΛ࣮ߦ • ϢʔβΞϓϦͷঢ়ଶΛ AsyncNoti
fi er Ͱอ࣋ UseCase
UseCase @riverpod class UserStateNotifier extends _$UserStateNotifier { @override FutureOr<UserState> build()
async { final user = await ref.watch(userRepositoryProvider).getUser(); return UserState( user: user, loggedIn: user != null, ); } Future<void> logout() async { await ref.read(userRepositoryProvider).logout(); state = const AsyncValue.data( UserState( user: null, loggedIn: false, ), ); } }
• UseCase ʹґଘ • Driver ͷΠϯλʔϑΣʔεΛఆٛ • Repository ͷ۩ମతͳॲཧΛ࣮ Gateway
Gateway @overridePod final pod = userRepositoryProvider.overrideWith(UserRepositoryImpl.new); class UserRepositoryImpl implements UserRepository
{ const UserRepositoryImpl(this.ref); final UserRepositoryRef ref; Future<User?> getUser() async { final response = await ref.read(dioProvider).user.fetch(); return User( id: response.id, name: response.name, createdAt: response.createdAt, ); } Future<void> logout() async { await ref.read(dioProvider).user.logout(); } }
Gateway @overridePod final pod = locationRepositoryProvider.overrideWith(LocationRepositoryImpl.new); class LocationRepositoryImpl implements LocationRepository
{ const LocationRepositoryImpl(this.ref); final LocationRepositoryRef ref; @override Future<LocationPermission> requestPermission() { return ref.read(locationProvider).requestPermission(); } @override Stream<LocationPosition> get position { return ref.read(locationProvider).getPositionStream(distanceFilter: 10); } }
• Gateway ʹґଘ • Driver ͷ۩ମతͳॲཧͷ࣮ • σόΠεʹґଘͨ͠ύοέʔδΛϥοϓ Driver
Driver import 'package:geolocator/geolocator.dart' as geolocator; @overridePod final pod = locationProvider.overrideWithValue(LocationImpl());
class LocationImpl implements Location { @override Future<LocationPermission> requestPermission() { return geolocator.Geolocator.requestPermission() .then((value) => value.toLocationPermission()); } @override Stream<LocationPosition> getPositionStream({ required int distanceFilter, }) { return geolocator.Geolocator.getPosition( locationSettings: locator.LocationSettings( distanceFilter: distanceFilter, ), ).map((event) => event.toLocationPosition()); } }
Presenter (Controller) • UseCase ʹґଘ • Repository ͷॲཧݺͼग़͞ͳ͍ • ෳͷ
State ͔Β ViewModel Λ࡞ͯ͠ UI ʹฦ͢ • UI ͔ΒͷೖྗΛड͚͚Δ
Presenter (Controller) @riverpod Future<UserPageModel> userPageController(UserPageControllerRef ref) async { final userState
= await ref.watch(userStateNotifierProvider.future); return UserPageModel( userName: userState.currentUser.name, createdAt: userState.currentUser.createdAt, action: UserPageAction( logout: () async { await ref.read(userStateNotifierProvider.notifier).logout(); }, ), ); }
UI • Presenter ʹґଘ • ViewModel Λड͚औͬͯ Widget Ͱදࣔ •
Ϣʔβͷૢ࡞Λ Controller ʹ͑Δ
UI class UserPage extends StatelessWidget { const UserPage({ super.key, required
this.model, }); final UserPageModel model; @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ Text(model.userName), Text(model.createdAt), ElevatedButton( onPressed: model.action.logout, child: const Text('Logout'), ), ], ), ); } }
App • Gateway / Driver / UI ʹґଘ • ্ॻ͖ͨ͠ґଘΛ
ProviderScope Ͱೖ
App import 'package:driver/driver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gateway/gateway.dart'; import
'package:ui/ui.dart'; void main() { runApp( ProviderScope( overrides: [ ...driverPodOverrides, ...gatewayPodOverrides, ], child: const App(), ), ); }
Driver Gateway UseCase Presenter UI ॲཧͷྲྀΕ
ґଘͷํ UI UseCase Gateway Presenter Driver App
App UI Driver Presenter Gateway Entity UseCase ύοέʔδߏ
ΫϦʔϯΞʔΩςΫνϟ 'SBNFXPSL%FWJDF ʹؔΘΔ෦
Flutter σόΠεʹґଘ͍ͯ͠Δύοέʔδ App UI Driver
Presenter Gateway Entity UseCase Flutter σόΠεʹґଘ͍ͯ͠ͳ͍ύοέʔδ
App UI Driver Presenter Gateway Entity UseCase ύοέʔδߏ
None
Melos • ઃఆͨ͠σΟϨΫτϦͷ pubspec.yaml Λ୳ͯ͠ղੳ • pubspec_overrides.yaml Λࣗಈੜͯ͠ϩʔΧϧͷύοέʔδͷґଘΛղܾ • λεΫͷฒྻॲཧ
ϩʔΧϧͷύοέʔδͷґଘΛղܾ name: app packages: - packages/**
ϩʔΧϧͷύοέʔδͷґଘΛղܾ $ melos bootstrap
ϩʔΧϧͷύοέʔδͷґଘΛղܾ # melos_managed_dependency_overrides: usecase dependency_overrides: usecase: path: ../usecase
ϩʔΧϧͷύοέʔδͷґଘΛղܾ name: gateway description: Gateway for infrastructure Layer environment: sdk:
'>=3.3.0 <4.0.0' dependencies: riverpod_annotation: usecase: # path: ../usecase dev_dependencies: build_runner: riverpod_generator: VTFDBTFύοέʔδͷ QBUIΛলུͰ͖Δ
λεΫͷฒྻॲཧ • concurrency Ͱฒྻ࣮ߦΛมߋՄೳ • orderDependents Ͱύοέʔδͷґଘؔάϥϑʹج͍ͮͯෳͷύοέʔ δͰεΫϦϓτΛ࣮ߦՄೳ • packageFilters
ʹͯ·Δύοέʔδશͯʹରͯ͠ฒྻͰλεΫΛ࣮ߦ͠ ͯ͘ΕΔ
λεΫͷฒྻॲཧ name: app packages: - packages/** scripts: gen: run: melos
gen:all --no-select gen:all: run: dart run build_runner build --delete-conflicting-outputs exec: concurrency: 8 orderDependents: true packageFilters: dependsOn: build_runner
ΞδΣϯμ •֓ཁ •ύοέʔδׂͷ࣮ྫ •ύοέʔδͷׂ •ςετʹ͍ͭͯ
fl utter test --coverage --update-goldens • --coverage • ςετͷΧόϨοδΛ lcov.info
ϑΝΠϧʹग़ྗ͢Δ • --update-goldens • Golden Test ͷߋ৽༻ͷը૾ग़ྗಉ࣌ʹߦ͏
Melos ΛͬͨϢχοτςετͷ࣮ߦ name: app packages: - packages/** scripts: test: run:
melos test:all --no-select test:all: run: flutter test --coverage --update-goldens exec: concurrency: 8 packageFilters: dirExists: - test UFTUσΟϨΫτϦ͕͋Δ ύοέʔδ͚࣮ͩߦ͢Δ
ϚϧνύοέʔδͷςετͷΧόϨοδ • ෳύοέʔδͰಉ͡ύεͷϑΝΠϧΧόϨοδ͕߹ࢉ͞ΕͨΓ͢Δ • ΠϯλʔϑΣʔεͷΈͷΧόϨοδͷܭଌɺΤϥʔΛ throw ͍ͯ͠Δ͚ͩ ͩͬͨΓɺ࣮ࡍʹॲཧ͕ແ͍ͷͰෆཁ
https://github.com/KyoheiG3/ lcov_excluder lcov_excluder
lcov_excluder • ΧόϨοδʹؚ·ΕΔෆཁͳܭଌϑΝΠϧΛআ֎ • আ֎ઃఆ͢Δ lcov.info Λɺҙͷ .yaml ʹఆ࣮ٛͯ͠ߦ
ΧόϨοδܭଌͷআ֎ઃఆྫ coverage: default: exclude: - lib/**/*.*.dart targets: - usecase: sourceRoot:
packages/usecase exclude: - lib/src/repository/*
ΧόϨοδܭଌͷআ֎ઃఆྫ $ dart run lcov_excluder exclude
·ͱΊ • ύοέʔδΛ͚Δ߹ׂΛҙࣝ͢Δ • Flutter Device ͳͲӨڹͷେ͖ͳͷ΄Ͳԁͷ֎ଆʹஔ͘Α͏ʹ͢Δ • Өڹͷখ͞ͳͷԁͷଆʹஔ͘Α͏ʹ͢Δ
• ςετͳͲฒྻͰ࣮ߦ͢Δ • ΠϯλʔϑΣʔεͷςετͷΧόϨοδܭଌߦΘͳ͍
Thanks !