[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
---
|
||||
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<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
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<List<User>> 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<String, dynamic> json) => _$UserFromJson(json);
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
dart run build_runner build # 코드 generate
|
||||
```
|
||||
|
||||
### Async / await
|
||||
```dart
|
||||
Future<void> _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]]
|
||||
Reference in New Issue
Block a user