Mobile No. 334

Flutter Widgets skill for Claude Code

Small const widgets, one state owner, no overflow stripes. Claude writes Flutter UI as small const widget classes instead of helper methods, places state deliberately with setState, Riverpod 3 or Cubit, and fixes layout exceptions from the constraints model rather than by trial and error. Async code checks context.mounted after every await, native calls go through Pigeon, and jank is diagnosed in profile mode with DevTools.

  • nameflutter-widgets
  • size219 lines · ≈2,300 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds Flutter apps with small const widget classes, correct layout under the constraints model, keys for stateful lists, state management chosen deliberately (setState, Riverpod 3, Bloc/Cubit), go_router navigation, safe async with context.mounted, and platform channels via Pigeon or MethodChannel. Diagnoses RenderFlex overflow and unbounded-height errors and janky rebuilds with DevTools. Use when writing or refactoring Flutter widgets, picking or migrating a state library, fixing layout exceptions, or calling native iOS/Android code from Dart.

Try asking

  • Fix the 'Vertical viewport was given unbounded height' error on this screen
  • Move this screen's state from setState to Riverpod with loading and error states
  • Call the native iOS and Android battery API from Flutter with type safety

Use it when

  • Writing or refactoring Flutter widgets and screens
  • Choosing or migrating between setState, Riverpod, Bloc and Provider
  • Layout throws overflow or unbounded constraint errors
  • Calling platform code or adding go_router navigation

Skip it when

How to check the result

flutter analyze and widget tests pass, screens show no overflow at maximum font scale on small phones, and profile-mode scrolling stays within the frame budget in DevTools.

The common mistake it prevents

Creating the future inside build for a FutureBuilder, which restarts the request on every rebuild and flickers between loading and data.

~/.claude/skills/flutter-widgets/SKILL.md

--- name: flutter-widgets description: Builds Flutter apps with small const widget classes, correct layout under the constraints model, keys for stateful lists, state management… license: MIT ---

Good Flutter code is a tree of small, mostly const widget classes; state lives in one deliberate place per concern; layout respects “constraints go down, sizes go up, parent sets position”; async gaps never touch a dead BuildContext; and performance is judged in profile mode, not debug.

Before changing anything

flutter --version
flutter pub deps --style=compact | grep -E "riverpod|bloc|provider|go_router|freezed" 
flutter analyze                     # baseline warnings
grep -n "include:" analysis_options.yaml   # flutter_lints / very_good_analysis

Follow the state library already in the project; do not introduce a second one for a single feature.

Workflow

- [ ] 1. Sketch the widget tree; mark what is static (const) vs. state-dependent
- [ ] 2. Place state: ephemeral (setState) vs. app state (Riverpod/Bloc)
- [ ] 3. Build small widget classes, const constructors, keys where identity matters
- [ ] 4. Layout with Expanded/Flexible/LayoutBuilder; slivers for complex scrolling
- [ ] 5. Async: loading/error/data states; context.mounted after awaits
- [ ] 6. Navigation via go_router routes and redirects
- [ ] 7. dart format, flutter analyze, widget tests, profile-mode check

Widget composition rules

  • Extract widgets as classes, not helper methods. Widget _buildHeader() rebuilds with its parent and cannot be const; class _Header extends StatelessWidget can.
  • Add const constructors and use const instances wherever inputs are compile-time constants; const subtrees are skipped on rebuild.
  • Pass only the data a widget needs, not whole models or ref/context-derived objects.
  • StatefulWidget only for ephemeral UI state (animation controllers, text controllers, expanded flags). Dispose controllers, focus nodes, and subscriptions in dispose().
  • Use ValueKey(item.id) on list items that hold state or can reorder; without keys Flutter matches by position and state jumps between rows. Avoid GlobalKey except for forms or rare cross-tree access.

Layout: constraints model and common errors

ErrorCauseFix
“A RenderFlex overflowed by N pixels”Row/Column children wider/taller than availableWrap flexible children in Expanded/Flexible; overflow: TextOverflow.ellipsis; make it scroll
“Vertical viewport was given unbounded height”ListView inside a ColumnWrap the list in Expanded, or use one CustomScrollView with slivers
“BoxConstraints forces an infinite width”TextField/Expanded inside an unconstrained RowGive it Expanded or a fixed width
“Incorrect use of ParentDataWidget”Expanded outside a Flex parentOnly use Expanded/Flexible directly inside Row/Column/Flex
Content under notch/home indicatorNo insets appliedSafeArea or Scaffold + MediaQuery.paddingOf(context)
  • Read size with MediaQuery.sizeOf(context) (rebuilds only on size changes), not MediaQuery.of(context).size.
  • Responsive components use LayoutBuilder (parent constraints), not screen size.
  • Complex scroll screens (collapsing header + grid + list): CustomScrollView with SliverAppBar, SliverGrid, SliverList.builder.
  • Long lists always use builders (ListView.builder, SliverList.builder), never ListView(children: bigList.map(...)).

State management choice

