--- id: mobile-flutter-patterns title: Flutter — Widget / State / Riverpod category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, flutter, dart, vibe-coding] tech_stack: { language: "Dart / Flutter", applicable_to: ["iOS", "Android", "Web", "Desktop"] } applied_in: [] aliases: [Flutter, Dart, Riverpod, Bloc, GoRouter, Material 3] --- # Flutter > Google 의 cross-platform UI. **단일 codebase → iOS / Android / Web / Desktop**. **Skia / Impeller 가 그림 — native 만큼 빠름**. Material 3 / Cupertino UI built-in. ## 📖 핵심 개념 - Widget: 모든 거. immutable. - State: StatefulWidget 의 mutable. - Riverpod / Bloc: state management. - Hot reload: 변경 즉시 반영. ## 💻 코드 패턴 ### 기본 widget ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue), home: const HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { int _count = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Counter')), body: Center(child: Text('$_count', style: Theme.of(context).textTheme.displayLarge)), floatingActionButton: FloatingActionButton( onPressed: () => setState(() => _count++), child: const Icon(Icons.add), ), ); } } ``` ### Layout ```dart Scaffold( body: SafeArea( child: Column( children: [ const Text('Header'), Expanded( child: ListView.builder( itemCount: items.length, itemBuilder: (ctx, i) => ListTile( title: Text(items[i].name), onTap: () => Navigator.push(ctx, MaterialPageRoute(builder: (_) => DetailPage(items[i]))), ), ), ), ], ), ), ) ``` ### Riverpod (state management) ```yaml # pubspec.yaml dependencies: flutter_riverpod: ^2.4.0 riverpod_annotation: ^2.3.0 ``` ```dart @riverpod class Counter extends _$Counter { @override int build() => 0; void increment() => state++; } @riverpod Future> users(UsersRef ref) async { final dio = ref.watch(dioProvider); final r = await dio.get('/users'); return (r.data as List).map((j) => User.fromJson(j)).toList(); } // Widget class CounterPage extends ConsumerWidget { @override Widget build(BuildContext ctx, WidgetRef ref) { final count = ref.watch(counterProvider); return Column( children: [ Text('$count'), ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: const Text('+'), ), ], ); } } ``` ### AsyncValue (network state) ```dart class UsersPage extends ConsumerWidget { @override Widget build(BuildContext ctx, WidgetRef ref) { final asyncUsers = ref.watch(usersProvider); return asyncUsers.when( data: (users) => ListView.builder(...), loading: () => const CircularProgressIndicator(), error: (e, _) => Text('Error: $e'), ); } } ``` ### GoRouter (navigation) ```dart final router = GoRouter( routes: [ GoRoute(path: '/', builder: (_, __) => const HomePage()), GoRoute( path: '/user/:id', builder: (_, state) => UserPage(id: state.pathParameters['id']!), ), ], ); MaterialApp.router(routerConfig: router, ...); ``` ### Network (Dio) ```dart final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com')); dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { options.headers['Authorization'] = 'Bearer $token'; handler.next(options); }, onError: (e, handler) { if (e.response?.statusCode == 401) refreshAuth(); handler.next(e); }, )); final r = await dio.get('/users/$id'); final user = User.fromJson(r.data); ``` ### Freezed (immutable model) ```dart @freezed class User with _$User { const factory User({ required String id, required String email, String? name, }) = _User; factory User.fromJson(Map json) => _$UserFromJson(json); } ``` ```bash dart run build_runner build # 코드 generate ``` ### Async / await ```dart Future _load() async { setState(() => _loading = true); try { final users = await api.fetchUsers(); setState(() => _users = users); } catch (e) { showError(e.toString()); } finally { setState(() => _loading = false); } } ``` ### 새 architecture (riverpod hooks 같이) ```dart class HomePage extends HookConsumerWidget { @override Widget build(BuildContext ctx, WidgetRef ref) { final controller = useTextEditingController(); final search = useState(''); final users = ref.watch(searchUsersProvider(search.value)); return Column( children: [ TextField(controller: controller, onChanged: (v) => search.value = v), users.when(...), ], ); } } ``` ### Test ```dart testWidgets('counter increments', (tester) async { await tester.pumpWidget(const MyApp()); expect(find.text('0'), findsOneWidget); await tester.tap(find.byIcon(Icons.add)); await tester.pump(); expect(find.text('1'), findsOneWidget); }); // Riverpod test('counter', () { final container = ProviderContainer(); expect(container.read(counterProvider), 0); container.read(counterProvider.notifier).increment(); expect(container.read(counterProvider), 1); }); ``` ### Build / release ```bash # iOS flutter build ipa --release # Android flutter build appbundle --release # Web flutter build web --release # Desktop flutter build macos --release flutter build windows --release ``` ### Performance - `const` constructor 사용 (rebuild skip). - Widget trees 작게 + split. - `RepaintBoundary` 큰 widget 분리. - Image cache (cached_network_image). - Lists = ListView.builder (lazy). ### vs React Native / Native ``` Flutter: + 빠름 (Skia compile) + 같은 UI 모든 platform + Hot reload 강력 - Dart language (학습) - Native 통합 = platform channel - Bundle 큼 (~5-15MB) React Native: + JS / TS 친숙 + 큰 ecosystem - JS bridge / new arch 학습 - Native UI 위주 Native: + Best UX / performance - 두 번 개발 ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Cross-platform + 일관 UI | Flutter | | 작은 팀 / 빠른 MVP | Flutter / RN | | Native UX strict | Native | | Web 도 같이 | Flutter Web | | Desktop 도 같이 | Flutter | | 큰 native ecosystem 의존 | Native / RN | ## ❌ 안티패턴 - **`const` 안 씀**: 매 rebuild. - **거대 build method**: 분리. - **setState 큰 tree**: 작은 stateful widget. - **Provider 너무 많음 (각 var)**: combine. - **Dio interceptor 안 + auth 매번**: 통일. - **Async/await 없는 callback hell**: future 변환. - **Native channel 직접 (필요 시)**: package 사용 / pigeon. ## 🤖 LLM 활용 힌트 - Riverpod + Freezed + GoRouter + Dio = 표준 stack. - AsyncValue.when 으로 loading/error 깔끔. - const constructor + ListView.builder. ## 🔗 관련 문서 - [[Mobile_KMP_Compose]] - [[React_Native_Bridge_Performance]] - [[Android_Compose_State_Hoisting]]