Claude Code for Flutter Advanced: State Management, Riverpod, and Performance — Claude Skills 360 Blog
Blog / Mobile / Claude Code for Flutter Advanced: State Management, Riverpod, and Performance
Mobile

Claude Code for Flutter Advanced: State Management, Riverpod, and Performance

Published: January 1, 2027
Read time: 9 min read
By: Claude Skills 360

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.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free