Merge pull request #32 from mljxxx/feat/add_flutter_dev_skill

add flutter dev skill
This commit is contained in:
zest0198
2026-03-26 19:25:39 +08:00
committed by GitHub
15 changed files with 4723 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ Development skills for AI coding agents. Plug into your favorite AI coding tool
| `fullstack-dev` | Full-stack backend architecture and frontend-backend integration. REST API design, auth flows (JWT, session, OAuth), real-time features (SSE, WebSocket), database integration (SQL / NoSQL), production hardening, and release checklist. Guided workflow: requirements → architecture → implementation. | Official | | `fullstack-dev` | Full-stack backend architecture and frontend-backend integration. REST API design, auth flows (JWT, session, OAuth), real-time features (SSE, WebSocket), database integration (SQL / NoSQL), production hardening, and release checklist. Guided workflow: requirements → architecture → implementation. | Official |
| `android-native-dev` | Android native application development with Material Design 3. Kotlin / Jetpack Compose, adaptive layouts, Gradle configuration, accessibility (WCAG), build troubleshooting, performance optimization, and motion system. | Official | | `android-native-dev` | Android native application development with Material Design 3. Kotlin / Jetpack Compose, adaptive layouts, Gradle configuration, accessibility (WCAG), build troubleshooting, performance optimization, and motion system. | Official |
| `ios-application-dev` | iOS application development guide covering UIKit, SnapKit, and SwiftUI. Touch targets, safe areas, navigation patterns, Dynamic Type, Dark Mode, accessibility, collection views, and Apple HIG compliance. | Official | | `ios-application-dev` | iOS application development guide covering UIKit, SnapKit, and SwiftUI. Touch targets, safe areas, navigation patterns, Dynamic Type, Dark Mode, accessibility, collection views, and Apple HIG compliance. | Official |
| `flutter-dev` | Flutter cross-platform development covering widget patterns, Riverpod/Bloc state management, GoRouter navigation, performance optimization, and testing strategies. | Official |
| `shader-dev` | Comprehensive GLSL shader techniques for creating stunning visual effects — ray marching, SDF modeling, fluid simulation, particle systems, procedural generation, lighting, post-processing, and more. ShaderToy-compatible. | Official | | `shader-dev` | Comprehensive GLSL shader techniques for creating stunning visual effects — ray marching, SDF modeling, fluid simulation, particle systems, procedural generation, lighting, post-processing, and more. ShaderToy-compatible. | Official |
| `gif-sticker-maker` | Convert photos (people, pets, objects, logos) into 4 animated GIF stickers with captions. Funko Pop / Pop Mart style, powered by MiniMax Image & Video Generation API. | Official | | `gif-sticker-maker` | Convert photos (people, pets, objects, logos) into 4 animated GIF stickers with captions. Funko Pop / Pop Mart style, powered by MiniMax Image & Video Generation API. | Official |
| `minimax-pdf` | Generate, fill, and reformat PDF documents with a token-based design system. CREATE polished PDFs from scratch (15 cover styles), FILL existing form fields, or REFORMAT documents into a new design. Print-ready output with typography and color derived from document type. | Official | | `minimax-pdf` | Generate, fill, and reformat PDF documents with a token-based design system. CREATE polished PDFs from scratch (15 cover styles), FILL existing form fields, or REFORMAT documents into a new design. Print-ready output with typography and color derived from document type. | Official |

View File

@@ -14,6 +14,7 @@
| `fullstack-dev` | 全栈后端架构与前后端集成。REST API 设计、认证流程JWT、Session、OAuth、实时功能SSE、WebSocket、数据库集成SQL / NoSQL、生产环境加固与发布清单。引导式工作流需求收集 → 架构决策 → 实现。 | Official | | `fullstack-dev` | 全栈后端架构与前后端集成。REST API 设计、认证流程JWT、Session、OAuth、实时功能SSE、WebSocket、数据库集成SQL / NoSQL、生产环境加固与发布清单。引导式工作流需求收集 → 架构决策 → 实现。 | Official |
| `android-native-dev` | 基于 Material Design 3 的 Android 原生应用开发。Kotlin / Jetpack Compose、自适应布局、Gradle 配置、无障碍WCAG、构建问题排查、性能优化与动效系统。 | Official | | `android-native-dev` | 基于 Material Design 3 的 Android 原生应用开发。Kotlin / Jetpack Compose、自适应布局、Gradle 配置、无障碍WCAG、构建问题排查、性能优化与动效系统。 | Official |
| `ios-application-dev` | iOS 应用开发指南,涵盖 UIKit、SnapKit 和 SwiftUI。触控目标、安全区域、导航模式、Dynamic Type、深色模式、无障碍、集合视图符合 Apple HIG 规范。 | Official | | `ios-application-dev` | iOS 应用开发指南,涵盖 UIKit、SnapKit 和 SwiftUI。触控目标、安全区域、导航模式、Dynamic Type、深色模式、无障碍、集合视图符合 Apple HIG 规范。 | Official |
| `flutter-dev` | Flutter 跨平台开发指南,涵盖 Widget 模式、Riverpod/Bloc 状态管理、GoRouter 导航、性能优化与测试策略。 | Official |
| `shader-dev` | 全面的 GLSL 着色器技术,用于创建惊艳的视觉效果 — 光线行进、SDF 建模、流体模拟、粒子系统、程序化生成、光照、后处理等。兼容 ShaderToy。 | Official | | `shader-dev` | 全面的 GLSL 着色器技术,用于创建惊艳的视觉效果 — 光线行进、SDF 建模、流体模拟、粒子系统、程序化生成、光照、后处理等。兼容 ShaderToy。 | Official |
| `gif-sticker-maker` | 将照片人物、宠物、物品、Logo转换为 4 张带字幕的动画 GIF 贴纸。Funko Pop / Pop Mart 盲盒风格,基于 MiniMax 图片与视频生成 API。 | Official | | `gif-sticker-maker` | 将照片人物、宠物、物品、Logo转换为 4 张带字幕的动画 GIF 贴纸。Funko Pop / Pop Mart 盲盒风格,基于 MiniMax 图片与视频生成 API。 | Official |
| `minimax-pdf` | 基于 token 化设计系统生成、填写和重排 PDF 文档。支持三种模式CREATE从零生成15 种封面风格、FILL填写现有表单字段、REFORMAT将已有文档重排为新设计。排版与配色由文档类型自动推导输出即可打印。 | Official | | `minimax-pdf` | 基于 token 化设计系统生成、填写和重排 PDF 文档。支持三种模式CREATE从零生成15 种封面风格、FILL填写现有表单字段、REFORMAT将已有文档重排为新设计。排版与配色由文档类型自动推导输出即可打印。 | Official |

128
skills/flutter-dev/SKILL.md Normal file
View File

@@ -0,0 +1,128 @@
---
name: flutter-dev
description: |
Flutter cross-platform development guide covering widget patterns, Riverpod/Bloc state management, GoRouter navigation, performance optimization, and platform-specific implementations. Includes const optimization, responsive layouts, testing strategies, and DevTools profiling.
Use when: building Flutter apps, implementing state management (Riverpod/Bloc), setting up GoRouter navigation, creating custom widgets, optimizing performance, writing widget tests, cross-platform development.
license: MIT
metadata:
version: "1.0.0"
category: mobile
sources:
- Flutter Documentation
- Riverpod Documentation
- Bloc Library Documentation
---
# Flutter Development Guide
A practical guide for building cross-platform applications with Flutter 3 and Dart. Focuses on proven patterns, state management, and performance optimization.
## Quick Reference
### Widget Patterns
| Purpose | Component |
|---------|-----------|
| State management (simple) | `StateProvider` + `ConsumerWidget` |
| State management (complex) | `NotifierProvider` / `Bloc` |
| Async data | `FutureProvider` / `AsyncNotifierProvider` |
| Real-time streams | `StreamProvider` |
| Navigation | `GoRouter` + `context.go/push` |
| Responsive layout | `LayoutBuilder` + breakpoints |
| List display | `ListView.builder` |
| Complex scrolling | `CustomScrollView` + Slivers |
| Hooks | `HookWidget` + `useState/useEffect` |
| Forms | `Form` + `TextFormField` + validation |
### Performance Patterns
| Purpose | Solution |
|---------|----------|
| Prevent rebuilds | `const` constructors |
| Selective updates | `ref.watch(provider.select(...))` |
| Isolate repaints | `RepaintBoundary` |
| Lazy lists | `ListView.builder` |
| Heavy computation | `compute()` isolate |
| Image caching | `cached_network_image` |
## Core Principles
### Widget Optimization
- Use `const` constructors wherever possible
- Extract static widgets to separate const classes
- Use `Key` for list items (ValueKey, ObjectKey)
- Prefer `ConsumerWidget` over `StatefulWidget` for state
### State Management
- Riverpod for dependency injection and simple state
- Bloc/Cubit for event-driven workflows and complex logic
- Never mutate state directly (create new instances)
- Use `select()` to minimize rebuilds
### Layout
- 8pt spacing increments (8, 16, 24, 32, 48)
- Responsive breakpoints: mobile (<650), tablet (650-1100), desktop (>1100)
- Support all screen sizes with flexible layouts
- Follow Material 3 / Cupertino design guidelines
### Performance
- Profile with DevTools before optimizing
- Target <16ms frame time for 60fps
- Use `RepaintBoundary` for complex animations
- Offload heavy work with `compute()`
## Checklist
### Widget Best Practices
- [ ] `const` constructors on all static widgets
- [ ] Proper `Key` on list items
- [ ] `ConsumerWidget` for state-dependent widgets
- [ ] No widget building inside `build()` method
- [ ] Extract reusable widgets to separate files
### State Management
- [ ] Immutable state objects
- [ ] `select()` for granular rebuilds
- [ ] Proper provider scoping
- [ ] Dispose controllers and subscriptions
- [ ] Handle loading/error states
### Navigation
- [ ] GoRouter with typed routes
- [ ] Auth guards via redirect
- [ ] Deep linking support
- [ ] State preservation across routes
### Performance
- [ ] Profile mode testing (`flutter run --profile`)
- [ ] <16ms frame rendering time
- [ ] No unnecessary rebuilds (DevTools check)
- [ ] Images cached and resized
- [ ] Heavy computation in isolates
### Testing
- [ ] Widget tests for UI components
- [ ] Unit tests for business logic
- [ ] Integration tests for user flows
- [ ] Bloc tests with `blocTest()`
## References
| Topic | Reference |
|-------|-----------|
| Widget patterns, const optimization, responsive layout | [Widget Patterns](references/widget-patterns.md) |
| Riverpod providers, notifiers, async state | [Riverpod State Management](references/riverpod-state.md) |
| Bloc, Cubit, event-driven state | [Bloc State Management](references/bloc-state.md) |
| GoRouter setup, routes, deep linking | [GoRouter Navigation](references/gorouter-navigation.md) |
| Feature-based structure, dependencies | [Project Structure](references/project-structure.md) |
| Profiling, const optimization, DevTools | [Performance Optimization](references/performance.md) |
| Widget tests, integration tests, mocking | [Testing Strategies](references/testing.md) |
| iOS/Android/Web specific implementations | [Platform Integration](references/platform-specific.md) |
| Implicit/explicit animations, Hero, transitions | [Animations](references/animations.md) |
| Dio, interceptors, error handling, caching | [Networking](references/networking.md) |
| Form validation, FormField, input formatters | [Forms](references/forms.md) |
| i18n, flutter_localizations, intl | [Localization](references/localization.md) |
---
Flutter, Dart, Material Design, and Cupertino are trademarks of Google LLC and Apple Inc. respectively. Riverpod, Bloc, and GoRouter are open-source packages by their respective maintainers.

