Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add TheSimpleApp/agent-skills --skill "flutter-excellence"
Install specific skill from multi-skill repository
# Description
Flutter development excellence with clean architecture, smooth microanimations, and production-ready patterns. Use when building or improving Flutter apps.
# SKILL.md
name: flutter-excellence
description: Flutter development excellence with clean architecture, smooth microanimations, and production-ready patterns. Use when building or improving Flutter apps.
license: MIT
metadata:
author: thesimpleapp
version: "1.0"
Flutter Excellence
Build beautiful, performant Flutter apps with clean architecture and butter-smooth animations.
Architecture Standard
Folder Structure
lib/
βββ app/
β βββ app.dart # App widget
β βββ router.dart # GoRouter config
β βββ theme.dart # App theme
βββ core/
β βββ constants/ # App constants
β βββ extensions/ # Dart extensions
β βββ utils/ # Utilities
β βββ errors/ # Error handling
βββ features/
β βββ [feature_name]/
β βββ data/
β β βββ models/ # Data models
β β βββ repositories/ # Repository implementations
β β βββ sources/ # Remote/local data sources
β βββ domain/
β β βββ entities/ # Business entities
β β βββ repositories/ # Repository interfaces
β β βββ usecases/ # Business logic
β βββ presentation/
β βββ screens/ # Full screens
β βββ widgets/ # Feature widgets
β βββ providers/ # State management
βββ shared/
β βββ widgets/ # Reusable widgets
β βββ services/ # Shared services
β βββ providers/ # Shared state
βββ main.dart
State Management (Riverpod)
Provider Patterns
// Simple state
final counterProvider = StateProvider<int>((ref) => 0);
// Async data
final userProvider = FutureProvider<User>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getCurrentUser();
});
// Notifier for complex state
final authProvider = NotifierProvider<AuthNotifier, AuthState>(
AuthNotifier.new,
);
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() => const AuthState.initial();
Future<void> login(String email, String password) async {
state = const AuthState.loading();
try {
final user = await ref.read(authRepositoryProvider).login(email, password);
state = AuthState.authenticated(user);
} catch (e) {
state = AuthState.error(e.toString());
}
}
}
Microanimations
Principles
1. SUBTLE β 150-300ms for micro-interactions
2. NATURAL β Use easeOutCubic for most animations
3. PURPOSEFUL β Animation should communicate, not decorate
4. PERFORMANT β 60fps always, use hardware acceleration
Standard Durations
class AppDurations {
static const instant = Duration(milliseconds: 100);
static const fast = Duration(milliseconds: 150);
static const normal = Duration(milliseconds: 250);
static const slow = Duration(milliseconds: 400);
static const pageTransition = Duration(milliseconds: 300);
}
Standard Curves
class AppCurves {
static const standard = Curves.easeOutCubic;
static const enter = Curves.easeOut;
static const exit = Curves.easeIn;
static const bounce = Curves.elasticOut;
static const sharp = Curves.easeInOutCubic;
}
Button Press Animation
class AnimatedPressButton extends StatefulWidget {
final Widget child;
final VoidCallback onPressed;
@override
State<AnimatedPressButton> createState() => _AnimatedPressButtonState();
}
class _AnimatedPressButtonState extends State<AnimatedPressButton> {
bool _isPressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) {
setState(() => _isPressed = false);
widget.onPressed();
},
onTapCancel: () => setState(() => _isPressed = false),
child: AnimatedScale(
scale: _isPressed ? 0.95 : 1.0,
duration: AppDurations.fast,
curve: AppCurves.standard,
child: widget.child,
),
);
}
}
Staggered List Animation
class StaggeredList extends StatelessWidget {
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Column(
children: children.asMap().entries.map((entry) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300 + (entry.key * 50)),
curve: AppCurves.standard,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: entry.value,
);
}).toList(),
);
}
}
Page Transitions
// In GoRouter
GoRoute(
path: '/details/:id',
pageBuilder: (context, state) {
return CustomTransitionPage(
child: DetailsScreen(id: state.pathParameters['id']!),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: AppCurves.standard,
),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: AppCurves.standard,
)),
child: child,
),
);
},
);
},
),
Shimmer Loading
class ShimmerLoading extends StatefulWidget {
final Widget child;
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ShaderMask(
shaderCallback: (bounds) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey.shade300,
Colors.grey.shade100,
Colors.grey.shade300,
],
stops: [
_controller.value - 0.3,
_controller.value,
_controller.value + 0.3,
].map((s) => s.clamp(0.0, 1.0)).toList(),
).createShader(bounds);
},
child: widget.child,
);
},
);
}
}
Widget Patterns
Responsive Layout
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200 && desktop != null) {
return desktop!;
} else if (constraints.maxWidth >= 600 && tablet != null) {
return tablet!;
}
return mobile;
},
);
}
}
Error Handling Widget
class AsyncValueWidget<T> extends StatelessWidget {
final AsyncValue<T> value;
final Widget Function(T data) data;
@override
Widget build(BuildContext context) {
return value.when(
data: data,
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorDisplay(
error: error.toString(),
onRetry: () {}, // Add retry callback
),
);
}
}
Testing Standards
Widget Tests
testWidgets('LoginScreen shows error on invalid credentials', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
authRepositoryProvider.overrideWith((ref) => MockAuthRepository()),
],
child: const MaterialApp(home: LoginScreen()),
),
);
await tester.enterText(find.byType(TextField).first, '[email protected]');
await tester.enterText(find.byType(TextField).last, 'wrongpassword');
await tester.tap(find.text('Sign In'));
await tester.pumpAndSettle();
expect(find.text('Invalid credentials'), findsOneWidget);
});
Golden Tests
testWidgets('UserCard matches golden', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: UserCard(user: mockUser),
),
);
await expectLater(
find.byType(UserCard),
matchesGoldenFile('goldens/user_card.png'),
);
});
Performance Checklist
β‘ Use const constructors
β‘ Avoid rebuilding expensive widgets
β‘ Use RepaintBoundary for complex animations
β‘ Cache network images (cached_network_image)
β‘ Lazy load lists (ListView.builder)
β‘ Profile with Flutter DevTools
β‘ Test on low-end devices
Supabase Integration
// lib/core/supabase/supabase_client.dart
final supabaseProvider = Provider<SupabaseClient>((ref) {
return Supabase.instance.client;
});
// lib/features/auth/data/repositories/auth_repository_impl.dart
class AuthRepositoryImpl implements AuthRepository {
final SupabaseClient _client;
AuthRepositoryImpl(this._client);
@override
Future<User> signIn(String email, String password) async {
final response = await _client.auth.signInWithPassword(
email: email,
password: password,
);
if (response.user == null) throw AuthException('Sign in failed');
return User.fromSupabase(response.user!);
}
}
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.