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.
- name
flutter-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
- The app is React Native — use react-native-expo
- You need store submission, signing and rollout steps — use app-store-release
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.
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 beconst;class _Header extends StatelessWidgetcan. - Add
constconstructors and useconstinstances 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. StatefulWidgetonly for ephemeral UI state (animation controllers, text controllers, expanded flags). Dispose controllers, focus nodes, and subscriptions indispose().- Use
ValueKey(item.id)on list items that hold state or can reorder; without keys Flutter matches by position and state jumps between rows. AvoidGlobalKeyexcept for forms or rare cross-tree access.
Layout: constraints model and common errors
| Error | Cause | Fix |
|---|---|---|
| “A RenderFlex overflowed by N pixels” | Row/Column children wider/taller than available | Wrap flexible children in Expanded/Flexible; overflow: TextOverflow.ellipsis; make it scroll |
| “Vertical viewport was given unbounded height” | ListView inside a Column | Wrap the list in Expanded, or use one CustomScrollView with slivers |
| “BoxConstraints forces an infinite width” | TextField/Expanded inside an unconstrained Row | Give it Expanded or a fixed width |
| “Incorrect use of ParentDataWidget” | Expanded outside a Flex parent | Only use Expanded/Flexible directly inside Row/Column/Flex |
| Content under notch/home indicator | No insets applied | SafeArea or Scaffold + MediaQuery.paddingOf(context) |
- Read size with
MediaQuery.sizeOf(context)(rebuilds only on size changes), notMediaQuery.of(context).size. - Responsive components use
LayoutBuilder(parent constraints), not screen size. - Complex scroll screens (collapsing header + grid + list):
CustomScrollViewwithSliverAppBar,SliverGrid,SliverList.builder. - Long lists always use builders (
ListView.builder,SliverList.builder), neverListView(children: bigList.map(...)).
State management choice
| Scope | Choose |
|---|---|
| Ephemeral UI state in one widget | setState / controllers |
| Simple shared value, no package | ValueNotifier + ValueListenableBuilder, or ChangeNotifier + ListenableBuilder |
| App state, async data, dependency injection, testability | Riverpod 3 (default for new projects) |
| Teams wanting explicit event → state transitions and strict layering | Bloc / Cubit |
| Existing Provider codebase | Keep 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.watchinbuild;ref.readin callbacks;ref.listenfor side effects (snackbars, navigation). Neverref.readinbuildto 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
buildforFutureBuilder(future: api.load()); it refires on every rebuild. Create it ininitStateor use a provider. - Heavy CPU work (JSON parsing of large payloads, image processing) goes off the UI
isolate with
Isolate.run(() => parse(bytes)).
Navigation (go_router)
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
MethodChannelcalls. - Raw
MethodChannel: same channel name on both sides, handlePlatformExceptionandMissingPluginExceptionin Dart, and call results on the platform main thread. - Streams from native (sensors, location) use
EventChannel; cancel the subscription indispose().
Performance
- 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. - Common fixes:
constwidgets, smaller rebuild scope (Consumer/selectdeep in the tree),ListView.builder,RepaintBoundaryaround frequently repainting regions,FadeTransitioninstead of animatingOpacity,cacheWidth/cacheHeighton large images. - Startup and memory: see
mobile-performance.
Verification
dart format --set-exit-if-changed .andflutter analyzepass with no new issues.flutter testwith widget tests (testWidgets,pumpWidget,find.byType,tester.tap,pumpAndSettle) for loading, error, and data states.- Run on a small phone and a tablet, with system font scale at maximum: no overflow stripes.
- 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.