View File

@@ -0,0 +1,497 @@
# Animations
Flutter animation patterns covering implicit animations, explicit animations, Hero transitions, and page transitions.
## Implicit Animations
Use implicit animations for simple property changes:
```dart
class ImplicitAnimationExample extends StatefulWidget {
const ImplicitAnimationExample({super.key});
@override
State<ImplicitAnimationExample> createState() => _ImplicitAnimationExampleState();
}
class _ImplicitAnimationExampleState extends State<ImplicitAnimationExample> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_expanded ? 16 : 8),
),
child: const Center(child: Text('Tap me')),
),
);
}
}
```
### Common Implicit Widgets
| Widget | Animates |
|--------|----------|
| `AnimatedContainer` | Size, color, padding, decoration |
| `AnimatedOpacity` | Opacity |
| `AnimatedPadding` | Padding |
| `AnimatedPositioned` | Position in Stack |
| `AnimatedAlign` | Alignment |
| `AnimatedCrossFade` | Cross-fade between two widgets |
| `AnimatedSwitcher` | Transition between child widgets |
| `AnimatedDefaultTextStyle` | Text style |
| `AnimatedScale` | Scale transform |
| `AnimatedRotation` | Rotation transform |
| `AnimatedSlide` | Slide offset |
### AnimatedSwitcher
```dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: _showFirst
? const Icon(Icons.check, key: ValueKey('check'))
: const Icon(Icons.close, key: ValueKey('close')),
)
```
## Explicit Animations
Use explicit animations for complex, custom, or controlled animations:
```dart
class ExplicitAnimationExample extends StatefulWidget {
const ExplicitAnimationExample({super.key});
@override
State<ExplicitAnimationExample> createState() => _ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _scaleAnimation;
late final Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_rotationAnimation = Tween<double>(begin: 0, end: 0.1).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) => _controller.reverse(),
onTapCancel: () => _controller.reverse(),
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: child,
),
);
},
child: const Card(child: Padding(padding: EdgeInsets.all(24), child: Text('Press me'))),
),
);
}
}
```
### Animation with Hooks
```dart
import 'package:flutter_hooks/flutter_hooks.dart';
class AnimatedButtonHook extends HookWidget {
const AnimatedButtonHook({super.key});
@override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final scale = useAnimation(
Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
),
);
return GestureDetector(
onTapDown: (_) => controller.forward(),
onTapUp: (_) => controller.reverse(),
onTapCancel: () => controller.reverse(),
child: Transform.scale(
scale: scale,
child: const Card(child: Text('Animated Button')),
),
);
}
}
```
### Staggered Animations
```dart
class StaggeredAnimation extends StatefulWidget {
const StaggeredAnimation({super.key});
@override
State<StaggeredAnimation> createState() => _StaggeredAnimationState();
}
class _StaggeredAnimationState extends State<StaggeredAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final List<Animation<double>> _itemAnimations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_itemAnimations = List.generate(5, (index) {
final start = index * 0.1;
final end = start + 0.4;
return Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end.clamp(0, 1), curve: Curves.easeOut),
),
);
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(5, (index) {
return AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return Opacity(
opacity: _itemAnimations[index].value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _itemAnimations[index].value)),
child: child,
),
);
},
child: ListTile(title: Text('Item $index')),
);
}),
);
}
}
```
## Hero Animations
```dart
class HeroSourcePage extends StatelessWidget {
const HeroSourcePage({super.key});
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return GestureDetector(
onTap: () => context.push('/detail/${item.id}'),
child: Hero(
tag: 'hero-${item.id}',
child: Image.network(item.imageUrl, fit: BoxFit.cover),
),
);
},
);
}
}
class HeroDetailPage extends StatelessWidget {
final String itemId;
const HeroDetailPage({super.key, required this.itemId});
@override
Widget build(BuildContext context) {
final item = getItem(itemId);
return Scaffold(
body: Column(
children: [
Hero(
tag: 'hero-${item.id}',
child: Image.network(item.imageUrl, fit: BoxFit.cover),
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(item.title, style: Theme.of(context).textTheme.headlineMedium),
),
],
),
);
}
}
```
### Hero with Custom Flight
```dart
Hero(
tag: 'avatar-$userId',
flightShuttleBuilder: (
flightContext,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Material(
color: Colors.transparent,
child: CircleAvatar(
radius: lerpDouble(24, 48, animation.value),
backgroundImage: NetworkImage(avatarUrl),
),
);
},
);
},
child: CircleAvatar(radius: 24, backgroundImage: NetworkImage(avatarUrl)),
)
```
## Page Transitions
### GoRouter Custom Transitions
```dart
GoRoute(
path: '/detail/:id',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: DetailPage(id: state.pathParameters['id']!),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
)),
child: child,
),
);
},
);
},
)
```
### Common Transition Patterns
```dart
extension PageTransitions on CustomTransitionPage {
static CustomTransitionPage<T> fade<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
);
}
static CustomTransitionPage<T> slideUp<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: child,
);
},
);
}
static CustomTransitionPage<T> scale<T>({
required LocalKey key,
required Widget child,
}) {
return CustomTransitionPage<T>(
key: key,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(begin: 0.9, end: 1).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: FadeTransition(opacity: animation, child: child),
);
},
);
}
}
```
### Shared Axis Transition
```dart
import 'package:animations/animations.dart';
GoRoute(
path: '/settings',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: const SettingsPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
);
},
)
```
## Common Curves
| Curve | Usage |
|-------|-------|
| `Curves.easeInOut` | General purpose (default) |
| `Curves.easeOut` | Deceleration (entering) |
| `Curves.easeIn` | Acceleration (exiting) |
| `Curves.elasticOut` | Bouncy effect |
| `Curves.bounceOut` | Bounce at end |
| `Curves.fastOutSlowIn` | Material standard |
| `Curves.easeOutCubic` | Smooth deceleration |
## Animation Performance
```dart
class PerformantAnimation extends StatelessWidget {
const PerformantAnimation({super.key});
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(animation.value * 100, 0),
child: child,
);
},
child: const ExpensiveWidget(),
),
);
}
}
```
### Performance Tips
| Tip | Implementation |
|-----|----------------|
| Use `child` parameter | Pass static content to `child` in `AnimatedBuilder` |
| `RepaintBoundary` | Isolate animated widgets |
| Avoid `Opacity` widget | Use `FadeTransition` instead |
| Prefer transforms | `Transform` is cheaper than layout changes |
| Pre-compute values | Calculate in `initState`, not `build` |
## Animation Checklist
| Item | Implementation |
|------|----------------|
| Simple animations | Use implicit widgets |
| Complex sequences | Use `AnimationController` |
| Widget transitions | `AnimatedSwitcher` with key |
| Cross-page elements | `Hero` with unique tags |
| Page transitions | `CustomTransitionPage` |
| Performance | `RepaintBoundary` + `child` parameter |
---
*Flutter and Material Design are trademarks of Google LLC.*

View File

