Flutter compiles Dart to native ARM code for iOS and Android, and to JavaScript/Wasm for web — a single codebase for all platforms. Its widget model is declarative: the UI is a function of state, and widgets rebuild efficiently when state changes. Flutter’s rendering engine draws every pixel itself, bypassing platform UI widgets entirely, which means pixel-perfect consistency across platforms. Claude Code writes Flutter widgets, Riverpod state management, go_router navigation, and the platform channel code that calls native APIs when needed.
CLAUDE.md for Flutter Projects
## Flutter Stack
- Flutter 3.x (latest stable channel)
- State: Riverpod 2.x (AsyncNotifier, NotifierProvider)
- Navigation: go_router 13.x — declarative URL routing
- HTTP: dio with retrofit code generation
- Local storage: flutter_secure_storage (secrets), Hive (structured data)
- Testing: flutter_test (widget tests), integration_test (E2E on device)
- Theming: Material 3 with custom ColorScheme
- No direct setState in business logic — all state in Providers
Widget Architecture
// lib/features/orders/order_list_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';
class OrderListScreen extends ConsumerWidget {
const OrderListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ordersAsync = ref.watch(ordersProvider);
return Scaffold(
appBar: AppBar(
title: const Text('My Orders'),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () => context.push('/orders/filter'),
),
],
),
body: ordersAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorView(
message: error.toString(),
onRetry: () => ref.refresh(ordersProvider),
),
data: (orders) => orders.isEmpty
? const EmptyOrdersView()
: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, index) => OrderCard(
order: orders[index],
onTap: () => context.push('/orders/${orders[index].id}'),
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => context.push('/orders/new'),
icon: const Icon(Icons.add),
label: const Text('New Order'),
),
);
}
}
// Reusable order card widget — no state, just renders
class OrderCard extends StatelessWidget {
final Order order;
final VoidCallback onTap;
const OrderCard({super.key, required this.order, required this.onTap});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
onTap: onTap,
title: Text(
'#${order.id.substring(0, 8)}',
style: theme.textTheme.titleMedium,
),
subtitle: Text(order.createdAt.toLocal().toString()),
trailing: OrderStatusBadge(status: order.status),
leading: CircleAvatar(
backgroundColor: theme.colorScheme.primaryContainer,
child: Text(_totalDisplay(order.totalCents)),
),
),
);
}
String _totalDisplay(int cents) => '\$${(cents / 100).toStringAsFixed(0)}';
}
Riverpod State Management
// lib/features/orders/providers/orders_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/order.dart';
import '../repositories/orders_repository.dart';
part 'orders_provider.g.dart';
// AsyncNotifier: async state with loading/error/data lifecycle
@riverpod
class Orders extends _$Orders {
@override
Future<List<Order>> build() async {
// Called on first watch and on invalidation
return ref.watch(ordersRepositoryProvider).getOrders();
}
Future<void> createOrder(CreateOrderInput input) async {
// Optimistic update
final previous = state;
state = const AsyncValue.loading();
try {
final newOrder = await ref.read(ordersRepositoryProvider).createOrder(input);
// Update list with new order at front
final current = await future;
state = AsyncValue.data([newOrder, ...current]);
} catch (err, stack) {
state = AsyncValue.error(err, stack);
// Rollback on error
state = previous;
}
}
Future<void> cancelOrder(String orderId) async {
await ref.read(ordersRepositoryProvider).cancelOrder(orderId);
ref.invalidateSelf(); // Refetch list
}
}
// Simple provider for filtered/derived state
@riverpod
List<Order> pendingOrders(PendingOrdersRef ref) {
return ref
.watch(ordersProvider)
.valueOrNull
?.where((o) => o.status == OrderStatus.pending)
.toList() ?? [];
}
go_router Navigation
// lib/routing/router.dart
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull?.isLoggedIn ?? false;
final isAuthPage = state.matchedLocation.startsWith('/login');
if (!isLoggedIn && !isAuthPage) return '/login';
if (isLoggedIn && isAuthPage) return '/';
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'orders',
builder: (context, state) => const OrderListScreen(),
routes: [
GoRoute(
path: 'new',
builder: (context, state) => const CreateOrderScreen(),
),
GoRoute(
path: ':orderId',
builder: (context, state) => OrderDetailScreen(
orderId: state.pathParameters['orderId']!,
),
),
],
),
],
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
);
});
Responsive Layout
// lib/shared/responsive_layout.dart — tablet/phone/desktop adaptation
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget? desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200 && desktop != null) return desktop!;
if (constraints.maxWidth >= 600 && tablet != null) return tablet!;
return mobile;
},
);
}
}
// Adaptive order list: side-by-side on tablet, stacked on phone
class OrdersLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
mobile: const OrderListScreen(),
tablet: const Row(
children: [
SizedBox(width: 360, child: OrderListScreen()),
Expanded(child: OrderDetailPlaceholder()),
],
),
);
}
}
Widget Testing
// test/features/orders/order_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
group('OrderCard', () {
testWidgets('displays order ID and status', (tester) async {
final order = Order(
id: 'abc12345-0000-0000-0000-000000000000',
status: OrderStatus.shipped,
totalCents: 2999,
createdAt: DateTime(2026, 11, 1),
items: [],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OrderCard(order: order, onTap: () {}),
),
),
);
expect(find.text('#abc12345'), findsOneWidget);
expect(find.text('shipped'), findsOneWidget);
expect(find.text('\$29'), findsOneWidget);
});
testWidgets('calls onTap when tapped', (tester) async {
var tapped = false;
final order = Order.fake();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OrderCard(order: order, onTap: () { tapped = true; }),
),
),
);
await tester.tap(find.byType(OrderCard));
expect(tapped, isTrue);
});
});
}
For the React Native alternative to Flutter for sharing JavaScript logic with web, the React Native guide covers Expo Router and NativeWind patterns. For the mobile CI/CD that distributes Flutter builds to TestFlight and Play Store, the mobile CI/CD guide covers Fastlane lanes and GitHub Actions for Flutter. The Claude Skills 360 bundle includes Flutter skill sets covering Riverpod state management, go_router navigation, widget testing, and responsive layouts. Start with the free tier to try Flutter widget generation.