Flutter renders to its own canvas — no native UI components, same code for iOS, Android, Web, and desktop. Riverpod 2’s AsyncNotifier handles async state with loading/error/data states. go_router provides type-safe navigation with deep links. Isar stores structured data locally with zero-overhead Dart queries. Platform channels bridge Flutter to native Swift/Kotlin APIs. Claude Code generates Flutter widgets, Riverpod providers, go_router configuration, platform channel implementations, and widget tests for production cross-platform applications.
CLAUDE.md for Flutter Projects
## Flutter Stack
- Version: Flutter 3.24+, Dart 3.5+
- State: Riverpod 2 (flutter_riverpod + riverpod_annotation) — NO Provider or BLoC
- Navigation: go_router 14+ with typed routes
- Local storage: Isar (structured), flutter_secure_storage (secrets)
- Network: Dio with interceptors, Retrofit for type-safe API clients
- DI: Riverpod providers (no get_it)
- Testing: flutter_test (widgets) + integration_test (E2E)
- Architecture: Feature-first folders with data/domain/presentation layers
- gen: build_runner + riverpod_generator + freezed
Feature-First Project Structure
lib/
├── main.dart
├── app.dart # App widget, router config
├── common/
│ ├── widgets/
│ └── extensions/
└── features/
└── orders/
├── data/
│ ├── order_repository.dart
│ └── dtos/
├── domain/
│ ├── models/order.dart
│ └── order_repository_interface.dart
└── presentation/
├── orders_screen.dart
├── order_detail_screen.dart
└── providers/
├── orders_provider.dart
└── order_detail_provider.dart
Freezed Data Models
// features/orders/domain/models/order.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'order.freezed.dart';
part 'order.g.dart';
enum OrderStatus { pending, processing, shipped, delivered, cancelled }
@freezed
class Order with _$Order {
const factory Order({
required String id,
required String customerId,
required OrderStatus status,
required int totalCents,
required List<OrderItem> items,
required DateTime createdAt,
DateTime? deliveredAt,
String? trackingNumber,
@Default(false) bool isHighValue,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
}
@freezed
class OrderItem with _$OrderItem {
const factory OrderItem({
required String productId,
required String productName,
required int quantity,
required int priceCents,
}) = _OrderItem;
factory OrderItem.fromJson(Map<String, dynamic> json) => _$OrderItemFromJson(json);
}
Riverpod AsyncNotifier
// features/orders/presentation/providers/orders_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/models/order.dart';
import '../../data/order_repository.dart';
part 'orders_provider.g.dart';
// AsyncNotifier for list of orders with CRUD operations
@riverpod
class Orders extends _$Orders {
@override
Future<List<Order>> build() async {
// Runs when provider is first read — auto-disposes if unused
return ref.watch(orderRepositoryProvider).getOrders();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(
() => ref.read(orderRepositoryProvider).getOrders(),
);
}
Future<void> cancelOrder(String orderId) async {
final repo = ref.read(orderRepositoryProvider);
// Optimistic update — update state before server confirms
state = AsyncValue.data(
state.value!.map((o) => o.id == orderId
? o.copyWith(status: OrderStatus.cancelled)
: o
).toList(),
);
try {
await repo.cancelOrder(orderId);
} catch (e) {
// Rollback on failure and re-fetch
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => repo.getOrders());
rethrow;
}
}
Future<Order> createOrder(CreateOrderRequest request) async {
final repo = ref.read(orderRepositoryProvider);
final order = await repo.createOrder(request);
state = AsyncValue.data([order, ...state.value ?? []]);
return order
}
}
// Family provider: single order by ID
@riverpod
Future<Order> orderDetail(OrderDetailRef ref, String orderId) {
return ref.watch(orderRepositoryProvider).getOrder(orderId);
}
Orders Screen with Async State
// features/orders/presentation/orders_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/orders_provider.dart';
import '../../domain/models/order.dart';
class OrdersScreen extends ConsumerWidget {
const OrdersScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ordersAsync = ref.watch(ordersProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Orders'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => ref.read(ordersProvider.notifier).refresh(),
),
],
),
body: RefreshIndicator(
onRefresh: () => ref.read(ordersProvider.notifier).refresh(),
child: ordersAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => ref.read(ordersProvider.notifier).refresh(),
child: const Text('Retry'),
),
],
),
),
data: (orders) => orders.isEmpty
? const Center(child: Text('No orders yet'))
: ListView.builder(
itemCount: orders.length,
itemBuilder: (ctx, i) => OrderCard(
order: orders[i],
onTap: () => context.push('/orders/${orders[i].id}'),
onCancel: orders[i].status == OrderStatus.pending
? () => ref.read(ordersProvider.notifier).cancelOrder(orders[i].id)
: null,
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.push('/orders/new'),
child: const Icon(Icons.add),
),
);
}
}
class OrderCard extends StatelessWidget {
const OrderCard({
super.key,
required this.order,
required this.onTap,
this.onCancel,
});
final Order order;
final VoidCallback onTap;
final VoidCallback? onCancel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'#${order.id.substring(order.id.length - 6)}',
style: theme.textTheme.titleSmall?.copyWith(
fontFamily: 'monospace',
),
),
const SizedBox(height: 4),
Text(
'\$${(order.totalCents / 100).toStringAsFixed(2)}',
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_StatusChip(status: order.status),
if (onCancel != null) ...[
const SizedBox(height: 8),
TextButton(
onPressed: onCancel,
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Cancel'),
),
],
],
),
],
),
),
),
);
}
}
Type-Safe Navigation with go_router
// app/router.dart — typed routes with go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/orders/presentation/orders_screen.dart';
import '../features/orders/presentation/order_detail_screen.dart';
// Typed route definitions
class OrdersRoute extends GoRouteData {
const OrdersRoute();
@override
Widget build(BuildContext context, GoRouterState state) =>
const OrdersScreen();
}
class OrderDetailRoute extends GoRouteData {
const OrderDetailRoute({required this.orderId});
final String orderId;
@override
Widget build(BuildContext context, GoRouterState state) =>
OrderDetailScreen(orderId: orderId);
}
final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/orders',
routes: [
GoRoute(
path: '/orders',
builder: (ctx, state) => const OrdersScreen(),
routes: [
GoRoute(
path: ':orderId',
builder: (ctx, state) => OrderDetailScreen(
orderId: state.pathParameters['orderId']!,
),
),
],
),
],
redirect: (context, state) async {
// Auth guard
final isLoggedIn = await checkAuth();
final isLoginRoute = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/orders';
return null;
},
);
});
Platform Channels
// services/biometrics_service.dart — platform channel for native biometrics
import 'package:flutter/services.dart';
class BiometricsService {
static const _channel = MethodChannel('com.myapp/biometrics');
Future<bool> authenticate({
required String reason,
}) async {
try {
final result = await _channel.invokeMethod<bool>(
'authenticate',
{'reason': reason, 'useErrorDialogs': true},
);
return result ?? false;
} on PlatformException catch (e) {
if (e.code == 'NotAvailable') return false;
rethrow;
}
}
}
// ios/Runner/AppDelegate.swift — Swift side
import LocalAuthentication
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(
name: "com.myapp/biometrics",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { call, result in
guard call.method == "authenticate" else {
result(FlutterMethodNotImplemented)
return
}
let args = call.arguments as! [String: Any]
let reason = args["reason"] as! String
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
result(FlutterError(code: "NotAvailable", message: error?.localizedDescription, details: nil))
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
DispatchQueue.main.async { result(success) }
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
For the React Native alternative that uses native UI components instead of a custom renderer, see the React Native guide and React Native Expo guide for JavaScript-based cross-platform development. For the Jetpack Compose Android-first approach that Flutter competes with on Android, the Android Kotlin guide covers native Android patterns. The Claude Skills 360 bundle includes Flutter skill sets covering Riverpod, go_router, platform channels, and testing. Start with the free tier to try Flutter widget generation.