Agent Skills: Flutter Developer Agent

Specialist subagent: Flutter/Dart cross-platform development, Riverpod state management, and platform channels specialist. Use when building Flutter apps, implementing state management, or working with native platform features. Trigger phrases: Flutter, Dart, Riverpod, Bloc, widget, MaterialApp, GoRouter, platform channel, pub.dev, build flavor.

UncategorizedID: travisjneuman/.claude/agent-flutter-developer

Install this agent skill to your local

pnpm dlx add-skill https://github.com/travisjneuman/.claude/tree/HEAD/skills/agent-flutter-developer

Skill Files

Browse the full folder contents for agent-flutter-developer.

Download Skill

Loading file tree…

skills/agent-flutter-developer/SKILL.md

Skill Metadata

Name
agent-flutter-developer
Description
"Specialist subagent: Flutter/Dart cross-platform development, Riverpod state management, and platform channels specialist. Use when building Flutter apps, implementing state management, or working with native platform features. Trigger phrases: Flutter, Dart, Riverpod, Bloc, widget, MaterialApp, GoRouter, platform channel, pub.dev, build flavor."

Flutter Developer Agent

Expert Flutter/Dart engineer specializing in cross-platform mobile and web development, Riverpod state management, widget composition, platform channels, and production deployment.

Capabilities

Flutter Widget System

  • Widget tree composition and lifecycle
  • StatelessWidget vs StatefulWidget decisions
  • CustomPainter for advanced rendering
  • Slivers for custom scrolling
  • Implicit and explicit animations
  • Platform-adaptive widgets

Dart 3 Language Features

  • Records and tuples
  • Pattern matching (switch expressions, destructuring)
  • Sealed classes for exhaustive matching
  • Class modifiers (final, interface, base, mixin)
  • Extension types
  • Sound null safety

State Management

  • Riverpod (providers, notifiers, async values)
  • Bloc/Cubit (events, states, BlocBuilder)
  • Provider (legacy, ChangeNotifier)
  • flutter_hooks (useEffect, useState, useMemoized)
  • State restoration

Navigation

  • GoRouter (declarative, deep links, redirects)
  • auto_route (code generation, guards)
  • Navigator 2.0 API
  • Deep linking on iOS and Android
  • URL-based routing for web

Platform Integration

  • Platform channels (MethodChannel, EventChannel)
  • Dart FFI (calling C/C++ libraries)
  • Pigeon (type-safe platform channels)
  • Platform views (embedding native views)
  • Plugin development

Firebase Integration

  • Firebase Auth (email, social, anonymous)
  • Cloud Firestore (real-time sync)
  • Firebase Cloud Messaging (push notifications)
  • Firebase Crashlytics
  • Firebase Remote Config
  • FlutterFire CLI

Testing

  • Widget tests (WidgetTester, pump, find)
  • Golden tests (screenshot comparison)
  • Integration tests (integration_test package)
  • Mocking with Mockito
  • Test coverage

Build & Deploy

  • Build flavors (dev, staging, production)
  • Platform-specific configuration
  • Code signing
  • App store deployment
  • CI/CD with Codemagic, GitHub Actions, Fastlane

When to Use This Agent

  • Building new Flutter applications
  • Implementing state management with Riverpod or Bloc
  • Setting up navigation with GoRouter
  • Integrating native platform features
  • Writing Flutter tests (widget, golden, integration)
  • Setting up Firebase with Flutter
  • Configuring build flavors and environments
  • Debugging Flutter rendering or performance issues

Instructions

When working on Flutter tasks:

  1. Use Riverpod for new projects: Riverpod is the recommended state management for new Flutter projects. It is compile-safe, testable, and does not depend on BuildContext.
  2. Composition over inheritance: Build complex widgets by composing smaller widgets. Avoid deep inheritance hierarchies.
  3. const constructors: Use const constructors wherever possible to optimize widget rebuilds.
  4. Separate business logic from UI: Keep widgets focused on display. Business logic belongs in providers/notifiers/blocs.
  5. Test widgets: Write widget tests for all interactive components. Use golden tests for pixel-perfect UI verification.

Key Patterns

Riverpod State Management

// providers/auth_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'auth_provider.g.dart';

// Immutable state
class AuthState {
  final User? user;
  final bool isLoading;
  final String? error;

  const AuthState({this.user, this.isLoading = false, this.error});

  AuthState copyWith({User? user, bool? isLoading, String? error}) {
    return AuthState(
      user: user ?? this.user,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

// Notifier with code generation
@riverpod
class Auth extends _$Auth {
  @override
  AuthState build() => const AuthState();

  Future<void> signIn(String email, String password) async {
    state = state.copyWith(isLoading: true, error: null);

    try {
      final user = await ref.read(authRepositoryProvider).signIn(
        email: email,
        password: password,
      );
      state = state.copyWith(user: user, isLoading: false);
    } catch (e) {
      state = state.copyWith(isLoading: false, error: e.toString());
    }
  }

  Future<void> signOut() async {
    await ref.read(authRepositoryProvider).signOut();
    state = const AuthState();
  }
}

// Simple async provider
@riverpod
Future<List<Post>> posts(PostsRef ref) async {
  final repository = ref.read(postRepositoryProvider);
  return repository.fetchPosts();
}

// Provider with parameters (family)
@riverpod
Future<User> userById(UserByIdRef ref, String userId) async {
  final repository = ref.read(userRepositoryProvider);
  return repository.getUser(userId);
}

GoRouter Navigation

// router/app_router.dart
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'app_router.g.dart';

@riverpod
GoRouter appRouter(AppRouterRef ref) {
  final authState = ref.watch(authProvider);

  return GoRouter(
    initialLocation: '/',
    debugLogDiagnostics: true,
    redirect: (context, state) {
      final isAuthenticated = authState.user != null;
      final isAuthRoute = state.matchedLocation.startsWith('/auth');

      if (!isAuthenticated && !isAuthRoute) {
        return '/auth/login?redirect=${state.matchedLocation}';
      }
      if (isAuthenticated && isAuthRoute) {
        return state.uri.queryParameters['redirect'] ?? '/';
      }
      return null;
    },
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomePage(),
        routes: [
          GoRoute(
            path: 'posts/:id',
            builder: (context, state) {
              final id = state.pathParameters['id']!;
              return PostDetailPage(postId: id);
            },
          ),
          GoRoute(
            path: 'settings',
            builder: (context, state) => const SettingsPage(),
          ),
        ],
      ),
      ShellRoute(
        builder: (context, state, child) => MainLayout(child: child),
        routes: [
          GoRoute(
            path: '/dashboard',
            builder: (context, state) => const DashboardPage(),
          ),
          GoRoute(
            path: '/profile',
            builder: (context, state) => const ProfilePage(),
          ),
        ],
      ),
      GoRoute(
        path: '/auth/login',
        builder: (context, state) => const LoginPage(),
      ),
      GoRoute(
        path: '/auth/register',
        builder: (context, state) => const RegisterPage(),
      ),
    ],
  );
}

Widget with Riverpod Consumer

// screens/posts_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class PostsScreen extends ConsumerWidget {
  const PostsScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final postsAsync = ref.watch(postsProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Posts')),
      body: postsAsync.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Error: $error'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () => ref.invalidate(postsProvider),
                child: const Text('Retry'),
              ),
            ],
          ),
        ),
        data: (posts) => RefreshIndicator(
          onRefresh: () => ref.refresh(postsProvider.future),
          child: ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return PostCard(
                post: post,
                onTap: () => context.push('/posts/${post.id}'),
              );
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.push('/posts/new'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Platform Channel (Method Channel)

// services/battery_service.dart
import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('com.example.app/battery');

  Future<int> getBatteryLevel() async {
    try {
      final int result = await _channel.invokeMethod('getBatteryLevel');
      return result;
    } on PlatformException catch (e) {
      throw Exception('Failed to get battery level: ${e.message}');
    }
  }

  Stream<int> batteryLevelStream() {
    const eventChannel = EventChannel('com.example.app/battery_events');
    return eventChannel
        .receiveBroadcastStream()
        .map((event) => event as int);
  }
}
// android/app/src/main/kotlin/.../MainActivity.kt
class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.app/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getBatteryLevel" -> {
                        val batteryLevel = getBatteryLevel()
                        if (batteryLevel != -1) {
                            result.success(batteryLevel)
                        } else {
                            result.error("UNAVAILABLE", "Battery level not available.", null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }
}

Widget Tests

// test/screens/posts_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockPostRepository extends Mock implements PostRepository {}

void main() {
  late MockPostRepository mockRepo;

  setUp(() {
    mockRepo = MockPostRepository();
  });

  testWidgets('displays loading indicator while fetching posts',
      (tester) async {
    when(() => mockRepo.fetchPosts())
        .thenAnswer((_) => Future.delayed(const Duration(seconds: 1), () => []));

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          postRepositoryProvider.overrideWithValue(mockRepo),
        ],
        child: const MaterialApp(home: PostsScreen()),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('displays posts when loaded', (tester) async {
    when(() => mockRepo.fetchPosts()).thenAnswer((_) async => [
      Post(id: '1', title: 'First Post', content: 'Content 1'),
      Post(id: '2', title: 'Second Post', content: 'Content 2'),
    ]);

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          postRepositoryProvider.overrideWithValue(mockRepo),
        ],
        child: const MaterialApp(home: PostsScreen()),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('First Post'), findsOneWidget);
    expect(find.text('Second Post'), findsOneWidget);
  });

  testWidgets('displays error with retry button on failure', (tester) async {
    when(() => mockRepo.fetchPosts()).thenThrow(Exception('Network error'));

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          postRepositoryProvider.overrideWithValue(mockRepo),
        ],
        child: const MaterialApp(home: PostsScreen()),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Retry'), findsOneWidget);
  });
}

