Flutter state management and widget design
Skill hamzabellouch/agent-skills/Mobile Development/flutter-state-management-and-widget-design
Comprehensive collection of 380+ production-ready Agent Skills (26 domains) conforming to the Agent Skills Standard, featuring native auto-discovery for Antigravity, Gemini CLI, Claude Code, Cursor, and Codex.
npx -y skills add hamzabellouch/agent-skills --skill flutter-state-management-and-widget-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Production-grade Flutter state management (Riverpod 2.x, BLoC/Cubit), widget tree optimization, immutable state modeling, declarative routing (GoRouter), clean architecture, and custom rendering performance patterns. Use when architecting or building scalable Flutter applications.
SKILL.md
7.7 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it
Flutter State Management & Widget Design
A comprehensive guide for building scalable, high-performance, and maintainable Flutter applications using Riverpod 2.x, BLoC/Cubit, GoRouter, and performance-first widget tree architecture.
1. State Management Architecture: Riverpod 2.x vs BLoC
Standard Decision Matrix
| Metric / Requirement | Riverpod 2.x (Notifier / AsyncNotifier) | BLoC / Cubit |
|---|---|---|
| Compile-time Safety | Highest (No ProviderNotFoundException) | High (Depends on BuildContext hierarchy) |
| Testability | Easy override via ProviderContainer | Easy via bloc_test |
| Dependency Injection | Built-in via Provider composition | Requires RepositoryProvider / get_it |
| Learning Curve | Moderate | Moderate-High (Event/State boilerplate) |
| Use Case Recommendation | Modern greenfield Flutter apps | Corporate/Enterprise apps with strict event logging |
2. Riverpod 2.x Production Pattern (AsyncNotifier)
Always use @riverpod code generation or AsyncNotifierProvider for async state to handle AsyncValue (data, loading, error) cleanly.
Immutable State & Controller Implementation
import 'package:flutter_riverpod/flutter_riverpod.dart';
// --- Domain Model ---
class UserProfile {
final String id;
final String name;
final String email;
const UserProfile({
required this.id,
required this.name,
required this.email,
});
UserProfile copyWith({String? name, String? email}) {
return UserProfile(
id: id,
name: name ?? this.name,
email: email ?? this.email,
);
}
}
// --- Repository Interface ---
abstract class UserRepository {
Future<UserProfile> fetchProfile(String userId);
Future<void> updateName(String userId, String newName);
}
// --- Repository Provider ---
final userRepositoryProvider = Provider<UserRepository>((ref) {
throw UnimplementedError('Override in ProviderScope');
});
// --- AsyncNotifier Controller ---
final userProfileControllerProvider =
AsyncNotifierProvider.family<UserProfileController, UserProfile, String>(
UserProfileController.new,
);
class UserProfileController extends FamilyAsyncNotifier<UserProfile, String> {
@override
Future<UserProfile> build(String arg) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchProfile(arg);
}
Future<void> updateName(String newName) async {
final repository = ref.read(userRepositoryProvider);
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await repository.updateName(arg, newName);
final current = state.valueOrNull ?? await repository.fetchProfile(arg);
return current.copyWith(name: newName);
});
}
}
3. Widget Tree Optimization Guidelines
Rule 1: Use const Constructors Everywhere
const widgets avoid unnecessary element updates and skip re-building when parent widgets re-render.
Rule 2: Rebuild Granularly with select
Do not watch an entire state object if you only need a single field.
// ❌ BAD: Rebuilds every time ANY field in UserProfile changes
class UserHeader extends ConsumerWidget {
final String userId;
const UserHeader({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncProfile = ref.watch(userProfileControllerProvider(userId));
return Text(asyncProfile.valueOrNull?.name ?? '');
}
}
// ✅ GOOD: Rebuilds ONLY when 'name' specifically changes
class UserHeaderOptimized extends ConsumerWidget {
final String userId;
const UserHeaderOptimized({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(
userProfileControllerProvider(userId).select((asyncVal) => asyncVal.valueOrNull?.name),
);
return Text(name ?? '');
}
}
Rule 3: Isolate Heavy Animations with RepaintBoundary
Wrap isolated, high-frequency animated subtrees in RepaintBoundary to prevent rasterizing the whole screen on every tick.
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
const StaticBackgroundView(),
RepaintBoundary(
child: CustomParticleAnimation(),
),
],
),
);
}
4. Declarative Routing with GoRouter
Implement type-safe, declarative routes with authentication guards.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final authStateProvider = StateProvider<bool>((ref) => false);
final routerProvider = Provider<GoRouter>((ref) {
final isAuthenticated = ref.watch(authStateProvider);
return GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final loggingIn = state.matchedLocation == '/login';
if (!isAuthenticated && !loggingIn) return '/login';
if (isAuthenticated && loggingIn) return '/home';
return null;
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
],
);
});
5. Critical Anti-Patterns & Pitfalls
❌ Anti-Pattern 1: Using BuildContext Across Async Gaps
Bad:
void onSubmit(BuildContext context) async {
await fetchData();
Navigator.of(context).pop(); // Dangerous! Context may no longer be mounted.
}
Good:
void onSubmit(BuildContext context) async {
await fetchData();
if (!context.mounted) return;
Navigator.of(context).pop();
}
❌ Anti-Pattern 2: Instantiating Controllers Inside build()
Bad:
Widget build(BuildContext context) {
final controller = AnimationController(vsync: this, duration: ...); // Memory leak! Re-created on rebuild!
return ...
}
Good: Initialize long-lived controllers in initState() / disposal in dispose(), or use flutter_hooks (useAnimationController).
❌ Anti-Pattern 3: Over-nesting Layout Widgets
Avoid deep nesting of Container, Padding, and SizedBox. Prefer explicit single-pass layout widgets (Padding, Gap, Flex, CustomScrollView with SliverList).
6. Unit & Widget Testing Patterns
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mockito/mockito.dart';
void main() {
testWidgets('UserHeader displays name from controller', (tester) async {
final mockRepository = MockUserRepository();
when(mockRepository.fetchProfile('123')).thenAnswer(
(_) async => const UserProfile(id: '123', name: 'Alice', email: '[email protected]'),
);
await tester.pumpWidget(
ProviderScope(
overrides: [
userRepositoryProvider.overrideWithValue(mockRepository),
],
child: const MaterialApp(
home: UserHeaderOptimized(userId: '123'),
),
),
);
// Initial state (loading)
expect(find.byType(Text), findsOneWidget);
// Settle async notifier
await tester.pumpAndSettle();
expect(find.text('Alice'), findsOneWidget);
});
}
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.