@@ -0,0 +1,281 @@
# Bloc State Management
Bloc state management guide covering events, states, Cubit, and widget integration for complex business logic.
## When to Use Bloc
Use **Bloc/Cubit** when you need:
- Explicit event → state transitions
- Complex business logic with multiple events
- Predictable, testable state flows
- Clear separation between UI and logic
| Use Case | Recommended |
|----------|-------------|
| Simple mutable state | Riverpod |
| Computed values | Riverpod |
| Event-driven workflows | Bloc |
| Forms, auth, wizards | Bloc |
| Feature modules with complex logic | Bloc |
## Core Concepts
| Concept | Description |
|---------|-------------|
| Event | User or system input that triggers state change |
| State | Immutable representation of UI state |
| Bloc | Maps events to new states |
| Cubit | Simplified Bloc without events |
## Cubit (Recommended for Simpler Logic)
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
void reset() => emit(0);
}
```
## Full Bloc Setup
### Event Definition
```dart
sealed class CounterEvent {}
final class CounterIncremented extends CounterEvent {}
final class CounterDecremented extends CounterEvent {}
final class CounterReset extends CounterEvent {}
```
### State Definition
```dart
class CounterState {
final int value;
final bool isLoading;
const CounterState({
required this.value,
this.isLoading = false,
});
CounterState copyWith({int? value, bool? isLoading}) {
return CounterState(
value: value ?? this.value,
isLoading: isLoading ?? this.isLoading,
);
}
}
```
### Bloc Implementation
```dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<CounterIncremented>(_onIncremented);
on<CounterDecremented>(_onDecremented);
on<CounterReset>(_onReset);
}
void _onIncremented(CounterIncremented event, Emitter<CounterState> emit) {
emit(state.copyWith(value: state.value + 1));
}
void _onDecremented(CounterDecremented event, Emitter<CounterState> emit) {
emit(state.copyWith(value: state.value - 1));
}
void _onReset(CounterReset event, Emitter<CounterState> emit) {
emit(const CounterState(value: 0));
}
}
```
## Providing Bloc to Widget Tree
```dart
// Single bloc
BlocProvider(
create: (_) => CounterBloc(),
child: const CounterScreen(),
);
// Multiple blocs
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthBloc()),
BlocProvider(create: (_) => ProfileBloc()),
BlocProvider(create: (_) => SettingsBloc()),
],
child: const AppRoot(),
);
```
## Using Bloc in Widgets
### BlocBuilder (UI Rebuilds)
```dart
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterBloc, CounterState>(
buildWhen: (prev, curr) => prev.value != curr.value,
builder: (context, state) {
return Text(
state.value.toString(),
style: Theme.of(context).textTheme.displayLarge,
);
},
);
}
}
```
### BlocListener (Side Effects)
```dart
BlocListener<AuthBloc, AuthState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == AuthStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Error')),
);
}
if (state.status == AuthStatus.authenticated) {
context.go('/home');
}
},
child: const LoginForm(),
);
```
### BlocConsumer (Builder + Listener)
```dart
BlocConsumer<FormBloc, FormState>(
listenWhen: (prev, curr) => prev.status != curr.status,
listener: (context, state) {
if (state.status == FormStatus.success) {
context.pop();
}
},
buildWhen: (prev, curr) => prev.isValid != curr.isValid,
builder: (context, state) {
return ElevatedButton(
onPressed: state.isValid
? () => context.read<FormBloc>().add(FormSubmitted())
: null,
child: const Text('Submit'),
);
},
);
```
### BlocSelector (Granular Rebuilds)
```dart
BlocSelector<UserBloc, UserState, String>(
selector: (state) => state.user.name,
builder: (context, name) {
return Text('Hello, $name');
},
);
```
## Async Bloc Pattern
```dart
on<UserRequested>((event, emit) async {
emit(state.copyWith(status: UserStatus.loading));
try {
final user = await repository.fetchUser(event.userId);
emit(state.copyWith(status: UserStatus.success, user: user));
} catch (e) {
emit(state.copyWith(status: UserStatus.failure, error: e.toString()));
}
});
```
## Bloc + GoRouter Auth Guard
```dart
redirect: (context, state) {
final authState = context.read<AuthBloc>().state;
final isAuthRoute = state.matchedLocation.startsWith('/auth');
if (authState.status != AuthStatus.authenticated && !isAuthRoute) {
return '/auth/login';
}
if (authState.status == AuthStatus.authenticated && isAuthRoute) {
return '/';
}
return null;
}
```
## Testing Bloc
```dart
import 'package:bloc_test/bloc_test.dart';
blocTest<CounterBloc, CounterState>(
'emits incremented value when CounterIncremented added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterIncremented()),
expect: () => [const CounterState(value: 1)],
);
blocTest<CounterBloc, CounterState>(
'emits multiple states',
build: () => CounterBloc(),
act: (bloc) {
bloc.add(CounterIncremented());
bloc.add(CounterIncremented());
bloc.add(CounterDecremented());
},
expect: () => [
const CounterState(value: 1),
const CounterState(value: 2),
const CounterState(value: 1),
],
);
```
## Best Practices
| Do | Don't |
|----|-------|
| Keep states immutable | Mutate state directly |
| Use small, focused blocs | Create "god blocs" with everything |
| One feature = one bloc | Share blocs across unrelated features |
| Use Cubit for simple cases | Overcomplicate with Bloc unnecessarily |
| Test all state transitions | Skip bloc testing |
| Use `buildWhen`/`listenWhen` | Rebuild on every state change |
## Widget Reference
| Widget | Purpose |
|--------|---------|
| `BlocBuilder` | UI rebuilds based on state |
| `BlocListener` | Side effects (navigation, snackbar) |
| `BlocConsumer` | Both builder and listener |
| `BlocSelector` | Granular state selection |
| `BlocProvider` | Dependency injection |
| `MultiBlocProvider` | Multiple bloc injection |
| `RepositoryProvider` | Repository injection |
---
*Bloc is an open-source state management library by Felix Angelov.*

View File

@@ -0,0 +1,656 @@
# Forms
Form validation, FormField patterns, input formatting, and reusable form components for Flutter.
## Basic Form Setup
```dart
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
await authService.login(
email: _emailController.text.trim(),
password: _passwordController.text,
);
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
autocorrect: false,
validator: Validators.email,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outlined),
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
validator: Validators.password,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Login'),
),
],
),
);
}
}
```
## Validators
```dart
class Validators {
static String? required(String? value) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
return null;
}
static String? email(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!regex.hasMatch(value.trim())) {
return 'Enter a valid email address';
}
return null;
}
static String? password(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
return null;
}
static String? strongPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return 'Password must contain an uppercase letter';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return 'Password must contain a lowercase letter';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return 'Password must contain a number';
}
return null;
}
static String? phone(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Phone number is required';
}
final digits = value.replaceAll(RegExp(r'\D'), '');
if (digits.length < 10 || digits.length > 15) {
return 'Enter a valid phone number';
}
return null;
}
static String? minLength(int min) {
return (String? value) {
if (value == null || value.length < min) {
return 'Must be at least $min characters';
}
return null;
};
}
static String? maxLength(int max) {
return (String? value) {
if (value != null && value.length > max) {
return 'Must be at most $max characters';
}
return null;
};
}
static String? Function(String?) combine(List<String? Function(String?)> validators) {
return (String? value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) return error;
}
return null;
};
}
static String? match(String pattern, String message) {
return (String? value) {
if (value != null && !RegExp(pattern).hasMatch(value)) {
return message;
}
return null;
};
}
static String? confirmPassword(TextEditingController passwordController) {
return (String? value) {
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
};
}
}
```
## Input Formatters
```dart
import 'package:flutter/services.dart';
class PhoneInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
final buffer = StringBuffer();
for (int i = 0; i < digits.length && i < 10; i++) {
if (i == 3 || i == 6) buffer.write('-');
buffer.write(digits[i]);
}
return TextEditingValue(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
class CreditCardFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');
final buffer = StringBuffer();
for (int i = 0; i < digits.length && i < 16; i++) {
if (i > 0 && i % 4 == 0) buffer.write(' ');
buffer.write(digits[i]);
}
return TextEditingValue(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
class CurrencyInputFormatter extends TextInputFormatter {
final int decimalPlaces;
CurrencyInputFormatter({this.decimalPlaces = 2});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) return newValue;
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
if (digits.isEmpty) return const TextEditingValue(text: '');
final value = int.parse(digits) / 100;
final formatted = value.toStringAsFixed(decimalPlaces);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
class UpperCaseFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return newValue.copyWith(text: newValue.text.toUpperCase());
}
}
```
### Using Formatters
```dart
TextFormField(
decoration: const InputDecoration(labelText: 'Phone'),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
PhoneInputFormatter(),
],
)
TextFormField(
decoration: const InputDecoration(labelText: 'Amount'),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d.]')),
CurrencyInputFormatter(),
],
)
```
## Custom FormFields
### Dropdown FormField
```dart
class DropdownFormField<T> extends FormField<T> {
DropdownFormField({
super.key,
required List<DropdownMenuItem<T>> items,
super.initialValue,
super.validator,
super.onSaved,
String? labelText,
String? hintText,
ValueChanged<T?>? onChanged,
}) : super(
builder: (state) {
return InputDecorator(
decoration: InputDecoration(
labelText: labelText,
errorText: state.errorText,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: state.value,
hint: hintText != null ? Text(hintText) : null,
isExpanded: true,
items: items,
onChanged: (value) {
state.didChange(value);
onChanged?.call(value);
},
),
),
);
},
);
}
```
### Checkbox FormField
```dart
class CheckboxFormField extends FormField<bool> {
CheckboxFormField({
super.key,
required Widget label,
super.initialValue = false,
super.validator,
super.onSaved,
}) : super(
builder: (state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Checkbox(
value: state.value ?? false,
onChanged: state.didChange,
),
Expanded(child: GestureDetector(
onTap: () => state.didChange(!(state.value ?? false)),
child: label,
)),
],
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(left: 12, top: 4),
child: Text(
state.errorText!,
style: TextStyle(
color: Theme.of(state.context).colorScheme.error,
fontSize: 12,
),
),
),
],
);
},
);
}
```
### Date Picker FormField
```dart
class DatePickerFormField extends FormField<DateTime> {
DatePickerFormField({
super.key,
super.initialValue,
super.validator,
super.onSaved,
String? labelText,
DateTime? firstDate,
DateTime? lastDate,
}) : super(
builder: (state) {
return GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: state.context,
initialDate: state.value ?? DateTime.now(),
firstDate: firstDate ?? DateTime(1900),
lastDate: lastDate ?? DateTime(2100),
);
if (picked != null) {
state.didChange(picked);
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: labelText,
errorText: state.errorText,
suffixIcon: const Icon(Icons.calendar_today),
),
child: Text(
state.value != null
? DateFormat.yMMMd().format(state.value!)
: 'Select date',
),
),
);
},
);
}
```
## Form with Hooks
```dart
import 'package:flutter_hooks/flutter_hooks.dart';
class HookLoginForm extends HookWidget {
const HookLoginForm({super.key});
@override
Widget build(BuildContext context) {
final formKey = useMemoized(GlobalKey<FormState>.new);
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
final emailFocus = useFocusNode();
final passwordFocus = useFocusNode();
final isLoading = useState(false);
Future<void> submit() async {
if (!formKey.currentState!.validate()) return;
isLoading.value = true;
try {
await authService.login(
email: emailController.text.trim(),
password: passwordController.text,
);
} finally {
isLoading.value = false;
}
}
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: emailController,
focusNode: emailFocus,
decoration: const InputDecoration(labelText: 'Email'),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => passwordFocus.requestFocus(),
validator: Validators.email,
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
focusNode: passwordFocus,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
onFieldSubmitted: (_) => submit(),
validator: Validators.password,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: isLoading.value ? null : submit,
child: isLoading.value
? const CircularProgressIndicator()
: const Text('Login'),
),
],
),
);
}
}
```
## Server-Side Validation
```dart
class ServerValidationForm extends StatefulWidget {
const ServerValidationForm({super.key});
@override
State<ServerValidationForm> createState() => _ServerValidationFormState();
}
class _ServerValidationFormState extends State<ServerValidationForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
Map<String, List<String>> _serverErrors = {};
String? _emailValidator(String? value) {
final clientError = Validators.email(value);
if (clientError != null) return clientError;
final serverError = _serverErrors['email'];
if (serverError != null && serverError.isNotEmpty) {
return serverError.first;
}
return null;
}
Future<void> _submit() async {
setState(() => _serverErrors = {});
if (!_formKey.currentState!.validate()) return;
try {
await api.register(email: _emailController.text);
} on ValidationException catch (e) {
setState(() => _serverErrors = e.errors);
_formKey.currentState!.validate();
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
validator: _emailValidator,
onChanged: (_) {
if (_serverErrors.containsKey('email')) {
setState(() => _serverErrors.remove('email'));
}
},
),
ElevatedButton(
onPressed: _submit,
child: const Text('Register'),
),
],
),
);
}
}
```
## Auto-Save Form
```dart
class AutoSaveForm extends StatefulWidget {
const AutoSaveForm({super.key});
@override
State<AutoSaveForm> createState() => _AutoSaveFormState();
}
class _AutoSaveFormState extends State<AutoSaveForm> {
final _formKey = GlobalKey<FormState>();
Timer? _debounce;
bool _hasChanges = false;
void _onChanged() {
setState(() => _hasChanges = true);
_debounce?.cancel();
_debounce = Timer(const Duration(seconds: 2), _autoSave);
}
Future<void> _autoSave() async {
if (!_hasChanges) return;
if (!_formKey.currentState!.validate()) return;
_formKey.currentState!.save();
await saveToServer();
setState(() => _hasChanges = false);
}
@override
void dispose() {
_debounce?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
onChanged: _onChanged,
child: Column(
children: [
if (_hasChanges)
const Text('Saving...', style: TextStyle(color: Colors.grey)),
TextFormField(
decoration: const InputDecoration(labelText: 'Title'),
onSaved: (value) => saveField('title', value),
),
TextFormField(
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 3,
onSaved: (value) => saveField('description', value),
),
],
),
);
}
}
```
## Common Keyboard Types
| Type | Usage |
|------|-------|
| `TextInputType.text` | General text |
| `TextInputType.emailAddress` | Email with @ keyboard |
| `TextInputType.phone` | Phone number pad |
| `TextInputType.number` | Numeric keyboard |
| `TextInputType.numberWithOptions(decimal: true)` | Numbers with decimal |
| `TextInputType.multiline` | Multi-line text |
| `TextInputType.url` | URL with shortcuts |
## Form Checklist
| Item | Implementation |
|------|----------------|
| GlobalKey | `GlobalKey<FormState>()` for form |
| Dispose controllers | Clean up in `dispose()` |
| Validation | Client + server-side |
| Input formatters | Phone, currency, etc. |
| Keyboard types | Match input type |
| Text actions | `textInputAction` for flow |
| Loading state | Disable during submission |
| Error display | Show below fields |
---
*Flutter and Material Design are trademarks of Google LLC.*

View File

@@ -0,0 +1,257 @@
# GoRouter Navigation
GoRouter navigation guide covering route setup, guards, deep linking, and shell routes.
## Basic Setup
```dart
import 'package:go_router/go_router.dart';
final goRouter = GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
redirect: (context, state) {
final isLoggedIn = /* check auth state */;
final isAuthRoute = state.matchedLocation.startsWith('/auth');
if (!isLoggedIn && !isAuthRoute) {
return '/auth/login';
}
if (isLoggedIn && isAuthRoute) {
return '/';
}
return null;
},
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
name: 'details',
builder: (context, state) {
final id = state.pathParameters['id']!;
final extra = state.extra as Map<String, dynamic>?;
return DetailsScreen(id: id, title: extra?['title']);
},
),
],
),
GoRoute(
path: '/auth/login',
name: 'login',
builder: (context, state) => const LoginScreen(),
),
],
);
```
### App Integration
```dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: goRouter,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
);
}
}
```
## Navigation Methods
```dart
// Navigate and replace entire stack
context.go('/details/123');
// Navigate and add to stack (can go back)
context.push('/details/123');
// Go back
context.pop();
// Go back with result
context.pop(result);
// Replace current route
context.pushReplacement('/home');
// Navigate with extra data
context.push('/details/123', extra: {'title': 'Item Title'});
// Navigate by name
context.goNamed('details', pathParameters: {'id': '123'});
context.pushNamed('details', pathParameters: {'id': '123'}, extra: data);
```
### Navigation Reference
| Method | Behavior |
|--------|----------|
| `context.go()` | Navigate, replace entire stack |
| `context.push()` | Navigate, add to stack |
| `context.pop()` | Go back one level |
| `context.pushReplacement()` | Replace current route |
| `context.goNamed()` | Navigate by route name |
| `context.canPop()` | Check if can go back |
## Shell Routes (Persistent UI)
```dart
final goRouter = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (_, __) => const HomeScreen(),
),
GoRoute(
path: '/search',
builder: (_, __) => const SearchScreen(),
),
GoRoute(
path: '/profile',
builder: (_, __) => const ProfileScreen(),
),
],
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _calculateSelectedIndex(context),
onDestinationSelected: (index) => _onItemTapped(index, context),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
if (location.startsWith('/home')) return 0;
if (location.startsWith('/search')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0: context.go('/home');
case 1: context.go('/search');
case 2: context.go('/profile');
}
}
}
```
## Query Parameters
```dart
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
return SearchScreen(query: query, page: page);
},
),
// Navigate with query params
context.go('/search?q=flutter&page=2');
context.goNamed('search', queryParameters: {'q': 'flutter', 'page': '2'});
```
## Riverpod Integration
```dart
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authProvider);
return GoRouter(
refreshListenable: authState,
redirect: (context, state) {
final isLoggedIn = authState.isAuthenticated;
final isAuthRoute = state.matchedLocation.startsWith('/auth');
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
if (isLoggedIn && isAuthRoute) return '/';
return null;
},
routes: [...],
);
});
// In app.dart
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(routerConfig: router);
}
}
```
## Error Handling
```dart
final goRouter = GoRouter(
errorBuilder: (context, state) {
return ErrorScreen(error: state.error);
},
routes: [...],
);
```
## Deep Linking
Deep links work automatically when routes are configured with path parameters:
```dart
// URL: myapp://details/123
// or: https://myapp.com/details/123
GoRoute(
path: '/details/:id',
builder: (context, state) => DetailsScreen(id: state.pathParameters['id']!),
),
```
## Best Practices
| Do | Don't |
|----|-------|
| Use named routes for maintainability | Hardcode paths everywhere |
| Use `push()` for detail screens | Use `go()` for all navigation |
| Pass simple data via `extra` | Pass complex objects via URL |
| Use redirect for auth guards | Check auth in every screen |
| Use ShellRoute for persistent UI | Rebuild nav bar in every screen |
---
*GoRouter is an open-source navigation package for Flutter.*

View File

@@ -0,0 +1,510 @@
# Localization
Internationalization (i18n) patterns using flutter_localizations and intl package for Flutter applications.
## Setup
### Dependencies
```yaml
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: true
```
### l10n Configuration
```yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
```
## ARB Files
### English (Template)
```json
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The application title"
},
"hello": "Hello",
"welcome": "Welcome, {name}!",
"@welcome": {
"description": "Welcome message with user name",
"placeholders": {
"name": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Number of items",
"placeholders": {
"count": {
"type": "int"
}
}
},
"lastUpdated": "Last updated: {date}",
"@lastUpdated": {
"description": "Last update timestamp",
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
"price": "Price: {amount}",
"@price": {
"description": "Product price",
"placeholders": {
"amount": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "$",
"decimalDigits": 2
}
}
}
},
"gender": "{gender, select, male{He} female{She} other{They}} liked this",
"@gender": {
"description": "Gender-specific message",
"placeholders": {
"gender": {
"type": "String"
}
}
}
}
```
### Chinese
```json
// lib/l10n/app_zh.arb
{
"@@locale": "zh",
"appTitle": "我的应用",
"hello": "你好",
"welcome": "欢迎,{name}",
"itemCount": "{count, plural, =0{没有项目} other{{count} 个项目}}",
"lastUpdated": "最后更新:{date}",
"price": "价格:{amount}",
"gender": "{gender, select, male{他} female{她} other{Ta}}喜欢了这个"
}
```
### Japanese
```json
// lib/l10n/app_ja.arb
{
"@@locale": "ja",
"appTitle": "マイアプリ",
"hello": "こんにちは",
"welcome": "ようこそ、{name}さん!",
"itemCount": "{count, plural, =0{アイテムなし} other{{count}件}}",
"lastUpdated": "最終更新:{date}",
"price": "価格:{amount}",
"gender": "{gender, select, male{彼} female{彼女} other{その人}}がいいねしました"
}
```
## App Configuration
```dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('zh'),
Locale('ja'),
],
locale: const Locale('en'),
home: const HomePage(),
);
}
}
```
## Using Translations
```dart
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
Text(l10n.hello),
Text(l10n.welcome('John')),
Text(l10n.itemCount(5)),
Text(l10n.lastUpdated(DateTime.now())),
Text(l10n.price(29.99)),
Text(l10n.gender('female')),
],
),
);
}
}
```
### Extension for Convenience
```dart
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
// Usage
Text(context.l10n.hello)
```
## Dynamic Locale Switching
### With Riverpod
```dart
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
final saved = ref.watch(sharedPreferencesProvider).getString('locale');
if (saved != null) {
return Locale(saved);
}
return const Locale('en');
}
void setLocale(Locale locale) {
ref.read(sharedPreferencesProvider).setString('locale', locale.languageCode);
state = locale;
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
home: const HomePage(),
);
}
}
```
### Language Selector
```dart
class LanguageSelector extends ConsumerWidget {
const LanguageSelector({super.key});
static const languages = [
(Locale('en'), 'English'),
(Locale('zh'), '中文'),
(Locale('ja'), '日本語'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeNotifierProvider);
return PopupMenuButton<Locale>(
initialValue: currentLocale,
onSelected: (locale) {
ref.read(localeNotifierProvider.notifier).setLocale(locale);
},
itemBuilder: (context) => languages.map((lang) {
return PopupMenuItem(
value: lang.$1,
child: Row(
children: [
if (currentLocale == lang.$1)
const Icon(Icons.check, size: 18),
const SizedBox(width: 8),
Text(lang.$2),
],
),
);
}).toList(),
child: const Icon(Icons.language),
);
}
}
```
## Date and Number Formatting
```dart
import 'package:intl/intl.dart';
class FormattingUtils {
static String formatDate(DateTime date, String locale) {
return DateFormat.yMMMd(locale).format(date);
}
static String formatDateTime(DateTime dateTime, String locale) {
return DateFormat.yMMMd(locale).add_jm().format(dateTime);
}
static String formatRelativeTime(DateTime dateTime, String locale) {
final now = DateTime.now();
final diff = now.difference(dateTime);
if (diff.inDays > 7) {
return DateFormat.yMMMd(locale).format(dateTime);
} else if (diff.inDays > 0) {
return '${diff.inDays}d ago';
} else if (diff.inHours > 0) {
return '${diff.inHours}h ago';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}m ago';
} else {
return 'Just now';
}
}
static String formatCurrency(double amount, String locale, {String? symbol}) {
return NumberFormat.currency(
locale: locale,
symbol: symbol,
decimalDigits: 2,
).format(amount);
}
static String formatNumber(num number, String locale) {
return NumberFormat.decimalPattern(locale).format(number);
}
static String formatPercent(double value, String locale) {
return NumberFormat.percentPattern(locale).format(value);
}
static String formatCompact(num number, String locale) {
return NumberFormat.compact(locale: locale).format(number);
}
}
```
### Usage with Locale
```dart
class FormattedContent extends StatelessWidget {
const FormattedContent({super.key});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
return Column(
children: [
Text(FormattingUtils.formatDate(DateTime.now(), locale)),
Text(FormattingUtils.formatCurrency(1234.56, locale, symbol: '\$')),
Text(FormattingUtils.formatNumber(1234567, locale)),
Text(FormattingUtils.formatPercent(0.75, locale)),
Text(FormattingUtils.formatCompact(1500000, locale)),
],
);
}
}
```
## RTL Support
```dart
class RtlAwareWidget extends StatelessWidget {
const RtlAwareWidget({super.key});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Row(
children: [
Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward),
const Expanded(child: Text('Content')),
Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: const Icon(Icons.settings),
),
],
);
}
}
```
### Directional Widgets
| Standard | Directional |
|----------|-------------|
| `EdgeInsets` | `EdgeInsetsDirectional` |
| `Padding` | `Padding` with `EdgeInsetsDirectional` |
| `Align` | `AlignmentDirectional` |
| `Positioned` | `PositionedDirectional` |
| `BorderRadius` | `BorderRadiusDirectional` |
```dart
// Use directional
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: child,
)
Container(
alignment: AlignmentDirectional.centerStart,
child: child,
)
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(8),
bottomStart: Radius.circular(8),
),
),
)
```
## Organized Translations
### Split by Feature
```
lib/
l10n/
app_en.arb # Common translations
app_zh.arb
features/
auth_en.arb # Auth feature translations
auth_zh.arb
settings_en.arb # Settings feature translations
settings_zh.arb
```
### Namespaced Keys
```json
// app_en.arb
{
"auth_login": "Login",
"auth_logout": "Logout",
"auth_forgotPassword": "Forgot Password?",
"settings_title": "Settings",
"settings_language": "Language",
"settings_theme": "Theme",
"error_network": "Network error. Please try again.",
"error_unknown": "An unknown error occurred."
}
```
## Testing
```dart
void main() {
testWidgets('shows localized text', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const HomePage(),
),
);
expect(find.text('Hello'), findsOneWidget);
});
testWidgets('switches locale', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('zh'),
home: const HomePage(),
),
),
);
expect(find.text('你好'), findsOneWidget);
});
}
```
## ARB Placeholders Reference
| Type | Format Options |
|------|----------------|
| `String` | None |
| `int` | `compact`, `compactCurrency`, `compactLong`, `compactSimpleCurrency` |
| `double` | `compact`, `compactCurrency`, `currency`, `decimalPattern`, `decimalPercentPattern`, `percentPattern`, `scientificPattern`, `simpleCurrency` |
| `DateTime` | Any `DateFormat` pattern (yMd, yMMMd, jm, etc.) |
| `num` | Same as `int` and `double` |
## Localization Checklist
| Item | Implementation |
|------|----------------|
| Dependencies | `flutter_localizations`, `intl` |
| l10n.yaml | Configure ARB paths and output |
| ARB files | Create for each supported locale |
| App config | Add delegates and supported locales |
| Generate | Run `flutter gen-l10n` |
| Use translations | `AppLocalizations.of(context)` |
| Date/number formatting | Use `intl` formatters with locale |
| RTL support | Use directional widgets |
| Persist preference | Save user's locale choice |
| Testing | Test with different locales |
---
*Flutter is a trademark of Google LLC. intl is an open-source package by the Dart team.*

View File

@@ -0,0 +1,566 @@
# Networking
Dio configuration, interceptors, error handling, and caching strategies for Flutter network requests.
## Dio Setup
```dart
import 'package:dio/dio.dart';
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
late final Dio dio;
ApiClient._internal() {
dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
dio.interceptors.addAll([
AuthInterceptor(),
LoggingInterceptor(),
RetryInterceptor(dio: dio),
]);
}
}
```
## Interceptors
### Auth Interceptor
```dart
class AuthInterceptor extends Interceptor {
final TokenStorage _tokenStorage;
AuthInterceptor({TokenStorage? tokenStorage})
: _tokenStorage = tokenStorage ?? TokenStorage();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await _tokenStorage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
final newToken = await _refreshToken();
if (newToken != null) {
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await Dio().fetch(err.requestOptions);
return handler.resolve(response);
}
} catch (e) {
await _tokenStorage.clearTokens();
}
}
handler.next(err);
}
Future<String?> _refreshToken() async {
final refreshToken = await _tokenStorage.getRefreshToken();
if (refreshToken == null) return null;
final response = await Dio().post(
'https://api.example.com/v1/auth/refresh',
data: {'refresh_token': refreshToken},
);
if (response.statusCode == 200) {
final newToken = response.data['access_token'];
await _tokenStorage.saveAccessToken(newToken);
return newToken;
}
return null;
}
}
```
### Logging Interceptor
```dart
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
debugPrint('${options.method} ${options.uri}');
if (options.data != null) {
debugPrint(' Body: ${options.data}');
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
debugPrint('${response.statusCode} ${response.requestOptions.uri}');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
debugPrint('${err.response?.statusCode} ${err.requestOptions.uri}');
debugPrint(' Error: ${err.message}');
handler.next(err);
}
}
```
### Retry Interceptor
```dart
class RetryInterceptor extends Interceptor {
final Dio dio;
final int maxRetries;
final Duration retryDelay;
RetryInterceptor({
required this.dio,
this.maxRetries = 3,
this.retryDelay = const Duration(seconds: 1),
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;
if (_shouldRetry(err) && retryCount < maxRetries) {
await Future.delayed(retryDelay * (retryCount + 1));
err.requestOptions.extra['retryCount'] = retryCount + 1;
try {
final response = await dio.fetch(err.requestOptions);
return handler.resolve(response);
} catch (e) {
return handler.next(err);
}
}
handler.next(err);
}
bool _shouldRetry(DioException err) {
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout ||
(err.response?.statusCode ?? 0) >= 500;
}
}
```
## Error Handling
### Custom Exception
```dart
sealed class ApiException implements Exception {
final String message;
final int? statusCode;
final dynamic data;
const ApiException({
required this.message,
this.statusCode,
this.data,
});
}
class NetworkException extends ApiException {
const NetworkException({super.message = 'Network connection failed'});
}
class ServerException extends ApiException {
const ServerException({
required super.message,
super.statusCode,
super.data,
});
}
class UnauthorizedException extends ApiException {
const UnauthorizedException({super.message = 'Authentication required'});
}
class ValidationException extends ApiException {
final Map<String, List<String>> errors;
const ValidationException({
required this.errors,
super.message = 'Validation failed',
});
}
```
### Error Handler
```dart
class ApiErrorHandler {
static ApiException handle(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return const NetworkException(message: 'Connection timeout');
case DioExceptionType.connectionError:
return const NetworkException(message: 'No internet connection');
case DioExceptionType.badResponse:
return _handleResponse(error.response);
case DioExceptionType.cancel:
return const ApiException(message: 'Request cancelled');
default:
return ApiException(message: error.message ?? 'Unknown error');
}
}
static ApiException _handleResponse(Response? response) {
final statusCode = response?.statusCode ?? 0;
final data = response?.data;
switch (statusCode) {
case 400:
if (data is Map && data.containsKey('errors')) {
return ValidationException(
errors: Map<String, List<String>>.from(
(data['errors'] as Map).map(
(k, v) => MapEntry(k.toString(), List<String>.from(v)),
),
),
);
}
return ServerException(
message: data?['message'] ?? 'Bad request',
statusCode: statusCode,
);
case 401:
return const UnauthorizedException();
case 403:
return const ServerException(
message: 'Access denied',
statusCode: 403,
);
case 404:
return const ServerException(
message: 'Resource not found',
statusCode: 404,
);
case 422:
return ValidationException(
errors: _parseValidationErrors(data),
);
case >= 500:
return ServerException(
message: 'Server error',
statusCode: statusCode,
);
default:
return ServerException(
message: data?['message'] ?? 'Unknown error',
statusCode: statusCode,
);
}
}
static Map<String, List<String>> _parseValidationErrors(dynamic data) {
if (data is! Map) return {};
final errors = data['errors'];
if (errors is! Map) return {};
return errors.map((k, v) => MapEntry(
k.toString(),
v is List ? v.map((e) => e.toString()).toList() : [v.toString()],
));
}
}
```
## Repository Pattern
```dart
abstract class BaseRepository {
final Dio dio;
BaseRepository(this.dio);
Future<T> safeCall<T>(Future<T> Function() call) async {
try {
return await call();
} on DioException catch (e) {
throw ApiErrorHandler.handle(e);
}
}
}
class UserRepository extends BaseRepository {
UserRepository(super.dio);
Future<User> getUser(String id) => safeCall(() async {
final response = await dio.get('/users/$id');
return User.fromJson(response.data);
});
Future<List<User>> getUsers({int page = 1, int limit = 20}) => safeCall(() async {
final response = await dio.get('/users', queryParameters: {
'page': page,
'limit': limit,
});
return (response.data['data'] as List)
.map((e) => User.fromJson(e))
.toList();
});
Future<User> updateUser(String id, Map<String, dynamic> data) => safeCall(() async {
final response = await dio.patch('/users/$id', data: data);
return User.fromJson(response.data);
});
}
```
## Caching
### Memory Cache
```dart
class CacheInterceptor extends Interceptor {
final Map<String, CacheEntry> _cache = {};
final Duration maxAge;
CacheInterceptor({this.maxAge = const Duration(minutes: 5)});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (options.method != 'GET') {
handler.next(options);
return;
}
final key = _cacheKey(options);
final cached = _cache[key];
if (cached != null && !cached.isExpired) {
return handler.resolve(Response(
requestOptions: options,
data: cached.data,
statusCode: 200,
));
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.method == 'GET') {
final key = _cacheKey(response.requestOptions);
_cache[key] = CacheEntry(
data: response.data,
expiry: DateTime.now().add(maxAge),
);
}
handler.next(response);
}
String _cacheKey(RequestOptions options) {
return '${options.uri}';
}
void invalidate(String pattern) {
_cache.removeWhere((key, _) => key.contains(pattern));
}
void clear() => _cache.clear();
}
class CacheEntry {
final dynamic data;
final DateTime expiry;
CacheEntry({required this.data, required this.expiry});
bool get isExpired => DateTime.now().isAfter(expiry);
}
```
### Disk Cache with Hive
```dart
import 'package:hive_flutter/hive_flutter.dart';
class DiskCacheInterceptor extends Interceptor {
static const String _boxName = 'api_cache';
final Duration maxAge;
DiskCacheInterceptor({this.maxAge = const Duration(hours: 1)});
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
if (options.method != 'GET') {
handler.next(options);
return;
}
final box = await Hive.openBox(_boxName);
final key = _cacheKey(options);
final cached = box.get(key);
if (cached != null) {
final entry = CachedResponse.fromJson(cached);
if (!entry.isExpired) {
return handler.resolve(Response(
requestOptions: options,
data: entry.data,
statusCode: 200,
));
}
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
if (response.requestOptions.method == 'GET') {
final box = await Hive.openBox(_boxName);
final key = _cacheKey(response.requestOptions);
await box.put(key, CachedResponse(
data: response.data,
expiry: DateTime.now().add(maxAge),
).toJson());
}
handler.next(response);
}
String _cacheKey(RequestOptions options) => options.uri.toString();
}
class CachedResponse {
final dynamic data;
final DateTime expiry;
CachedResponse({required this.data, required this.expiry});
bool get isExpired => DateTime.now().isAfter(expiry);
factory CachedResponse.fromJson(Map<String, dynamic> json) {
return CachedResponse(
data: json['data'],
expiry: DateTime.parse(json['expiry']),
);
}
Map<String, dynamic> toJson() => {
'data': data,
'expiry': expiry.toIso8601String(),
};
}
```
## Riverpod Integration
```dart
@riverpod
Dio dio(Ref ref) {
return ApiClient().dio;
}
@riverpod
UserRepository userRepository(Ref ref) {
return UserRepository(ref.watch(dioProvider));
}
@riverpod
Future<User> user(Ref ref, String id) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUser(id);
}
@riverpod
class Users extends _$Users {
@override
Future<List<User>> build() => _fetch();
Future<List<User>> _fetch() async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUsers();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(_fetch);
}
}
```
## Request Cancellation
```dart
class SearchRepository extends BaseRepository {
CancelToken? _searchToken;
SearchRepository(super.dio);
Future<List<SearchResult>> search(String query) async {
_searchToken?.cancel();
_searchToken = CancelToken();
return safeCall(() async {
final response = await dio.get(
'/search',
queryParameters: {'q': query},
cancelToken: _searchToken,
);
return (response.data as List)
.map((e) => SearchResult.fromJson(e))
.toList();
});
}
}
```
## Common Patterns
| Pattern | Usage |
|---------|-------|
| Singleton client | Single Dio instance across app |
| Interceptor chain | Auth → Retry → Cache → Logging |
| Repository layer | Abstract API from business logic |
| Error mapping | Convert DioException to app exceptions |
| Cancel tokens | Debounce/cancel previous requests |
| Cache invalidation | Clear cache on mutations |
## Networking Checklist
| Item | Implementation |
|------|----------------|
| Base configuration | Timeouts, headers, base URL |
| Auth handling | Token injection, refresh on 401 |
| Error handling | Typed exceptions, user messages |
| Retry logic | Exponential backoff for transient errors |
| Request logging | Debug interceptor |
| Caching | Memory/disk cache for GET requests |
| Cancellation | Cancel tokens for search/debounce |
---
*Dio is an open-source package by the Flutter China community.*

View File

@@ -0,0 +1,306 @@
# Performance Optimization
Flutter performance guide covering profiling, const optimization, and DevTools analysis.
## Profiling Commands
```bash
# Run in profile mode (required for accurate measurements)
flutter run --profile
# Analyze code issues
flutter analyze
# Launch DevTools
flutter pub global activate devtools
flutter pub global run devtools
# Build release for testing
flutter build apk --release
flutter build ios --release
```
## Const Widget Optimization
The most important optimization for preventing unnecessary rebuilds:
```dart
// BAD - Creates new objects every build
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16), // New object each time
child: Text('Hello'), // New widget each time
);
}
// GOOD - Const prevents rebuilds
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
);
}
```
### Extracting Const Widgets
```dart
// BAD - Inline static content
class MyScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(Icons.star, size: 48),
Text('Welcome'),
Text('Description text here'),
],
);
}
}
// GOOD - Extract to const classes
class MyScreen extends StatelessWidget {
const MyScreen({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
_Header(),
_Description(),
],
);
}
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return const Column(
children: [
Icon(Icons.star, size: 48),
Text('Welcome'),
],
);
}
}
```
## Selective Provider Watching
```dart
// BAD - Rebuilds on any user change
class UserAvatar extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
);
}
}
// GOOD - Only rebuilds when avatarUrl changes
class UserAvatar extends ConsumerWidget {
const UserAvatar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final avatarUrl = ref.watch(userProvider.select((u) => u.avatarUrl));
return CircleAvatar(
backgroundImage: NetworkImage(avatarUrl),
);
}
}
```
## RepaintBoundary
Isolate expensive widgets to prevent unnecessary repaints:
```dart
// Isolate complex animated widgets
RepaintBoundary(
child: ComplexAnimatedWidget(),
)
// Isolate frequently updating widgets
RepaintBoundary(
child: StreamBuilder<int>(
stream: counterStream,
builder: (context, snapshot) => Text('${snapshot.data}'),
),
)
```
## List Optimization
```dart
// BAD - Builds all items upfront
ListView(
children: items.map((item) => ItemWidget(item: item)).toList(),
)
// GOOD - Lazy loading with builder
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ItemWidget(
key: ValueKey(items[index].id),
item: items[index],
);
},
)
// For heterogeneous content
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) => ItemWidget(item: items[index]),
)
```
## Image Optimization
```dart
// Use cached_network_image for network images
CachedNetworkImage(
imageUrl: url,
placeholder: (_, __) => const ShimmerPlaceholder(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
memCacheWidth: 200,
memCacheHeight: 200,
)
// Resize images in memory
Image.network(
url,
cacheWidth: 200, // Decode at smaller size
cacheHeight: 200, // Saves memory
)
// Precache images
precacheImage(NetworkImage(url), context);
```
## Heavy Computation
```dart
// BAD - Blocks UI thread
void processData() {
final result = heavyComputation(data); // UI freezes
updateUI(result);
}
// GOOD - Run in isolate
Future<void> processData() async {
final result = await compute(heavyComputation, data);
updateUI(result);
}
// For multiple operations
Future<void> processMultiple() async {
final results = await Future.wait([
compute(process1, data1),
compute(process2, data2),
compute(process3, data3),
]);
}
```
## Animation Performance
```dart
// Use AnimatedBuilder for custom animations
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Transform.rotate(
angle: controller.value * 2 * pi,
child: child, // Child not rebuilt
);
},
child: const ExpensiveWidget(),
)
// Prefer implicit animations for simple cases
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: expanded ? 200 : 100,
child: const Content(),
)
```
## DevTools Analysis
### Key Metrics
| Metric | Target | Action if Exceeded |
|--------|--------|-------------------|
| Frame time | < 16ms (60fps) | Profile build/paint |
| Build time | < 8ms | Add const, extract widgets |
| Paint time | < 8ms | Add RepaintBoundary |
| Memory | Stable | Check for leaks |
### Common Issues
| Issue | Symptom | Solution |
|-------|---------|----------|
| Expensive builds | High build time | Extract const widgets |
| Excessive repaints | High paint time | Add RepaintBoundary |
| Memory leaks | Growing memory | Dispose controllers |
| Jank | Dropped frames | Use compute() |
## Performance Checklist
| Check | Solution |
|-------|----------|
| Unnecessary rebuilds | Add `const`, use `select()` |
| Large lists | Use `ListView.builder` |
| Image loading | Use `cached_network_image` |
| Heavy computation | Use `compute()` |
| Jank in animations | Use `RepaintBoundary` |
| Memory leaks | Dispose controllers, cancel subscriptions |
| Network calls | Cache responses, debounce requests |
| Startup time | Defer initialization, lazy loading |
## Dispose Pattern
```dart
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final TextEditingController _controller;
late final StreamSubscription _subscription;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_subscription = stream.listen(handleData);
}
@override
void dispose() {
_controller.dispose();
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) => Container();
}
```
---
*Flutter and DevTools are trademarks of Google LLC.*

View File

@@ -0,0 +1,417 @@
# Platform Integration
Flutter platform-specific implementations for iOS, Android, Web, and Desktop.
## Platform Detection
```dart
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
bool get isIOS => !kIsWeb && Platform.isIOS;
bool get isAndroid => !kIsWeb && Platform.isAndroid;
bool get isWeb => kIsWeb;
bool get isDesktop => !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
bool get isMobile => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
```
## Adaptive Widgets
### Platform-Aware Components
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class AdaptiveButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
const AdaptiveButton({
super.key,
required this.label,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoButton.filled(
onPressed: onPressed,
child: Text(label),
);
}
return ElevatedButton(
onPressed: onPressed,
child: Text(label),
);
}
}
```
### Adaptive Dialog
```dart
Future<bool?> showAdaptiveConfirmDialog(
BuildContext context, {
required String title,
required String content,
}) async {
if (Platform.isIOS) {
return showCupertinoDialog<bool>(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: Text(content),
actions: [
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
],
),
);
}
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
}
```
### Adaptive Scaffold
```dart
class AdaptiveScaffold extends StatelessWidget {
final String title;
final Widget body;
final List<Widget>? actions;
const AdaptiveScaffold({
super.key,
required this.title,
required this.body,
this.actions,
});
@override
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(title),
trailing: actions != null
? Row(mainAxisSize: MainAxisSize.min, children: actions!)
: null,
),
child: SafeArea(child: body),
);
}
return Scaffold(
appBar: AppBar(title: Text(title), actions: actions),
body: body,
);
}
}
```
## Platform Channels
### Method Channel (Dart Side)
```dart
import 'package:flutter/services.dart';
class NativeBridge {
static const _channel = MethodChannel('com.example.app/native');
static Future<String> getPlatformVersion() async {
final version = await _channel.invokeMethod<String>('getPlatformVersion');
return version ?? 'Unknown';
}
static Future<void> triggerHaptic() async {
await _channel.invokeMethod('triggerHaptic');
}
static Future<Map<String, dynamic>> getDeviceInfo() async {
final result = await _channel.invokeMethod<Map>('getDeviceInfo');
return Map<String, dynamic>.from(result ?? {});
}
}
```
### iOS Implementation (Swift)
```swift
// ios/Runner/AppDelegate.swift
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "com.example.app/native",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { (call, result) in
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "triggerHaptic":
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
result(nil)
case "getDeviceInfo":
result([
"model": UIDevice.current.model,
"name": UIDevice.current.name,
"systemVersion": UIDevice.current.systemVersion
])
default:
result(FlutterMethodNotImplemented)
}
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
```
### Android Implementation (Kotlin)
```kotlin
// android/app/src/main/kotlin/.../MainActivity.kt
package com.example.app
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.app/native"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${Build.VERSION.RELEASE}")
}
"triggerHaptic" -> {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(
VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)
)
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(50)
}
result.success(null)
}
"getDeviceInfo" -> {
result.success(mapOf(
"model" to Build.MODEL,
"manufacturer" to Build.MANUFACTURER,
"version" to Build.VERSION.RELEASE
))
}
else -> result.notImplemented()
}
}
}
}
```
## iOS-Specific Configuration
### Info.plist Permissions
```xml
<!-- ios/Runner/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to save images</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to show nearby places</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice recording</string>
```
### iOS App Icons and Launch Screen
```
ios/Runner/Assets.xcassets/
├── AppIcon.appiconset/
│ ├── Contents.json
│ └── Icon-App-*.png
└── LaunchImage.imageset/
├── Contents.json
└── LaunchImage*.png
```
## Android-Specific Configuration
### AndroidManifest.xml Permissions
```xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="My App"
android:icon="@mipmap/ic_launcher">
<!-- ... -->
</application>
</manifest>
```
### Build Gradle Configuration
```groovy
// android/app/build.gradle
android {
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
multiDexEnabled true
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
```
## Web-Specific
### Conditional Imports
```dart
// lib/services/storage_service.dart
export 'storage_service_stub.dart'
if (dart.library.io) 'storage_service_native.dart'
if (dart.library.html) 'storage_service_web.dart';
```
```dart
// lib/services/storage_service_web.dart
import 'dart:html' as html;
class StorageService {
void save(String key, String value) {
html.window.localStorage[key] = value;
}
String? load(String key) {
return html.window.localStorage[key];
}
}
```
### Web Index Configuration
```html
<!-- web/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="manifest" href="manifest.json">
<link rel="icon" type="image/png" href="favicon.png"/>
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
```
## Platform-Specific Styling
```dart
ThemeData get theme {
final baseTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
);
if (Platform.isIOS) {
return baseTheme.copyWith(
// iOS-style page transitions
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
);
}
return baseTheme;
}
```
## Platform Reference
| Feature | iOS | Android | Web |
|---------|-----|---------|-----|
| Navigation | Cupertino style | Material style | URL-based |
| Haptics | UIFeedbackGenerator | Vibrator | Not available |
| Storage | NSUserDefaults | SharedPreferences | localStorage |
| Deep links | Universal Links | App Links | URL routing |
| Notifications | APNs | FCM | Web Push |
---
*Flutter, iOS, Android, and their respective logos are trademarks of Google LLC and Apple Inc.*

