TheSimpleApp

flutter-excellence

0
0
# Install this skill:
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.