Claude Code for Flutter: Cross-Platform Mobile and Web Development — Claude Skills 360 Blog
Blog / Mobile / Claude Code for Flutter: Cross-Platform Mobile and Web Development
Mobile

Claude Code for Flutter: Cross-Platform Mobile and Web Development

Published: November 10, 2026
Read time: 9 min read
By: Claude Skills 360

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.

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