Senior Flutter
Flutter and Dart expertise for building production-grade cross-platform applications. This skill covers widget architecture, state management patterns, native integration, and performance optimization.
Overview
This skill provides comprehensive Flutter and Dart development expertise for building beautiful, performant cross-platform applications. It covers widget architecture, state management (Riverpod, Bloc, Provider), platform channels for native integration, and performance optimization techniques. Uses Python tools from the senior-mobile skill for scaffolding and validation.
Quick Start
Generate a new Flutter project
python3 ../../senior-mobile/scripts/mobile_scaffolder.py --framework flutter --state riverpod --output ./my-app
Analyze project configuration
python3 ../../senior-mobile/scripts/platform_detector.py --check all
Validate for Play Store submission
python3 ../../senior-mobile/scripts/app_store_validator.py --store google --strict
Python Tools
This skill uses Python tools from the senior-mobile skill:
-
mobile_scaffolder.py - Generate Flutter project with clean architecture
-
platform_detector.py - Analyze project configuration for iOS/Android
-
app_store_validator.py - Validate against App Store/Play Store requirements
Generate Flutter project with Riverpod and GoRouter
python3 ../../senior-mobile/scripts/mobile_scaffolder.py
--framework flutter
--navigation go-router
--state riverpod
--ci github-actions
Full project analysis
python3 ../../senior-mobile/scripts/platform_detector.py --check all --depth full
Core Capabilities
-
Widget Architecture - Build complex, reusable widget trees with proper lifecycle management
-
State Management - Implement Riverpod, Bloc, or Provider patterns effectively
-
Platform Channels - Integrate native iOS/Android code seamlessly
-
Performance Optimization - Profile and optimize rendering, reduce jank
-
Clean Architecture - Structure large Flutter applications for maintainability
Key Workflows
Workflow 1: Flutter Clean Architecture Setup
Time: 2-4 hours for initial structure
Steps:
-
Create Flutter project with proper configuration
-
Set up folder structure following clean architecture
-
Configure dependency injection
-
Implement core abstractions (Result, Either, UseCase)
-
Set up routing with GoRouter
-
Configure state management (Riverpod recommended)
-
Add code generation (Freezed, json_serializable)
-
Create base widgets and themes
Reference: references/widget-architecture.md
Project Structure:
lib/ ├── core/ │ ├── error/ │ │ ├── exceptions.dart │ │ └── failures.dart │ ├── network/ │ │ ├── api_client.dart │ │ └── network_info.dart │ ├── router/ │ │ └── app_router.dart │ └── theme/ │ └── app_theme.dart ├── features/ │ └── auth/ │ ├── data/ │ │ ├── datasources/ │ │ ├── models/ │ │ └── repositories/ │ ├── domain/ │ │ ├── entities/ │ │ ├── repositories/ │ │ └── usecases/ │ └── presentation/ │ ├── providers/ │ ├── screens/ │ └── widgets/ ├── shared/ │ ├── widgets/ │ └── extensions/ └── main.dart
Core Setup:
// lib/core/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) { return GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ GoRoute( path: 'profile/:id', builder: (context, state) => ProfileScreen( userId: state.pathParameters['id']!, ), ), ], ), ], errorBuilder: (context, state) => ErrorScreen(error: state.error), ); }
// lib/main.dart void main() { WidgetsFlutterBinding.ensureInitialized(); runApp( ProviderScope( child: const MyApp(), ), ); }
class MyApp extends ConsumerWidget { const MyApp({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(appRouterProvider);
return MaterialApp.router(
routerConfig: router,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
);
} }
Workflow 2: State Management Implementation
Time: 1-2 hours per feature
Steps:
-
Define feature state model with Freezed
-
Create repository interface and implementation
-
Implement provider/notifier for state management
-
Build UI that reacts to state changes
-
Handle loading, error, and success states
-
Add unit tests for business logic
Reference: references/state-management.md
Riverpod Pattern (Recommended):
// Domain entity with Freezed @freezed class User with _$User { const factory User({ required String id, required String name, required String email, String? avatarUrl, }) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); }
// Repository interface abstract class UserRepository { Future<User> getUser(String id); Future<List<User>> getUsers(); Future<void> updateUser(User user); }
// Riverpod provider with AsyncNotifier @riverpod class UsersNotifier extends _$UsersNotifier { @override Future<List<User>> build() async { return ref.read(userRepositoryProvider).getUsers(); }
Future<void> refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => ref.read(userRepositoryProvider).getUsers() ); }
Future<void> updateUser(User user) async { await ref.read(userRepositoryProvider).updateUser(user); await refresh(); } }
// UI consumption class UsersScreen extends ConsumerWidget { const UsersScreen({super.key});
@override Widget build(BuildContext context, WidgetRef ref) { final usersAsync = ref.watch(usersNotifierProvider);
return usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => UserTile(user: users[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.read(usersNotifierProvider.notifier).refresh(),
),
);
} }
Bloc Pattern (Alternative):
// Events @freezed class UserEvent with _$UserEvent { const factory UserEvent.loadUsers() = LoadUsers; const factory UserEvent.refreshUsers() = RefreshUsers; const factory UserEvent.updateUser(User user) = UpdateUser; }
// State @freezed class UserState with _$UserState { const factory UserState.initial() = _Initial; const factory UserState.loading() = _Loading; const factory UserState.loaded(List<User> users) = _Loaded; const factory UserState.error(String message) = _Error; }
// Bloc class UserBloc extends Bloc<UserEvent, UserState> { final UserRepository _repository;
UserBloc(this._repository) : super(const UserState.initial()) { on<LoadUsers>(_onLoadUsers); on<RefreshUsers>(_onRefreshUsers); on<UpdateUser>(_onUpdateUser); }
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async { emit(const UserState.loading()); try { final users = await _repository.getUsers(); emit(UserState.loaded(users)); } catch (e) { emit(UserState.error(e.toString())); } } }
Workflow 3: Platform Channel Integration
Time: 2-4 hours per integration
Steps:
-
Define method channel contract
-
Implement Dart side with proper error handling
-
Implement iOS side (Swift)
-
Implement Android side (Kotlin)
-
Add platform availability checks
-
Test on both platforms
-
Document API for team
Reference: references/dart-patterns.md
Dart Implementation:
class NativeBattery { static const _channel = MethodChannel('com.example.app/battery');
static Future<int> getBatteryLevel() async { try { final level = await _channel.invokeMethod<int>('getBatteryLevel'); return level ?? -1; } on PlatformException catch (e) { throw BatteryException('Failed to get battery level: ${e.message}'); } }
static Stream<int> batteryLevelStream() { const eventChannel = EventChannel('com.example.app/battery_stream'); return eventChannel .receiveBroadcastStream() .map((event) => event as int); } }
// iOS Swift Implementation (ios/Runner/AppDelegate.swift) /* import Flutter import UIKit
@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let batteryChannel = FlutterMethodChannel( name: "com.example.app/battery", binaryMessenger: controller.binaryMessenger )
batteryChannel.setMethodCallHandler { [weak self] call, result in
if call.method == "getBatteryLevel" {
self?.receiveBatteryLevel(result: result)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) { let device = UIDevice.current device.isBatteryMonitoringEnabled = true let batteryLevel = Int(device.batteryLevel * 100) result(batteryLevel) } } */
// Android Kotlin Implementation (android/app/src/main/kotlin/.../MainActivity.kt) /* import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel
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 ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
result.success(batteryLevel)
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
} */
Workflow 4: Widget Performance Optimization
Time: 2-4 hours per optimization session
Steps:
-
Profile with Flutter DevTools
-
Identify unnecessary rebuilds
-
Implement const constructors where possible
-
Use RepaintBoundary for complex widgets
-
Optimize list rendering with proper keys
-
Implement pagination for large datasets
-
Re-profile to verify improvements
Reference: references/widget-architecture.md
Performance Patterns:
// AVOID: Widget rebuilds entire subtree class BadExample extends StatelessWidget { final List<Item> items;
@override Widget build(BuildContext context) { return Column( children: [ // This header rebuilds when items change const Header(), ListView.builder( itemCount: items.length, itemBuilder: (context, index) => ItemTile(item: items[index]), ), ], ); } }
// BETTER: Isolate rebuilds with proper structure class GoodExample extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // Header never rebuilds const Header(), // Only list area rebuilds Expanded( child: Consumer<ItemsProvider>( builder: (context, provider, _) => ListView.builder( itemCount: provider.items.length, itemBuilder: (context, index) => ItemTile( key: ValueKey(provider.items[index].id), item: provider.items[index], ), ), ), ), ], ); } }
// Use RepaintBoundary for expensive widgets class ExpensiveAnimation extends StatelessWidget { @override Widget build(BuildContext context) { return RepaintBoundary( child: CustomPaint( painter: ComplexAnimationPainter(), size: const Size(200, 200), ), ); } }
// Optimize images class OptimizedImage extends StatelessWidget { final String url;
@override Widget build(BuildContext context) { return CachedNetworkImage( imageUrl: url, // Resize to actual display size memCacheWidth: 200, memCacheHeight: 200, placeholder: (context, url) => const ShimmerPlaceholder(), errorWidget: (context, url, error) => const Icon(Icons.error), ); } }
Dart Patterns
Null Safety and Pattern Matching
// Pattern matching with Dart 3 sealed class Result<T> { const Result(); }
class Success<T> extends Result<T> { final T data; const Success(this.data); }
class Failure<T> extends Result<T> { final Exception error; const Failure(this.error); }
// Using pattern matching void handleResult(Result<User> result) { switch (result) { case Success(data: final user): print('User: ${user.name}'); case Failure(error: final e): print('Error: $e'); } }
// Records for multiple return values (String, int) getUserInfo() { return ('John', 30); }
void main() { final (name, age) = getUserInfo(); print('$name is $age years old'); }
Extension Methods
extension StringExtensions on String { String get capitalize => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
bool get isValidEmail => RegExp(r'^[\w-.]+@([\w-]+.)+[\w-]{2,4}$').hasMatch(this); }
extension ContextExtensions on BuildContext { ThemeData get theme => Theme.of(this); TextTheme get textTheme => theme.textTheme; ColorScheme get colorScheme => theme.colorScheme;
void showSnackBar(String message) { ScaffoldMessenger.of(this).showSnackBar( SnackBar(content: Text(message)), ); } }
// Usage final email = 'test@example.com'; if (email.isValidEmail) { context.showSnackBar('Valid email!'); }
Async Patterns
// Proper async error handling Future<Result<User>> fetchUser(String id) async { try { final response = await dio.get('/users/$id'); return Success(User.fromJson(response.data)); } on DioException catch (e) { return Failure(NetworkException(e.message ?? 'Network error')); } catch (e) { return Failure(UnknownException(e.toString())); } }
// Stream transformations Stream<List<Message>> watchMessages(String chatId) { return firestore .collection('chats') .doc(chatId) .collection('messages') .orderBy('timestamp', descending: true) .limit(50) .snapshots() .map((snapshot) => snapshot.docs .map((doc) => Message.fromFirestore(doc)) .toList()); }
References
-
dart-patterns.md - Dart 3.x patterns, null safety, async
-
widget-architecture.md - Widget lifecycle, keys, render objects
-
state-management.md - Provider, Riverpod, Bloc comparison
Tools Integration
This skill uses Python tools from the senior-mobile skill:
Generate Flutter project structure
python3 ../../senior-mobile/scripts/mobile_scaffolder.py
--framework flutter
--navigation go-router
--state riverpod
Detect Flutter project configuration
python3 ../../senior-mobile/scripts/platform_detector.py --check all
Validate for Play Store
python3 ../../senior-mobile/scripts/app_store_validator.py --store google
Best Practices
Widget Design
-
Keep widgets small and focused (single responsibility)
-
Use composition over inheritance
-
Implement const constructors
-
Separate logic from UI
-
Use proper keys for dynamic lists
State Management
-
Choose one pattern and use consistently
-
Keep state as local as possible
-
Avoid global state when possible
-
Test business logic independently
Performance
-
Profile before optimizing
-
Use lazy loading for large lists
-
Implement image caching
-
Minimize widget rebuilds
-
Use isolates for heavy computation
Testing
-
Unit test business logic
-
Widget test UI components
-
Integration test critical flows
-
Use golden tests for visual regression
Success Metrics
-
UI Development Speed: 55% faster with hot reload
-
Code Sharing: 90%+ across platforms
-
App Performance: 60 FPS on mid-range devices
-
Test Coverage: 80%+ for business logic