ScopeChoose
Ephemeral UI state in one widgetsetState / controllers
Simple shared value, no packageValueNotifier + ValueListenableBuilder, or ChangeNotifier + ListenableBuilder
App state, async data, dependency injection, testabilityRiverpod 3 (default for new projects)
Teams wanting explicit event → state transitions and strict layeringBloc / Cubit
Existing Provider codebaseKeep Provider; migrate gradually if needed

Riverpod 3 (code generation)

// orders_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'orders_provider.g.dart';

@riverpod
class Orders extends _$Orders {
  @override
  Future<List<Order>> build({String query = ''}) async {
    final api = ref.watch(ordersApiProvider);
    return api.fetchOrders(query: query);
  }

  Future<void> archive(String id) async {
    await ref.read(ordersApiProvider).archive(id);
    ref.invalidateSelf();                     // refetch
  }
}
class OrdersList extends ConsumerWidget {
  const OrdersList({super.key, required this.query});
  final String query;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final orders = ref.watch(ordersProvider(query: query));
    return switch (orders) {
      AsyncData(:final value) when value.isEmpty => const EmptyOrders(),
      AsyncData(:final value) => ListView.builder(
          itemCount: value.length,
          itemBuilder: (_, i) => OrderTile(key: ValueKey(value[i].id), order: value[i]),
        ),
      AsyncError(:final error) => ErrorRetry(message: '$error', onRetry: () => ref.invalidate(ordersProvider)),
      _ => const Center(child: CircularProgressIndicator()),
    };
  }
}
  • ref.watch in build; ref.read in callbacks; ref.listen for side effects (snackbars, navigation). Never ref.read in build to avoid rebuilds — it causes stale UI.
  • Run code generation: dart run build_runner watch --delete-conflicting-outputs.
  • Override providers in tests and previews with ProviderScope(overrides: [...]).

Bloc / Cubit

class CartCubit extends Cubit<CartState> {
  CartCubit(this._repo) : super(const CartState());
  final CartRepository _repo;

  Future<void> add(Product p) async {
    emit(state.copyWith(status: CartStatus.saving));
    try {
      final cart = await _repo.add(p.id);
      emit(state.copyWith(status: CartStatus.ready, items: cart.items));
    } catch (e) {
      emit(state.copyWith(status: CartStatus.failure, error: '$e'));
    }
  }
}

States must be immutable with value equality (freezed or equatable), or BlocBuilder will not rebuild / will rebuild unnecessarily. Use BlocSelector or context.select to rebuild on one field.

Async and BuildContext

Future<void> _save() async {
  final messenger = ScaffoldMessenger.of(context);  // capture before the gap, or check mounted
  final ok = await repo.save(form);
  if (!context.mounted) return;                     // widget may be gone after await
  if (ok) context.pop(); else messenger.showSnackBar(const SnackBar(content: Text('Could not save')));
}
  • Never create the future inside build for FutureBuilder (future: api.load()); it refires on every rebuild. Create it in initState or use a provider.
  • Heavy CPU work (JSON parsing of large payloads, image processing) goes off the UI isolate with Isolate.run(() => parse(bytes)).
final router = GoRouter(
  refreshListenable: authNotifier,
  redirect: (context, state) {
    final signedIn = authNotifier.isSignedIn;
    final atLogin = state.matchedLocation == '/login';
    if (!signedIn && !atLogin) return '/login?from=${Uri.encodeComponent(state.uri.toString())}';
    if (signedIn && atLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/', builder: (_, __) => const HomeScreen(), routes: [
      GoRoute(path: 'orders/:id', builder: (_, s) => OrderScreen(id: s.pathParameters['id']!)),
    ]),
    GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
  ],
);

context.go replaces the stack (tabs, deep links); context.push adds a page (detail screens). Pass IDs in paths, not objects. Deep link platform setup: deep-links.

Platform channels

  • Prefer an existing plugin from pub.dev (check platform support and maintenance).
  • For custom native code, use Pigeon to generate type-safe Dart/Swift/Kotlin interfaces instead of stringly typed MethodChannel calls.
  • Raw MethodChannel: same channel name on both sides, handle PlatformException and MissingPluginException in Dart, and call results on the platform main thread.
  • Streams from native (sensors, location) use EventChannel; cancel the subscription in dispose().

Performance

  1. Profile in profile mode on a real device: flutter run --profile, then DevTools → Performance (frame chart) and the rebuild tracker. Debug mode is not representative.
  2. Common fixes: const widgets, smaller rebuild scope (Consumer/select deep in the tree), ListView.builder, RepaintBoundary around frequently repainting regions, FadeTransition instead of animating Opacity, cacheWidth/cacheHeight on large images.
  3. Startup and memory: see mobile-performance.

Verification

  1. dart format --set-exit-if-changed . and flutter analyze pass with no new issues.
  2. flutter test with widget tests (testWidgets, pumpWidget, find.byType, tester.tap, pumpAndSettle) for loading, error, and data states.
  3. Run on a small phone and a tablet, with system font scale at maximum: no overflow stripes.
  4. Profile-mode scroll of the heaviest list stays under the frame budget (16 ms at 60 Hz) in the DevTools frame chart.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc