Flutter
Purpose
Build Flutter cross-platform applications with clean architecture, Riverpod or BLoC state management, GoRouter navigation, and layered testing.
Agent Protocol
Trigger
User request includes: flutter, dart, flutter widget, flutter architecture, flutter state, flutter testing, flutter layout, flutter navigation, material design, cupertino.
Input Context
- Flutter SDK version (stable/beta/master)
- Dart version
- State management (Provider, Riverpod, BLoC, GetX)
- Platform target (iOS, Android, Web, Desktop)
- Architecture (Clean Architecture, MVC, TDD)
Output Artifact
A markdown document containing:
- Project structure
- Widget tree / component hierarchy
- State management setup
- Navigation/routing config
- Data layer (repository, model, API)
- Test plan
Response Format
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
Max Response Length
4096 tokens
Workflow
Step 1: Set Up Project Structure
Organize code with feature-first architecture: core, features (data/domain/presentation), and app-level config.
Step 2: Choose State Management
Select Riverpod for scalable reactive state or BLoC for event-driven architectures with clear state transitions.
Step 3: Configure Navigation
Set up GoRouter with declarative routing, nested routes, and deep linking support.
Step 4: Implement Data Layer
Use repository pattern with remote and local data sources, model mapping, and error handling.
Step 5: Write Tests
Cover business logic with unit tests, widgets with widget tests, and flows with integration tests.
Architecture Decision Trees
State Management Selection
App complexity?
├── Simple (<10 screens)
│ → Provider (simple, well-known) or setState (for forms/settings)
├── Medium (10-30 screens, moderate state)
│ → Riverpod — compile-safe, auto-dispose, test-friendly
├── Complex (real-time, event-heavy, large team)
│ → BLoC — explicit events/states, best for event-sourced architectures
└── High-frequency data (chat, trading, gaming)
→ BLoC or Riverpod Notifier (avoid setState, which rebuilds entire widget tree)
Navigation Strategy
Platform target?
├── Mobile + Web
│ → GoRouter — declarative, deep linking, URL-based routes
├── Mobile only
│ → GoRouter or auto_route (code-gen, type-safe)
├── Desktop
│ → GoRouter with ShellRoute for nested navigation
└── Simple app (2-3 screens)
→ Navigator 2.0 or GetX navigation (minimal boilerplate)
Platform Channel vs FFI
Need native platform access?
├── Simple (sensor reading, battery level) → MethodChannel
├── Complex (ML inference, audio processing) → FFI (dart:ffi)
├── Third-party SDK (payment, maps) → Flutter plugin from pub.dev
└── Custom UI component (native map, camera) → PlatformView
Project Structure
lib/
├── app/
│ ├── app.dart
│ └── router.dart
├── core/
│ ├── constants/
│ ├── errors/
│ ├── network/
│ ├── theme/
│ └── utils/
├── features/
│ ├── orders/
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ └── presentation/
│ │ ├── providers/
│ │ ├── screens/
│ │ └── widgets/
│ └── ...
└── main.dart
test/
├── unit/
├── widget/
└── integration/
State Management — Riverpod
// lib/features/orders/presentation/providers/order_provider.dart
@riverpod
class OrderList extends _$OrderList {
@override
Future<List<Order>> build() async {
final repo = ref.watch(orderRepositoryProvider);
return repo.getOrders();
}
Future<void> refresh() async => ref.invalidateSelf();
}
// StateNotifierProvider for mutable state
@riverpod
class OrderFilter extends _$OrderFilter {
@override
OrderFilterState build() => const OrderFilterState();
void setStatus(OrderStatus? status) => state = state.copyWith(status: status);
void setQuery(String q) => state = state.copyWith(query: q);
}
// Usage in widget
class OrderListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final ordersAsync = ref.watch(orderListProvider);
final filter = ref.watch(orderFilterProvider);
return ordersAsync.when(
data: (orders) => ListView.builder(
itemCount: orders.length,
itemBuilder: (_, i) => OrderCard(order: orders[i]),
),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
State Management — BLoC
// lib/features/orders/presentation/bloc/order_bloc.dart
class OrderBloc extends Bloc<OrderEvent, OrderState> {
final OrderRepository _repo;
OrderBloc(this._repo) : super(OrderInitial()) {
on<LoadOrders>(_onLoadOrders);
on<FilterOrders>(_onFilterOrders);
}
Future<void> _onLoadOrders(LoadOrders event, Emitter<OrderState> emit) async {
emit(OrderLoading());
try {
final orders = await _repo.getOrders();
emit(OrderLoaded(orders));
} catch (e) {
emit(OrderError(e.toString()));
}
}
Future<void> _onFilterOrders(FilterOrders event, Emitter<OrderState> emit) async {
final current = state;
if (current is OrderLoaded) {
final filtered = current.orders.where((o) => o.status == event.status).toList();
emit(OrderLoaded(filtered));
}
}
}
Navigation — GoRouter
// lib/app/router.dart
final router = GoRouter(
initialLocation: '/orders',
routes: [
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(
path: '/orders',
builder: (_, __) => const OrderListScreen(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) => OrderDetailScreen(
orderId: state.pathParameters['id']!,
),
),
],
),
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
],
),
],
);
Data Layer — Repository Pattern
// lib/features/orders/data/repositories/order_repository_impl.dart
class OrderRepositoryImpl implements OrderRepository {
final OrderRemoteDataSource remote;
final OrderLocalDataSource local;
OrderRepositoryImpl({required this.remote, required this.local});
@override
Future<List<Order>> getOrders() async {
try {
final models = await remote.fetchOrders();
await local.cacheOrders(models);
return models.map((m) => m.toEntity()).toList();
} catch (e) {
final cached = await local.getCachedOrders();
if (cached.isNotEmpty) return cached.map((c) => c.toEntity()).toList();
rethrow;
}
}
@override
Future<Order> getOrder(String id) async {
try {
return (await remote.fetchOrder(id)).toEntity();
} catch (_) {
final cached = await local.getCachedOrder(id);
if (cached != null) return cached.toEntity();
rethrow;
}
}
}
Dependency Injection — Riverpod
// Providers central
final dioProvider = Provider<Dio>((ref) => Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
)));
final remoteDataSourceProvider = Provider<OrderRemoteDataSource>((ref) {
return OrderRemoteDataSourceImpl(ref.watch(dioProvider));
});
final localDataSourceProvider = Provider<OrderLocalDataSource>((ref) {
return OrderLocalDataSourceImpl(ref.watch(databaseProvider));
});
final orderRepositoryProvider = Provider<OrderRepository>((ref) {
return OrderRepositoryImpl(
remote: ref.watch(remoteDataSourceProvider),
local: ref.watch(localDataSourceProvider),
);
});
Testing
// test/unit/order_repository_test.dart
void main() {
late MockOrderRemoteDataSource mockRemote;
late MockOrderLocalDataSource mockLocal;
late OrderRepositoryImpl repo;
setUp(() {
mockRemote = MockOrderRemoteDataSource();
mockLocal = MockOrderLocalDataSource();
repo = OrderRepositoryImpl(remote: mockRemote, local: mockLocal);
});
group('getOrders', () {
test('returns orders from remote and caches locally', () async {
final models = [OrderModel(id: '1', customerName: 'Test')];
when(() => mockRemote.fetchOrders()).thenAnswer((_) async => models);
when(() => mockLocal.cacheOrders(any())).thenAnswer((_) async => {});
final result = await repo.getOrders();
expect(result.length, 1);
verify(() => mockLocal.cacheOrders(models)).called(1);
});
test('falls back to cache when remote fails', () async {
when(() => mockRemote.fetchOrders()).thenThrow(Exception('Network error'));
when(() => mockLocal.getCachedOrders()).thenAnswer((_) async => [
CachedOrder(id: '1', customerName: 'Cached'),
]);
final result = await repo.getOrders();
expect(result.length, 1);
expect(result.first.customerName, 'Cached');
});
test('rethrows when both remote and cache fail', () async {
when(() => mockRemote.fetchOrders()).thenThrow(Exception('Network error'));
when(() => mockLocal.getCachedOrders()).thenAnswer((_) async => []);
expect(() => repo.getOrders(), throwsA(isA<Exception>()));
});
});
}
Platform Channels
// lib/core/platform/payment_channel.dart
class PaymentChannel {
static const _channel = MethodChannel('com.example.app/payment');
static Future<String> processPayment(double amount, String currency) async {
try {
final result = await _channel.invokeMethod<String>('processPayment', {
'amount': amount,
'currency': currency,
});
return result ?? '';
} on MissingPluginException {
throw UnsupportedError('Payment not available on this platform');
}
}
}
Flutter Performance Patterns
- Use
constconstructors on all widgets that don't mutate RepaintBoundaryfor widgets that repaint independently (maps, animations)ListView.builderoverListView(children:)for large lists (lazy construction)AnimatedBuilderorValueListenableBuilderfor local animations — avoidsetStateon parentImageCache.maximumSizeset to ~100MB for image-heavy appsshouldRepaintreturns false in custom painters when visual output unchangedOpacitywidget is expensive — useAnimatedOpacityor draw with alpha in canvas- Profile builds only — never measure performance on debug builds (JIT is 2-5x slower)
- Use
flutter build apk --split-debug-infoto reduce release binary size
Platform Considerations
- iOS:
platform_channels_iosfor Swift/Obj-C interop - Android:
platform_channels_androidfor Kotlin interop, FlutterEngineGroup for multiple engines - Web:
dart:htmlis deprecated — usepackage:webfor DOM access - Desktop:
window_managerfor window controls,system_trayfor tray icons - Keyboard:
Shortcuts+Actionswidget for keyboard shortcuts (desktop/web)
Pubspec Strategy
dependencies:
flutter:
sdk: flutter
# State management
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
# Navigation
go_router: ^14.0.0
# Network
dio: ^5.4.0
# Local storage
drift: ^2.18.0
sqlite3_flutter_libs: ^0.5.0
# Code gen
freezed_annotation: ^2.4.0
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.0
freezed: ^2.5.0
json_serializable: ^6.8.0
riverpod_generator: ^2.4.0
mockito: ^5.4.0
drift_dev: ^2.18.0
Anti-Patterns
- Giant Widget build methods: Extract reusable widgets — one widget = one screen section
- setState for everything: Use state management for shared state;
setStatefor local form state only - GlobalKey misuse: Use keys for widget identity, not for accessing state from outside
- Business logic in Widgets: Extract to providers/blocs — widgets should be pure UI + event dispatch
- Navigator.push everywhere: Use GoRouter's context.go() for URL-based navigation
- Streams without disposal: Riverpod auto-disposes, but raw StreamSubscription must be cancelled in dispose()
- Infinite ListView without builders: Always use
ListView.builder,GridView.builder, orCustomScrollView - Asset loading on main thread: Use
compute()orIsolatefor heavy asset processing (image decoding) - Ignoring null safety: Use
requiredkeyword, nullable types with??and?., avoidlatewithout guarantees
Rules
- Prefer Riverpod over BLoC for most apps — BLoC overhead justified only for event-heavy features
- Use const constructors on all widgets that don't mutate — enables Flutter's rebuild optimization
- Repository pattern must cache-fallback on network failure — never surface network errors directly
- BlocListener for side effects (navigation, snackbar), BlocBuilder for UI
- GoRouter routes defined in one file — never scatter route definitions across features
- All async operations must handle loading, error, and data states in the UI
- Keep business logic out of widgets — use providers/blocs for all stateful logic
- Platform channels must have web fallback handlers for Flutter web
- Use freezed for immutable data classes and sealed unions
- Run
flutter analyzebefore every commit — zero warnings policy - Profile builds for performance measurement — never use debug builds
References
- references/architecture.md — Flutter Architecture
- references/flutter-advanced.md — Flutter Advanced Topics
- references/flutter-fundamentals.md — Flutter Fundamentals
- references/platform-channels.md — Flutter Platform Channels
- references/state-management-provider.md — Flutter State Management with Provider
- references/state-management.md — Flutter State Management
- references/testing.md — Flutter Testing
- references/widgets.md — Flutter Widgets
Handoff
Hand off to mobile/universal/deployment/SKILL.md for build and deployment.
Source: j4flmao/agent-skills — distributed by TomeVault.