View File

@@ -0,0 +1,274 @@
# Project Structure
Flutter project architecture guide covering feature-based structure, dependencies, and entry point setup.
## Feature-Based Structure
```
lib/
├── main.dart # Entry point
├── app.dart # App widget, MaterialApp.router
├── core/
│ ├── constants/
│ │ ├── app_colors.dart
│ │ ├── app_strings.dart
│ │ └── app_sizes.dart
│ ├── theme/
│ │ ├── app_theme.dart
│ │ └── text_styles.dart
│ ├── utils/
│ │ ├── extensions.dart
│ │ └── validators.dart
│ └── errors/
│ └── failures.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository_impl.dart
│ │ │ └── datasources/
│ │ │ ├── auth_remote_datasource.dart
│ │ │ └── auth_local_datasource.dart
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── user.dart
│ │ │ ├── repositories/
│ │ │ │ └── auth_repository.dart
│ │ │ └── usecases/
│ │ │ ├── login.dart
│ │ │ └── logout.dart
│ │ ├── presentation/
│ │ │ ├── screens/
│ │ │ │ ├── login_screen.dart
│ │ │ │ └── register_screen.dart
│ │ │ └── widgets/
│ │ │ └── auth_form.dart
│ │ └── providers/
│ │ └── auth_provider.dart
│ └── home/
│ ├── data/
│ ├── domain/
│ ├── presentation/
│ └── providers/
├── shared/
│ ├── widgets/
│ │ ├── buttons/
│ │ │ └── primary_button.dart
│ │ ├── inputs/
│ │ │ └── text_input.dart
│ │ └── cards/
│ │ └── info_card.dart
│ ├── services/
│ │ ├── api_service.dart
│ │ └── storage_service.dart
│ └── models/
│ └── api_response.dart
└── routes/
└── app_router.dart
```
## Feature Layer Responsibilities
| Layer | Responsibility |
|-------|----------------|
| **data/** | API calls, local storage, DTOs, repository implementations |
| **domain/** | Business logic, entities, abstract repositories, use cases |
| **presentation/** | UI screens, widgets, view logic |
| **providers/** | Riverpod providers or Bloc definitions |
## pubspec.yaml Essentials
```yaml
name: my_app
description: A Flutter application.
version: 1.0.0+1
publish_to: 'none'
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# State Management (choose one)
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
# OR
flutter_bloc: ^8.1.0
# Navigation
go_router: ^14.0.0
# Networking
dio: ^5.4.0
# Code Generation
freezed_annotation: ^2.4.0
json_annotation: ^4.9.0
# Storage
shared_preferences: ^2.2.0
hive_flutter: ^1.1.0
# Utilities
flutter_hooks: ^0.20.0
cached_network_image: ^3.3.0
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
# Code Generation
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
freezed: ^2.5.0
json_serializable: ^6.8.0
# Linting
flutter_lints: ^4.0.0
# Testing
bloc_test: ^9.1.0
mocktail: ^1.0.0
flutter:
uses-material-design: true
```
## Main Entry Point
```dart
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize services
await Hive.initFlutter();
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
```
```dart
// app.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
title: 'My App',
routerConfig: router,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
);
}
}
```
## Router Provider
```dart
// routes/app_router.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
redirect: (context, state) {
// Auth guard logic
return null;
},
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
// Add more routes
],
);
});
```
## Environment Configuration
```dart
// core/constants/environment.dart
enum Environment { dev, staging, prod }
class EnvConfig {
static Environment current = Environment.dev;
static String get baseUrl {
switch (current) {
case Environment.dev:
return 'https://dev-api.example.com';
case Environment.staging:
return 'https://staging-api.example.com';
case Environment.prod:
return 'https://api.example.com';
}
}
}
```
## Dependency Injection with Riverpod
```dart
// shared/services/api_service.dart
final apiServiceProvider = Provider<ApiService>((ref) {
final dio = Dio(BaseOptions(
baseUrl: EnvConfig.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
// Add interceptors
dio.interceptors.add(AuthInterceptor(ref));
dio.interceptors.add(LogInterceptor(responseBody: true));
return ApiService(dio);
});
// features/auth/providers/auth_provider.dart
final authRepositoryProvider = Provider<AuthRepository>((ref) {
final api = ref.watch(apiServiceProvider);
final storage = ref.watch(storageServiceProvider);
return AuthRepositoryImpl(api: api, storage: storage);
});
```
## Best Practices
| Practice | Description |
|----------|-------------|
| Feature isolation | Each feature is self-contained |
| Dependency inversion | Domain depends on abstractions |
| Single responsibility | One class, one purpose |
| Naming conventions | Clear, descriptive names |
| Barrel exports | One index.dart per folder |
---
*Flutter is a trademark of Google LLC.*

View File

@@ -0,0 +1,232 @@
# Riverpod State Management
Riverpod 2.0 state management guide covering provider types, notifier patterns, and widget integration.
## Provider Types
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Simple computed value
final greetingProvider = Provider<String>((ref) {
final name = ref.watch(userNameProvider);
return 'Hello, $name';
});
// Simple mutable state
final counterProvider = StateProvider<int>((ref) => 0);
// Async state (API calls)
final usersProvider = FutureProvider<List<User>>((ref) async {
final api = ref.read(apiProvider);
return api.getUsers();
});
// Stream state (real-time)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return ref.read(chatServiceProvider).messagesStream;
});
```
### Provider Type Reference
| Provider | Use Case |
|----------|----------|
| `Provider` | Computed/derived values, dependency injection |
| `StateProvider` | Simple mutable state (counter, toggle) |
| `FutureProvider` | Async operations (one-time fetch) |
| `StreamProvider` | Real-time data streams |
| `NotifierProvider` | Complex state with methods |
| `AsyncNotifierProvider` | Async state with methods |
## Notifier Pattern (Riverpod 2.0)
### Synchronous Notifier
```dart
@riverpod
class TodoList extends _$TodoList {
@override
List<Todo> build() => [];
void add(Todo todo) {
state = [...state, todo];
}
void toggle(String id) {
state = [
for (final todo in state)
if (todo.id == id)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
void remove(String id) {
state = state.where((t) => t.id != id).toList();
}
}
```
### Async Notifier
```dart
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
return ref.read(apiProvider).getCurrentUser();
}
Future<void> updateName(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = await ref.read(apiProvider).updateUser(name: name);
return updated;
});
}
Future<void> refresh() async {
ref.invalidateSelf();
await future;
}
}
```
## Usage in Widgets
### ConsumerWidget (Recommended)
```dart
class TodoScreen extends ConsumerWidget {
const TodoScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
key: ValueKey(todo.id),
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
),
);
},
);
}
}
```
### Selective Rebuilds with select
```dart
class UserAvatar extends ConsumerWidget {
const UserAvatar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Only rebuilds when avatarUrl changes
final avatarUrl = ref.watch(userProvider.select((u) => u?.avatarUrl));
return CircleAvatar(
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null,
);
}
}
```
### Async State Handling
```dart
class UserProfileScreen extends ConsumerWidget {
const UserProfileScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProfileProvider);
return userAsync.when(
data: (user) => UserProfileContent(user: user),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => ErrorView(
message: err.toString(),
onRetry: () => ref.invalidate(userProfileProvider),
),
);
}
}
```
### Consumer for Scoped Rebuilds
```dart
class MyScreen extends StatelessWidget {
const MyScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Static content'),
Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
},
),
],
);
}
}
```
## Provider Modifiers
```dart
// Auto-dispose when no longer used
@riverpod
class AutoDisposeExample extends _$AutoDisposeExample {
@override
String build() => 'value';
}
// Family - parameterized providers
@riverpod
Future<User> userById(UserByIdRef ref, String id) async {
return ref.read(apiProvider).getUser(id);
}
// Usage
final user = ref.watch(userByIdProvider('123'));
```
## Best Practices
| Do | Don't |
|----|-------|
| Use `ref.watch()` in build | Use `ref.watch()` in callbacks |
| Use `ref.read()` in callbacks | Use `ref.read()` in build |
| Use `select()` for granular rebuilds | Watch entire state unnecessarily |
| Create new state instances | Mutate state directly |
| Use `AsyncValue.guard()` for errors | Catch errors manually |
## Quick Reference
| Method | When to Use |
|--------|-------------|
| `ref.watch()` | In build method, rebuilds on change |
| `ref.read()` | In callbacks, one-time read |
| `ref.listen()` | Side effects on change |
| `ref.invalidate()` | Force provider refresh |
| `ref.refresh()` | Invalidate and get new value |
---
*Riverpod is an open-source state management library by Remi Rousselet.*

View File

@@ -0,0 +1,364 @@
# Testing Strategies
Flutter testing guide covering widget tests, unit tests, integration tests, and mocking patterns.
## Test Types
| Type | Purpose | Speed | Scope |
|------|---------|-------|-------|
| Unit tests | Business logic, utilities | Fast | Single function/class |
| Widget tests | UI components | Medium | Single widget |
| Integration tests | Full user flows | Slow | Multiple screens |
## Widget Tests
### Basic Widget Test
```dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Counter increments when button tapped', (tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterScreen()));
// Verify initial state
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the increment button
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify state changed
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
```
### Testing with Riverpod
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('displays user name from provider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWithValue(
AsyncValue.data(User(name: 'Test User')),
),
],
child: const MaterialApp(home: UserScreen()),
),
);
expect(find.text('Test User'), findsOneWidget);
});
testWidgets('shows loading indicator', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWithValue(const AsyncValue.loading()),
],
child: const MaterialApp(home: UserScreen()),
),
);
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
testWidgets('shows error message', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWithValue(
AsyncValue.error('Network error', StackTrace.current),
),
],
child: const MaterialApp(home: UserScreen()),
),
);
expect(find.text('Network error'), findsOneWidget);
});
}
```
### Testing with Bloc
```dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockCounterBloc extends MockBloc<CounterEvent, CounterState>
implements CounterBloc {}
void main() {
late MockCounterBloc mockBloc;
setUp(() {
mockBloc = MockCounterBloc();
});
testWidgets('displays current count', (tester) async {
when(() => mockBloc.state).thenReturn(const CounterState(value: 42));
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<CounterBloc>.value(
value: mockBloc,
child: const CounterScreen(),
),
),
);
expect(find.text('42'), findsOneWidget);
});
testWidgets('calls increment on button tap', (tester) async {
when(() => mockBloc.state).thenReturn(const CounterState(value: 0));
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<CounterBloc>.value(
value: mockBloc,
child: const CounterScreen(),
),
),
);
await tester.tap(find.byIcon(Icons.add));
verify(() => mockBloc.add(CounterIncremented())).called(1);
});
}
```
## Bloc Tests
```dart
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
});
group('UserBloc', () {
blocTest<UserBloc, UserState>(
'emits loading then success when user loaded',
setUp: () {
when(() => mockRepository.getUser())
.thenAnswer((_) async => User(name: 'Test'));
},
build: () => UserBloc(repository: mockRepository),
act: (bloc) => bloc.add(UserRequested()),
expect: () => [
const UserState(status: UserStatus.loading),
UserState(status: UserStatus.success, user: User(name: 'Test')),
],
);
blocTest<UserBloc, UserState>(
'emits loading then failure when error occurs',
setUp: () {
when(() => mockRepository.getUser())
.thenThrow(Exception('Network error'));
},
build: () => UserBloc(repository: mockRepository),
act: (bloc) => bloc.add(UserRequested()),
expect: () => [
const UserState(status: UserStatus.loading),
isA<UserState>()
.having((s) => s.status, 'status', UserStatus.failure),
],
);
});
}
```
## Unit Tests
```dart
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Validator', () {
test('returns error for empty email', () {
expect(Validator.email(''), 'Email is required');
});
test('returns error for invalid email', () {
expect(Validator.email('invalid'), 'Invalid email format');
});
test('returns null for valid email', () {
expect(Validator.email('test@example.com'), isNull);
});
});
group('Calculator', () {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
test('adds two numbers', () {
expect(calculator.add(2, 3), 5);
});
test('throws on division by zero', () {
expect(() => calculator.divide(10, 0), throwsArgumentError);
});
});
}
```
## Mocking with Mocktail
```dart
import 'package:mocktail/mocktail.dart';
// Create mock classes
class MockApiService extends Mock implements ApiService {}
class MockStorageService extends Mock implements StorageService {}
// Register fallback values for complex types
setUpAll(() {
registerFallbackValue(User(name: 'fallback'));
});
void main() {
late MockApiService mockApi;
setUp(() {
mockApi = MockApiService();
});
test('fetches user from API', () async {
// Arrange
when(() => mockApi.getUser(any()))
.thenAnswer((_) async => User(name: 'Test'));
// Act
final repository = UserRepository(api: mockApi);
final user = await repository.getUser('123');
// Assert
expect(user.name, 'Test');
verify(() => mockApi.getUser('123')).called(1);
});
}
```
## Integration Tests
```dart
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('complete login flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Navigate to login
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Enter credentials
await tester.enterText(
find.byKey(const Key('email_field')),
'test@example.com',
);
await tester.enterText(
find.byKey(const Key('password_field')),
'password123',
);
// Submit form
await tester.tap(find.text('Sign In'));
await tester.pumpAndSettle();
// Verify navigation to home
expect(find.text('Welcome'), findsOneWidget);
});
}
```
Run integration tests:
```bash
flutter test integration_test/app_test.dart
```
## Test Helpers
```dart
// test/helpers/pump_app.dart
extension PumpApp on WidgetTester {
Future<void> pumpApp(Widget widget, {List<Override>? overrides}) {
return pumpWidget(
ProviderScope(
overrides: overrides ?? [],
child: MaterialApp(
home: widget,
),
),
);
}
}
// Usage
await tester.pumpApp(const MyWidget());
```
## Golden Tests
```dart
testWidgets('matches golden', (tester) async {
await tester.pumpWidget(const MaterialApp(home: MyWidget()));
await expectLater(
find.byType(MyWidget),
matchesGoldenFile('goldens/my_widget.png'),
);
});
```
Update goldens:
```bash
flutter test --update-goldens
```
## Testing Checklist
| Test Type | What to Test |
|-----------|--------------|
| Widget tests | UI rendering, user interactions, state changes |
| Bloc tests | Event → state transitions, async operations |
| Unit tests | Validators, formatters, utilities, models |
| Integration tests | Critical user flows, navigation |
---
*Flutter and flutter_test are trademarks of Google LLC.*

View File

@@ -0,0 +1,233 @@
# Widget Patterns
Flutter widget best practices covering const optimization, responsive layouts, hooks, and sliver patterns.
## Optimized Widget Pattern
Always use `const` constructors for static widgets to prevent unnecessary rebuilds:
```dart
class OptimizedCard extends StatelessWidget {
final String title;
final VoidCallback onTap;
const OptimizedCard({
super.key,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
),
),
);
}
}
```
### Extracting Const Widgets
```dart
class MyScreen extends StatelessWidget {
const MyScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: const [
_Header(),
_Body(),
_Footer(),
],
);
}
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return const Text('Header');
}
}
```
## Responsive Layout
```dart
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
static const double mobileBreakpoint = 650;
static const double desktopBreakpoint = 1100;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= desktopBreakpoint) return desktop;
if (constraints.maxWidth >= mobileBreakpoint) return tablet ?? mobile;
return mobile;
},
);
}
}
```
### Breakpoint Reference
| Type | Width | Usage |
|------|-------|-------|
| Mobile | < 650pt | Single column, bottom nav |
| Tablet | 650-1100pt | Two columns, side nav optional |
| Desktop | > 1100pt | Multi-column, persistent nav |
## Custom Hooks (flutter_hooks)
```dart
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterWidget extends HookWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context) {
final counter = useState(0);
final controller = useTextEditingController();
final isMounted = useIsMounted();
useEffect(() {
debugPrint('Widget mounted');
return () {
debugPrint('Widget disposed');
};
}, const []);
return Column(
children: [
Text('Count: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
TextField(controller: controller),
],
);
}
}
```
### Common Hooks
| Hook | Purpose |
|------|---------|
| `useState` | Local state management |
| `useEffect` | Side effects with cleanup |
| `useMemoized` | Expensive computation caching |
| `useTextEditingController` | Text field controller |
| `useAnimationController` | Animation controller |
| `useFocusNode` | Focus management |
| `useIsMounted` | Check if widget is mounted |
## Sliver Patterns
```dart
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Title'),
background: Image.network(imageUrl, fit: BoxFit.cover),
),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
key: ValueKey(items[index].id),
title: Text(items[index].title),
),
childCount: items.length,
),
),
),
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Footer'),
),
),
],
)
```
### Sliver Types
| Sliver | Usage |
|--------|-------|
| `SliverAppBar` | Collapsing app bar |
| `SliverList` | Lazy list |
| `SliverGrid` | Lazy grid |
| `SliverToBoxAdapter` | Single non-sliver widget |
| `SliverPadding` | Add padding to sliver |
| `SliverFillRemaining` | Fill remaining space |
## Key Usage Patterns
```dart
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: ValueKey(item.id),
child: ListTile(
key: ValueKey('tile_${item.id}'),
title: Text(item.title),
),
);
},
)
```
| Key Type | When to Use |
|----------|-------------|
| `ValueKey` | Unique ID available |
| `ObjectKey` | Object identity matters |
| `UniqueKey` | Force rebuild |
| `GlobalKey` | Access state across tree |
## Optimization Checklist
| Pattern | Implementation |
|---------|----------------|
| const widgets | Add `const` to static widgets |
| Keys | Use `ValueKey` for list items |
| Select | `ref.watch(provider.select(...))` |
| RepaintBoundary | Isolate expensive repaints |
| ListView.builder | Lazy loading for lists |
| const constructors | Always use when possible |
---
*Flutter and Material Design are trademarks of Google LLC.*