7.3 KiB
7.3 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mobile-flutter-patterns | Flutter — Widget / State / Riverpod | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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
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
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)
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
@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)
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)
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)
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)
@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);
}
dart run build_runner build # 코드 generate
Async / await
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 같이)
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
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
# 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
constconstructor 사용 (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.