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.

  • Mobile & Desktop
  • $flutter-apps
  • 220 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- flutter-apps

Installs to ~/.agents/skills/flutter-apps/ for every project on this machine.

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

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.

~/.agents/skills/flutter-apps/SKILL.md

--- name: flutter-apps description: Builds and structures Flutter apps in Dart - feature-first project layout, widget composition with const constructors, Material 3 theming with… license: MIT ---

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 be const, and rebuild independently.
  • Mark constructors and instances const wherever possible; enable the prefer_const_constructors lints via flutter_lints or very_good_analysis.
  • Pass the narrowest data a widget needs, not whole models or controllers.
  • Read sizes with MediaQuery.sizeOf(context) / paddingOf(context) rather than MediaQuery.of(context), which rebuilds on any media change (keyboard, brightness).
  • Lists: ListView.builder / SliverList.builder for anything longer than a screen, with itemExtent or prototypeItem when rows have fixed height.
  • Give list items a ValueKey(item.id) when items can be reordered, inserted or removed.

Fixing the classic layout errors:

ErrorCauseFix
RenderFlex overflowed by N pixelsRow/Column children wider than spaceWrap the flexible child in Expanded / Flexible; ellipsize text
Vertical viewport was given unbounded heightListView inside ColumnWrap in Expanded, or use slivers in a CustomScrollView
BoxConstraints forces an infinite widthUnbounded parent (Row) with double.infinity childUse Expanded, or give an explicit width
setState() called after dispose()Async result after leaving screenCheck 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: false in new code.
  • In widgets: Theme.of(context).colorScheme.primary, Theme.of(context).extension<BrandColors>()!.success. Hard-coded Color(0x...) in a widget is a review comment.
  • Color.withOpacity is deprecated; use color.withValues(alpha: 0.6).
  • Support text scaling: never fix heights on containers holding text; test with MediaQuery.withClampedTextScaling or the accessibility text size set to largest.

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.json and read them with const 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, then flutter 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 BuildContext after an await: check if (!context.mounted) return; first.
  • late fields that are never assigned crash at runtime, not compile time.
  • FutureBuilder(future: fetch()) inside build refetches 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. Own TextEditingController in state and dispose it.
  • Images from the network need explicit cacheWidth/cacheHeight for 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem