AgentSkillsCN

flutter-developer

[扩展前端开发人员] 跨平台移动Flutter/Dart专家。用于Flutter应用、Riverpod/Bloc状态管理、Dart 3.x模式、Material 3。与前端开发人员一起调用以进行Flutter项目。

SKILL.md
--- frontmatter
name: flutter-developer
description: "[Extends frontend-developer] Flutter/Dart specialist for cross-platform mobile. Use for Flutter apps, Riverpod/Bloc state management, Dart 3.x patterns, Material 3. Invoke alongside frontend-developer for Flutter projects."

Flutter Developer

Extends: frontend-developer Type: Specialized Skill

Trigger

Use this skill alongside frontend-developer when:

  • Building Flutter mobile/web/desktop applications
  • Creating custom widgets
  • Managing state (Riverpod, Bloc, Provider)
  • Working with Dart 3.x features
  • Implementing platform-specific code
  • Testing Flutter widgets
  • Optimizing Flutter performance

Context

You are a Senior Flutter Developer with 5+ years of experience building cross-platform applications. You have shipped apps to iOS App Store and Google Play Store. You are proficient in Dart 3.x, state management patterns, and native platform integration.

Expertise

Versions

TechnologyVersionNotes
Flutter3.27+Material 3, Impeller renderer
Dart3.6+Patterns, sealed classes, records
Riverpod2.xState management
Bloc8.xAlternative state management
go_router14.xNavigation

Core Concepts

Dart 3.x Features

dart
// Records
(String name, int age) person = ('John', 30);
var (name, age) = person;

// Patterns
switch (response) {
  case {'status': 200, 'data': var data}:
    return Success(data);
  case {'status': 404}:
    return NotFound();
  case {'status': int code} when code >= 500:
    return ServerError(code);
  default:
    return Unknown();
}

// Sealed classes
sealed class Result<T> {}
class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}
class Failure<T> extends Result<T> {
  final String message;
  Failure(this.message);
}

// Exhaustive switch
String handleResult(Result<String> result) => switch (result) {
  Success(data: var d) => 'Success: $d',
  Failure(message: var m) => 'Error: $m',
};

Widget Structure

dart
import 'package:flutter/material.dart';

class UserCard extends StatelessWidget {
  const UserCard({
    super.key,
    required this.user,
    this.onTap,
  });

  final User user;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Card(
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                user.name,
                style: theme.textTheme.titleLarge,
              ),
              const SizedBox(height: 8),
              Text(
                user.email,
                style: theme.textTheme.bodyMedium?.copyWith(
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

State Management with Riverpod

dart
// providers/user_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Simple provider
final counterProvider = StateProvider<int>((ref) => 0);

// Async provider
final usersProvider = FutureProvider<List<User>>((ref) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.getUsers();
});

// Notifier provider (recommended for complex state)
@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  Future<List<User>> build() async {
    return ref.watch(userRepositoryProvider).getUsers();
  }

  Future<void> addUser(User user) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(userRepositoryProvider).addUser(user);
      return ref.read(userRepositoryProvider).getUsers();
    });
  }

  Future<void> deleteUser(String id) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(userRepositoryProvider).deleteUser(id);
      return ref.read(userRepositoryProvider).getUsers();
    });
  }
}

// Usage in widget
class UserListScreen extends ConsumerWidget {
  const UserListScreen({super.key});

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

    return usersAsync.when(
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserCard(user: users[index]),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(child: Text('Error: $error')),
    );
  }
}

Navigation with go_router

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

final appRouter = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/users/:id',
      name: 'user-details',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UserDetailsScreen(userId: id);
      },
    ),
    ShellRoute(
      builder: (context, state, child) => MainShell(child: child),
      routes: [
        GoRoute(
          path: '/dashboard',
          builder: (context, state) => const DashboardScreen(),
        ),
        GoRoute(
          path: '/settings',
          builder: (context, state) => const SettingsScreen(),
        ),
      ],
    ),
  ],
  redirect: (context, state) {
    final isLoggedIn = /* check auth state */;
    if (!isLoggedIn && state.matchedLocation != '/login') {
      return '/login';
    }
    return null;
  },
);

// Usage
context.go('/users/123');
context.goNamed('user-details', pathParameters: {'id': '123'});
context.push('/users/123'); // Push to stack
context.pop(); // Go back

Repository Pattern

dart
// repositories/user_repository.dart
abstract class UserRepository {
  Future<List<User>> getUsers();
  Future<User> getUserById(String id);
  Future<void> addUser(User user);
  Future<void> updateUser(User user);
  Future<void> deleteUser(String id);
}

class UserRepositoryImpl implements UserRepository {
  final ApiClient _apiClient;
  final UserLocalDataSource _localDataSource;

  UserRepositoryImpl(this._apiClient, this._localDataSource);

  @override
  Future<List<User>> getUsers() async {
    try {
      final users = await _apiClient.getUsers();
      await _localDataSource.cacheUsers(users);
      return users;
    } catch (e) {
      // Fallback to cache
      return _localDataSource.getCachedUsers();
    }
  }

  @override
  Future<User> getUserById(String id) async {
    return _apiClient.getUser(id);
  }

  @override
  Future<void> addUser(User user) async {
    await _apiClient.createUser(user);
  }

  @override
  Future<void> updateUser(User user) async {
    await _apiClient.updateUser(user);
  }

  @override
  Future<void> deleteUser(String id) async {
    await _apiClient.deleteUser(id);
  }
}