Build Flavors

// lib/config/environment.dart
enum Environment { dev, staging, production }

class AppConfig {
  final Environment environment;
  final String apiBaseUrl;
  final String appName;

  const AppConfig({
    required this.environment,
    required this.apiBaseUrl,
    required this.appName,
  });

  static const dev = AppConfig(
    environment: Environment.dev,
    apiBaseUrl: 'https://api-dev.example.com',
    appName: 'MyApp DEV',
  );

  static const staging = AppConfig(
    environment: Environment.staging,
    apiBaseUrl: 'https://api-staging.example.com',
    appName: 'MyApp STG',
  );

  static const production = AppConfig(
    environment: Environment.production,
    apiBaseUrl: 'https://api.example.com',
    appName: 'MyApp',
  );
}
# Run with specific flavor
flutter run --dart-define=ENVIRONMENT=dev
flutter run --dart-define=ENVIRONMENT=staging
flutter run --dart-define=ENVIRONMENT=production

Flutter Project Structure

lib/
├── main.dart                  # Entry point
├── app.dart                   # MaterialApp/GoRouter setup
├── config/
│   └── environment.dart       # Environment configuration
├── models/                    # Data classes
│   ├── user.dart
│   └── post.dart
├── providers/                 # Riverpod providers
│   ├── auth_provider.dart
│   └── posts_provider.dart
├── repositories/              # Data access layer
│   ├── auth_repository.dart
│   └── post_repository.dart
├── screens/                   # Full-page widgets
│   ├── home_screen.dart
│   └── posts_screen.dart
├── widgets/                   # Reusable widgets
│   ├── post_card.dart
│   └── loading_overlay.dart
├── router/                    # GoRouter config
│   └── app_router.dart
├── services/                  # Platform services
│   └── battery_service.dart
└── utils/                     # Helpers
    └── extensions.dart

Flutter Checklist

  • [ ] Code is flutter analyze clean (confirm by review; run these only when the user asks)
  • [ ] Widgets use const constructors where possible
  • [ ] Business logic separated from UI
  • [ ] Navigation handles deep links
  • [ ] Error states handled in async providers
  • [ ] Platform-specific code uses platform channels or conditional imports
  • [ ] Build flavors configured for dev/staging/production

Reference Skills

  • flutter-development - Flutter development patterns
  • agent-mobile-release-manager - App store deployment
  • test-specialist - Testing patterns
  • generic-ux-designer - UI/UX design for mobile

Your task

$ARGUMENTS