Flutter Apps skill for Codex
Small widgets, one theme, typed native calls. Codex organises Flutter code by feature, extracts widget classes instead of helper methods, and routes every colour and text style through a Material 3 theme with extensions for brand tokens. Native calls go through Pigeon-generated APIs instead of string-keyed channels, and each change is checked with flutter analyze, widget tests and builds for the platforms touched.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- flutter-apps Installs to ~/.agents/skills/flutter-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install flutter-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project flutter-apps Installs to .agents/skills/flutter-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/flutter-apps.zipunzip -o flutter-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) flutter-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $flutter-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and structures Flutter apps in Dart - feature-first project layout, widget composition with const constructors, Material 3 theming with ColorScheme.fromSeed and ThemeExtension, go_router navigation, flavors and dart-define config, platform channels generated with Pigeon, and widget and golden tests. Use when starting a Flutter app, splitting a giant widget, setting up light and dark themes, calling Swift or Kotlin code from Dart, adding build flavors, or fixing analyzer warnings, overflow errors and BuildContext async gaps.
Use it when
- Starting a Flutter app or reorganising an existing lib folder
- Theming, dark mode or text scaling problems
- Calling Swift or Kotlin code from Dart
- Layout overflow errors or analyzer warnings pile up
Use something else when
- The question is which state library to use or how to structure providers or blocs: use flutter-state-management
- Sharing business logic with Kotlin rather than Dart: use kotlin-multiplatform
How to check the result
dart format, flutter analyze and flutter test all pass, and changed screens were checked in light and dark mode at the largest text size on a built app.
The mistake it prevents
Creating a Future inside build for FutureBuilder, which refires the network request on every rebuild.
Done well means: flutter analyze is clean, widgets are small and mostly const, all
colours and text styles come from the theme, native calls go through typed Pigeon APIs,
and every screen has at least one widget test.
Inspect first
cat AGENTS.md 2>/dev/null
flutter --version # Flutter and Dart versions drive API availability
cat pubspec.yaml analysis_options.yaml
ls lib lib/* | head -50
flutter analyze
Note the state management package already in use (see $flutter-state-management) and
follow it. flutter pub get needs network unless the pub cache is warm; ask before
adding dependencies in a sandbox without network.
Project layout
Feature-first scales better than layer-first once there are more than a handful of screens:
lib/
main.dart # bootstrap only: config, DI, runApp
app/ # MaterialApp.router, theme, router config
core/ # http client, storage, errors, shared widgets
features/
orders/
data/ # DTOs, repositories, API clients
domain/ # models (immutable), business rules
presentation/ # screens, widgets, controllers
test/ # mirrors lib/
Keep main.dart tiny. Business logic never lives in widgets; widgets read state and
dispatch intents.
Widgets
- Extract widgets into classes, not helper methods returning
Widget. Classes get their own element, can beconst, and rebuild independently. - Mark constructors and instances
constwherever possible; enable theprefer_const_constructorslints viaflutter_lintsorvery_good_analysis. - Pass the narrowest data a widget needs, not whole models or controllers.
- Read sizes with
MediaQuery.sizeOf(context)/paddingOf(context)rather thanMediaQuery.of(context), which rebuilds on any media change (keyboard, brightness). - Lists:
ListView.builder/SliverList.builderfor anything longer than a screen, withitemExtentorprototypeItemwhen rows have fixed height. - Give list items a
ValueKey(item.id)when items can be reordered, inserted or removed.
Fixing the classic layout errors:
| Error | Cause | Fix |
|---|---|---|
RenderFlex overflowed by N pixels | Row/Column children wider than space | Wrap the flexible child in Expanded / Flexible; ellipsize text |
Vertical viewport was given unbounded height | ListView inside Column | Wrap in Expanded, or use slivers in a CustomScrollView |
BoxConstraints forces an infinite width | Unbounded parent (Row) with double.infinity child | Use Expanded, or give an explicit width |
setState() called after dispose() | Async result after leaving screen | Check if (!mounted) return; before setState |
Theming
final lightScheme = ColorScheme.fromSeed(seedColor: const Color(0xFF0B7A75));
final darkScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF0B7A75), brightness: Brightness.dark);
@immutable
class BrandColors extends ThemeExtension<BrandColors> {
const BrandColors({required this.success, required this.warning});
final Color success;
final Color warning;
@override
BrandColors copyWith({Color? success, Color? warning}) =>
BrandColors(success: success ?? this.success, warning: warning ?? this.warning);
@override
BrandColors lerp(BrandColors? other, double t) => other == null
? this
: BrandColors(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!);
}
ThemeData buildTheme(ColorScheme scheme) => ThemeData(
colorScheme: scheme,
textTheme: Typography.material2021().black.apply(fontFamily: 'Inter'),
extensions: const [BrandColors(success: Color(0xFF1B873F), warning: Color(0xFFB25E00))],
);
- Material 3 is the default; do not set
useMaterial3: falsein new code. - In widgets:
Theme.of(context).colorScheme.primary,Theme.of(context).extension<BrandColors>()!.success. Hard-codedColor(0x...)in a widget is a review comment. Color.withOpacityis deprecated; usecolor.withValues(alpha: 0.6).- Support text scaling: never fix heights on containers holding text; test with
MediaQuery.withClampedTextScalingor the accessibility text size set to largest.
Navigation
Use go_router for URL-based routing and deep links:
final router = GoRouter(
initialLocation: '/orders',
redirect: (context, state) {
final signedIn = authNotifier.isSignedIn;
final atLogin = state.matchedLocation == '/login';
if (!signedIn && !atLogin) return '/login';
if (signedIn && atLogin) return '/orders';
return null;
},
refreshListenable: authNotifier,
routes: [
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
GoRoute(
path: '/orders',
builder: (_, __) => const OrdersScreen(),
routes: [
GoRoute(path: ':id',
builder: (_, s) => OrderScreen(id: s.pathParameters['id']!)),
],
),
],
);
Use context.go to replace the location and context.push to stack on top. Use
StatefulShellRoute.indexedStack for bottom navigation that keeps each tab’s stack.
Config and flavors
- Per-environment values:
flutter run --dart-define-from-file=config/dev.jsonand read them withconst String.fromEnvironment('API_URL'). These are compiled into the binary and are not secret. - Different app IDs and icons per environment need flavors: Android
productFlavors, iOS schemes and build configurations, thenflutter run --flavor dev.
Platform channels with Pigeon
Hand-written MethodChannel string keys drift between Dart and native. Generate them:
// pigeons/device.dart
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/core/native/device_api.g.dart',
kotlinOut: 'android/app/src/main/kotlin/com/acme/app/DeviceApi.g.kt',
swiftOut: 'ios/Runner/DeviceApi.g.swift',
))
@HostApi()
abstract class DeviceApi {
@async
int batteryLevel();
}
dart run pigeon --input pigeons/device.dart
Implement the generated protocol/interface in AppDelegate.swift and MainActivity.kt,
and register it with the engine’s binary messenger. Use EventChannel (or Pigeon event
channel APIs) for streams such as sensor updates. Native code runs on the platform main
thread by default; move slow work off it.
Gotchas
- Using
BuildContextafter anawait: checkif (!context.mounted) return;first. latefields that are never assigned crash at runtime, not compile time.FutureBuilder(future: fetch())insidebuildrefetches on every rebuild; create the future in state or a provider.- Text fields losing focus on rebuild means a key or the controller is recreated in
build. OwnTextEditingControllerin state and dispose it. - Images from the network need explicit
cacheWidth/cacheHeightfor thumbnails or they decode at full resolution. - Impeller is the default renderer on iOS and modern Android; check rendering issues on device before assuming a Skia-only workaround is needed.
Verification
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test
flutter test --update-goldens # only when a visual change is intended, then review diffs
flutter build apk --debug # quick compile check; flutter build ios --no-codesign on macOS
Widget test skeleton:
testWidgets('shows empty state when there are no orders', (tester) async {
await tester.pumpWidget(const MaterialApp(home: OrdersScreen(orders: [])));
expect(find.text('No orders yet'), findsOneWidget);
});
Done when analyze, format and tests pass, the app builds for each touched platform, and changed screens were checked in light and dark mode at the largest text size.
Report template
**Flutter / Dart:** 3.x / 3.x, state: Riverpod
**Changed:** split OrdersScreen into OrdersList and OrderTile; BrandColors theme extension
**Native:** Pigeon DeviceApi (Swift + Kotlin) for battery level
**Checks:** analyze clean, 42 tests pass, goldens unchanged, built Android debug and iOS no-codesign What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Flutter Apps"
short_description: "Small widgets, one theme, typed native calls"
default_prompt: "$flutter-apps Restructure this Flutter project by feature and split the 900-line home screen" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.