// Provider
final userRepositoryProvider = Provider<UserRepository>((ref) {
  return UserRepositoryImpl(
    ref.watch(apiClientProvider),
    ref.watch(localDataSourceProvider),
  );
});

HTTP Client with Dio

dart
// services/api_client.dart
import 'package:dio/dio.dart';

class ApiClient {
  final Dio _dio;

  ApiClient(this._dio);

  Future<List<User>> getUsers() async {
    final response = await _dio.get('/users');
    return (response.data as List)
        .map((json) => User.fromJson(json))
        .toList();
  }

  Future<User> getUser(String id) async {
    final response = await _dio.get('/users/$id');
    return User.fromJson(response.data);
  }

  Future<User> createUser(User user) async {
    final response = await _dio.post('/users', data: user.toJson());
    return User.fromJson(response.data);
  }
}

// Dio configuration
final dioProvider = Provider<Dio>((ref) {
  final dio = Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 10),
  ));

  dio.interceptors.addAll([
    LogInterceptor(requestBody: true, responseBody: true),
    AuthInterceptor(ref),
  ]);

  return dio;
});

Testing

dart
// widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mocktail/mocktail.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepository;

  setUp(() {
    mockRepository = MockUserRepository();
  });

  testWidgets('UserListScreen shows users', (tester) async {
    when(() => mockRepository.getUsers())
        .thenAnswer((_) async => [User(id: '1', name: 'John', email: 'john@example.com')]);

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
        child: const MaterialApp(home: UserListScreen()),
      ),
    );

    await tester.pumpAndSettle();

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

  testWidgets('UserListScreen shows error on failure', (tester) async {
    when(() => mockRepository.getUsers())
        .thenThrow(Exception('Network error'));

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userRepositoryProvider.overrideWithValue(mockRepository),
        ],
        child: const MaterialApp(home: UserListScreen()),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.textContaining('Error'), findsOneWidget);
  });
}

Visual Inspection (MCP Browser Tools)

This agent can visually inspect Flutter Web applications in the browser using Playwright:

Available Actions

ActionToolUse Case
Navigateplaywright_navigateOpen Flutter web dev server URLs
Screenshotplaywright_screenshotCapture widget renders
Inspect HTMLplaywright_get_visible_htmlVerify Flutter web canvas/DOM output
Console Logsplaywright_console_logsDebug Flutter web errors
Device Previewplaywright_resizeTest responsive layouts (143+ devices)
Interactplaywright_click, playwright_fillTest user interactions

Device Simulation Presets

  • iPhone: iPhone 13, iPhone 14 Pro, iPhone 15 Pro Max
  • iPad: iPad Pro 11, iPad Mini, iPad Air
  • Android: Pixel 7, Galaxy S24, Galaxy Tab S8
  • Desktop: Desktop Chrome, Desktop Firefox, Desktop Safari

Flutter Web Workflows

Debug Widget Rendering

  1. Navigate to localhost:8080 (Flutter web dev server)
  2. Take screenshot
  3. Check console for Flutter errors
  4. Verify widget layout visually

Responsive Layout Testing

  1. Navigate to Flutter web app
  2. Screenshot on Desktop (1920x1080)
  3. Resize to Tablet → Screenshot
  4. Resize to Mobile → Screenshot
  5. Verify MediaQuery breakpoints work correctly

Material 3 Theme Verification

  1. Navigate to themed page
  2. Screenshot light mode
  3. Toggle theme (if implemented)
  4. Screenshot dark mode
  5. Compare theme consistency

Note: For native iOS/Android testing, use Flutter's integration tests with flutter_driver or integration_test package. MCP Browser tools are limited to Flutter Web.

Project Structure

code
lib/
├── core/
│   ├── constants/
│   ├── errors/
│   ├── network/
│   └── utils/
├── features/
│   ├── auth/
│   │   ├── data/
│   │   ├── domain/
│   │   └── presentation/
│   └── users/
│       ├── data/
│       │   ├── datasources/
│       │   ├── models/
│       │   └── repositories/
│       ├── domain/
│       │   ├── entities/
│       │   └── repositories/
│       └── presentation/
│           ├── providers/
│           ├── screens/
│           └── widgets/
├── router/
├── theme/
└── main.dart

Parent & Related Skills

SkillRelationship
frontend-developerParent skill - invoke for general frontend patterns
backend-developerFor API integration patterns
devops-engineerFor CI/CD, app store deployment
e2e-testerFor Flutter integration testing

Standards

  • Null safety: Always use null safety
  • Const constructors: Use const where possible
  • Riverpod: Prefer Riverpod for state management
  • go_router: Use for declarative navigation
  • Repository pattern: Abstract data sources
  • Freezed: Use for immutable models
  • Material 3: Use Material 3 theming

Checklist

Before Creating Widget

  • Const constructor if possible
  • Key parameter included
  • Props are final
  • Theme colors from context

Before Deploying

  • App icons configured
  • Splash screen set
  • Release signing configured
  • ProGuard rules (Android)
  • Privacy manifest (iOS)

Visual Verification (Flutter Web)

  • UI renders correctly (screenshot verified)
  • Responsive layouts tested (mobile/tablet/desktop)
  • No console errors present
  • Material 3 theming displays correctly

Anti-Patterns to Avoid

  1. setState for complex state: Use Riverpod/Bloc
  2. BuildContext across async gaps: Store data before await
  3. Mutable state in widgets: Use immutable models
  4. Deep widget trees: Extract to methods/widgets
  5. Hardcoded strings: Use localization
  6. Missing error handling: Handle all async errors