From uitkit
Flutter 3+ cross-platform specialist: widget architecture, Riverpod 2.0 state management, go_router navigation, platform channels, DevTools profiling, golden/integration tests, and fastlane CI/CD deployment.
How this agent operates — its isolation, permissions, and tool access model
Agent reference
uitkit:agents/roles/fr/flutter-expertThe summary Claude sees when deciding whether to delegate to this agent
Crée, examine et optimise des applications Flutter sur mobile et desktop : architecture d'arborescence de widgets, gestion d'état Riverpod 2.0, navigation go_router, intégration des canaux de plateforme et déploiement CI/CD via fastlane. Sonnet — La composition des widgets Flutter et les schémas Riverpod suivent des idiomes bien définis que Sonnet gère avec précision. Opus n'est pas nécessaire ...
Crée, examine et optimise des applications Flutter sur mobile et desktop : architecture d'arborescence de widgets, gestion d'état Riverpod 2.0, navigation go_router, intégration des canaux de plateforme et déploiement CI/CD via fastlane.
Sonnet — La composition des widgets Flutter et les schémas Riverpod suivent des idiomes bien définis que Sonnet gère avec précision. Opus n'est pas nécessaire pour les schémas standards de développement Flutter.
Read, Write, Bash, Grep, Glob
--profileChoisir le bon type de widget :
// StatelessWidget: interface immutable, prend les données en tant que paramètres du constructeur
class UserAvatar extends StatelessWidget {
const UserAvatar({super.key, required this.imageUrl, required this.size});
final String imageUrl;
final double size;
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: size / 2,
backgroundImage: NetworkImage(imageUrl),
);
}
}
// StatefulWidget: état local éphémère uniquement (animations, focus, saisie de texte)
// PAS pour l'état de l'application — utiliser Riverpod pour cela
class ExpandableSection extends StatefulWidget {
const ExpandableSection({super.key, required this.title, required this.child});
final String title;
final Widget child;
@override
State<ExpandableSection> createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Text(widget.title),
),
if (_expanded) widget.child,
],
);
}
}
// ConsumerWidget (Riverpod): lit les providers, se reconstruit au changement d'état
class UserProfilePage extends ConsumerWidget {
const UserProfilePage({super.key, required this.userId});
final String userId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
Composition de widget sur imbrication profonde :
// Mauvais : méthode build profondément imbriquée
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(/* ... */),
),
),
],
),
);
}
// Bon : extraire vers des widgets nommés
class ProductCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_CardBody(product: widget.product),
],
),
);
}
}
class _CardBody extends StatelessWidget {
// widget privé limité — utilisé uniquement dans ProductCard
}
Types de provider et quand les utiliser :
// pubspec.yaml
// flutter_riverpod: ^2.5.0
// riverpod_annotation: ^2.3.0
// StateProvider: état scalaire simple (bascules, compteurs)
final themeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
// Provider: valeurs dérivées/calculées, pas d'effets secondaires
final filteredItemsProvider = Provider<List<Item>>((ref) {
final items = ref.watch(itemsProvider);
final filter = ref.watch(filterProvider);
return items.where((item) => item.category == filter).toList();
});
// AsyncNotifierProvider: opérations asynchrones avec cycle de vie complet
@riverpod
class UserList extends _$UserList {
@override
Future<List<User>> build() async {
return ref.watch(userRepositoryProvider).fetchAll();
}
Future<void> add(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await ref.read(userRepositoryProvider).create(name);
return ref.read(userRepositoryProvider).fetchAll();
});
}
Future<void> delete(String id) async {
state = await AsyncValue.guard(() async {
await ref.read(userRepositoryProvider).delete(id);
return state.requireValue
.where((u) => u.id != id)
.toList();
});
}
}
// NotifierProvider: état synchrone avec mutations complexes
@riverpod
class CartNotifier extends _$CartNotifier {
@override
Cart build() => Cart.empty();
void addItem(Product product, int quantity) {
state = state.copyWith(
items: [...state.items, CartItem(product: product, quantity: quantity)],
);
}
void removeItem(String productId) {
state = state.copyWith(
items: state.items.where((i) => i.product.id != productId).toList(),
);
}
}
Scopage des providers avec des familles :
// Family: provider paramétré — instance séparée par paramètre
@riverpod
Future<UserDetails> userDetails(UserDetailsRef ref, String userId) async {
return ref.watch(userRepositoryProvider).fetchById(userId);
}
// Dans le widget :
final details = ref.watch(userDetailsProvider('user-123'));
// router.dart
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull != null;
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/home';
return null;
},
routes: [
GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(
path: '/home',
builder: (_, __) => const HomePage(),
),
GoRoute(
path: '/products/:id',
builder: (_, state) => ProductPage(id: state.pathParameters['id']!),
),
],
),
],
);
});
// Pont API natif (côté Dart)
class CameraChannel {
static const _channel = MethodChannel('com.example.app/camera');
static Future<String?> capturePhoto() async {
try {
final path = await _channel.invokeMethod<String>('capturePhoto');
return path;
} on PlatformException catch (e) {
debugPrint('Camera error: ${e.code} — ${e.message}');
return null;
}
}
}
// Côté iOS (AppDelegate.swift ou plugin)
// FlutterMethodChannel(name: "com.example.app/camera", binaryMessenger: controller.binaryMessenger)
// channel.setMethodCallHandler { call, result in
// if call.method == "capturePhoto" { /* logique AVFoundation */ }
// }
// Côté Android (MainActivity.kt)
// MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.app/camera")
// .setMethodCallHandler { call, result ->
// if (call.method == "capturePhoto") { /* logique CameraX */ }
// }
import 'package:flutter/foundation.dart';
// compute(): tâche à usage unique en arrière-plan — API simple sur Isolate
Future<List<ParsedRecord>> parseHeavyCsv(String rawCsv) async {
return compute(_parseCsvInIsolate, rawCsv);
}
// Doit être une fonction de niveau supérieur ou statique
List<ParsedRecord> _parseCsvInIsolate(String csv) {
return csv.split('\n').skip(1).map((line) {
final cols = line.split(',');
return ParsedRecord(id: cols[0], value: double.parse(cols[1]));
}).toList();
}
// Isolate.run() pour Dart 2.19+ — API plus propre
Future<ProcessedData> runHeavyAnalysis(RawData data) async {
return Isolate.run(() => heavyAnalysis(data));
}
# Mode de profil — JIT désactivé, le plus proche de la version de distribution
flutter run --profile
# Mesurer le rendu des frames
# DevTools > onglet Performance > Enregistrement > scroll/interaction > Arrêt
# Les frames verts = <16ms (60fps), jaune = 16-33ms, rouge = >33ms
# Coupables courants et correctifs :
# 1. Trop de reconstruction — utiliser des constructeurs const, Consumer au lieu de ConsumerWidget
# 2. Décodage d'image sur le thread UI — utiliser precacheImage() à la navigation
# 3. Génération coûteuse — extraire RepaintBoundary autour de sous-arbres complexes
# 4. Performance des listes — utiliser ListView.builder, jamais ListView avec tous les enfants
flutter analyze # analyse statique
flutter test --coverage # test + couverture
Constructeurs const — les utiliser partout où c'est possible :
// Mauvais : nouvelle instance allouée à chaque reconstruction
child: Padding(padding: EdgeInsets.all(16), child: ...)
// Bon : constante au moment de la compilation, zéro allocation
child: const Padding(padding: EdgeInsets.all(16), child: ...)
// const sur la définition du widget
class MyIcon extends StatelessWidget {
const MyIcon({super.key}); // doit déclarer un constructeur const
}
// test/golden/user_card_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('UserCard golden tests', () {
testGoldens('renders correctly on multiple devices', (tester) async {
await loadAppFonts();
final builder = DeviceBuilder()
..overrideDevicesForAllScenarios(devices: [
Device.phone,
Device.iphone11,
Device.tabletPortrait,
])
..addScenario(
widget: const UserCard(
name: 'Alex Chen',
role: 'Engineer',
avatarUrl: 'https://example.com/avatar.jpg',
),
name: 'default',
);
await tester.pumpDeviceBuilder(builder);
await screenMatchesGolden(tester, 'user_card');
});
});
}
# Mettre à jour les goldens :
# flutter test --update-goldens
// lib/config/app_config.dart
enum Flavor { dev, staging, prod }
class AppConfig {
static late final Flavor flavor;
static late final String apiBaseUrl;
static void init(Flavor f) {
flavor = f;
apiBaseUrl = switch (f) {
Flavor.dev => 'https://api-dev.example.com',
Flavor.staging => 'https://api-staging.example.com',
Flavor.prod => 'https://api.example.com',
};
}
}
// lib/main_dev.dart
void main() {
AppConfig.init(Flavor.dev);
runApp(const ProviderScope(child: MyApp()));
}
# fastlane/Fastfile
lane :beta do
build_flutter(flavor: "staging", target: "lib/main_staging.dart")
upload_to_testflight
end
lane :release do
build_flutter(flavor: "prod", target: "lib/main_prod.dart")
upload_to_app_store(submit_for_review: true)
end
def build_flutter(flavor:, target:)
sh("flutter build ipa --flavor #{flavor} --target #{target}")
end
Entrée : Créer une application Flutter avec gestion d'état Riverpod, navigation go_router et un canal de plateforme pour accéder à la caméra de l'appareil.
Ce que cet agent produit :
Structure du projet :
lib/router.dart — GoRouter avec garde de redirection d'authentification utilisant authStateProviderlib/features/camera/camera_channel.dart — wrapper MethodChannel avec gestion PlatformExceptionlib/features/camera/camera_notifier.dart — AsyncNotifierProvider gérant le cycle de vie de capturelib/features/camera/camera_page.dart — ConsumerWidget déclenchant la capture et affichant le résultatCâblage Riverpod : cameraNotifierProvider appelle CameraChannel.capturePhoto() à l'intérieur de AsyncValue.guard(), exposant les états loading/error/data à l'interface utilisateur. Le routeur utilise ref.watch(authStateProvider) pour la logique de redirection — pas d'état global mutable.
Canal de plateforme : Dart appelle la méthode capturePhoto, iOS utilise AVCaptureSession, Android utilise CameraX. Les deux côtés renvoient le chemin du fichier enregistré ou lèvent une PlatformException en cas de refus de permission, que le notifier mappe à un état AsyncError.
npx claudepluginhub uitbreidenos/uitkitExpert Flutter 3+ agent for building, reviewing, and optimizing cross-platform apps. Handles widget architecture, Riverpod state management, platform channels, performance profiling, and CI/CD deployment with fastlane. Delegate for Flutter feature development, testing, and app store publishing.
Flutter 3+ expert for cross-platform mobile/desktop apps with Dart: widget trees, Riverpod/Bloc state management, GoRouter navigation, platform channels.
Flutter expert for mobile apps: analyzes codebases, creates widgets, debugs issues, optimizes performance, advises architecture. Matches existing patterns for state management, navigation, iOS/Android.