Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Flutter Patterns

ASecurity

Provides production Flutter and Dart 3 patterns covering widget composition, state management choice (Riverpod, BLoC, plain ChangeNotifier), immutability with freezed and records, sealed-class state, async and streams, Firebase usage (Auth, Firestore, Functions), navigation, and widget/integration testing. Use when writing, reviewing, or architecting Flutter apps or Dart packages.

5 stars
0 votes
0 copies
0 views
Added 9/28/2026
ai-agentsgoreactexpresstestingsecurity

Works with

cli

Security Analysis

A100/100

Scanned 9/28/2026

Install to Claude Code

$npx -y skills add harshadmadaye/OPM --skill flutter-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Patterns?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Flutter Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/harshadmadaye-flutter-patterns/badge)](https://www.skillsdirectory.com/skills/harshadmadaye-flutter-patterns)

More formats (shields.io, HTML) on the badges page.

Files
SKILL.md
---
name: flutter-patterns
description: Provides production Flutter and Dart 3 patterns covering widget composition, state management choice (Riverpod, BLoC, plain ChangeNotifier), immutability with freezed and records, sealed-class state, async and streams, Firebase usage (Auth, Firestore, Functions), navigation, and widget/integration testing. Use when writing, reviewing, or architecting Flutter apps or Dart packages.
---

# Flutter Patterns

Small const widgets, immutable state modelled with sealed types, side effects in a thin layer
above the UI, and tests at the widget boundary. Dart 3 features (records, patterns, sealed
classes) are assumed.

## When to use

- Starting a Flutter feature and choosing how to structure widgets and state.
- Reviewing Dart for null safety, immutability, async correctness, or rebuild scope.
- Integrating Firebase services in a Flutter app.
- Writing widget, unit, or integration tests.

## Null safety and Dart idioms

```dart
// Avoid `!`; use `?.`, `??`, guards, or pattern matching
final name = user?.name ?? 'Guest';

String describe(User? user) => switch (user) {
  User(:final name, :final email) => '$name <$email>',
  null => 'Guest',
};

// `late` only when initialisation is guaranteed before first read (initState)
late final AnimationController _controller;
```

- Prefer `final` everywhere; `var` only when reassignment is intended.
- `firstWhereOrNull` (package:collection) instead of `firstWhere` with a throwing `orElse`.
- Exhaustive `switch` expressions over `if` chains for sealed types and enums.
- Records for lightweight multi-value returns: `(int count, double total) summarise(...)`.

## Immutability

### Sealed classes for state

```dart
sealed class LoadState<T> {
  const LoadState();
}
final class Loading<T> extends LoadState<T> { const Loading(); }
final class Loaded<T> extends LoadState<T> { const Loaded(this.data); final T data; }
final class Failed<T> extends LoadState<T> { const Failed(this.error); final Object error; }

Widget render(LoadState<List<Item>> state) => switch (state) {
  Loading() => const CircularProgressIndicator(),
  Loaded(:final data) => ItemList(items: data),
  Failed(:final error) => ErrorView(message: '$error'),
};
```

The compiler enforces every branch; adding a state variant breaks every incomplete switch at
build time, which is exactly what you want.

### freezed for data classes

```dart
@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    @Default(false) bool isAdmin,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

final updated = user.copyWith(name: 'New name');   // never mutate, always copy
```

Use freezed for models that cross a boundary (JSON, Firestore documents, state objects with
several fields). Use a plain `const` class with `final` fields for two- or three-field values,
and records for ad-hoc tuples. Run `dart run build_runner build --delete-conflicting-outputs`
after model changes; commit the generated files.

## Widget composition

**Extract to classes, not methods.** A `_buildHeader()` method rebuilds with its parent and
cannot be `const`; a `_Header` widget can be skipped by the framework.

```dart
class _Header extends StatelessWidget {
  const _Header(this.title);
  final String title;

  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.all(16),
    child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
  );
}
```

**Push rebuilds to the leaves.** The widget that watches state should be as small as possible.

```dart
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) => const Scaffold(
    body: Column(children: [
      ExpensiveHeader(),   // const: never rebuilt
      _CounterText(),      // only this watches the provider
      ExpensiveFooter(),
    ]),
  );
}

class _CounterText extends ConsumerWidget {
  const _CounterText();
  @override
  Widget build(BuildContext context, WidgetRef ref) => Text('${ref.watch(counterProvider)}');
}
```

- `const` constructors and `const` child trees wherever the values are compile-time constants.
- Widgets take data and callbacks; they do not fetch, persist, or navigate on their own.
- Keep `build` free of side effects and heavy work; compute in the state layer.
- Use `Theme.of(context)` tokens, not hardcoded colours and sizes.
- `ListView.builder` / `SliverList` for anything longer than a screen; never a `Column` of
  hundreds of children.

## State management

Pick one approach per app and hold to it.

| Situation | Choice |
|---|---|
| Local, ephemeral UI state (a toggle, a text field, an animation) | `StatefulWidget` + `setState` |
| App or feature state, most new apps | Riverpod (code-generated providers) |
| Teams that want explicit event -> state transitions and event logs | BLoC / Cubit |
| Tiny app or package with no dependency budget | `ChangeNotifier` + `ListenableBuilder` |

### Riverpod

```dart
@riverpod
Future<List<Product>> products(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.fetchAll();
}

@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() => const [];

  void add(Product product) {
    final existing = state.firstWhereOrNull((i) => i.productId == product.id);
    state = existing == null
        ? [...state, CartItem(productId: product.id, quantity: 1)]
        : [for (final i in state) i.productId == product.id ? i.copyWith(quantity: i.quantity + 1) : i];
  }
}

@riverpod
int cartCount(Ref ref) => ref.watch(cartProvider).length;   // derived, cached
```

`ref.watch` in build, `ref.read` in callbacks, `ref.listen` for one-shot reactions (snackbars,
navigation). Derived providers replace manual selectors.

### Cubit

```dart
class AuthCubit extends Cubit<AuthState> {
  AuthCubit(this._auth) : super(const AuthState.initial());
  final AuthService _auth;

  Future<void> signIn(String email, String password) async {
    emit(const AuthState.loading());
    try {
      emit(AuthState.authenticated(await _auth.signIn(email, password)));
    } on AuthException catch (e) {
      emit(AuthState.error(e.message));
    }
  }
}
```

## Async and streams

```dart
// Concurrent futures with record destructuring
final (users, orders) = await (userRepo.fetchAll(), orderRepo.recent()).wait;

// Always guard BuildContext after an await
Future<void> _submit() async {
  setState(() => _busy = true);
  try {
    await _service.save(_draft);
    if (!mounted) return;
    context.go('/done');
  } on ServiceException catch (e) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
  } finally {
    if (mounted) setState(() => _busy = false);
  }
}

// Streams: expose from the repository, consume declaratively
Stream<List<Message>> watchThread(String id) => _source.watch(id).map(Message.listFrom);

StreamBuilder<List<Message>>(
  stream: repo.watchThread(threadId),
  builder: (context, snapshot) => switch (snapshot) {
    AsyncSnapshot(connectionState: ConnectionState.waiting) => const Loader(),
    AsyncSnapshot(:final error?) => ErrorView(message: '$error'),
    AsyncSnapshot(:final data?) => MessageList(messages: data),
    _ => const SizedBox.shrink(),
  },
)
```

- Never store a `StreamSubscription` without cancelling it in `dispose`; prefer `StreamBuilder`
  or a provider that manages the lifecycle.
- Wrap external failures in typed exceptions at the repository layer; the UI never catches
  `FirebaseException` directly.
- Cancel or debounce user-driven async work (search-as-you-type) so stale results cannot
  overwrite fresh ones.

## Firebase usage patterns

Keep Firebase behind repository interfaces so widgets and state classes depend on your types,
not on the SDK, and tests can substitute fakes.

```dart
abstract interface class NotesRepository {
  Stream<List<Note>> watchAll(String uid);
  Future<void> upsert(Note note);
}

class FirestoreNotesRepository implements NotesRepository {
  FirestoreNotesRepository(this._db);
  final FirebaseFirestore _db;

  CollectionReference<Note> _notes(String uid) => _db
      .collection('users').doc(uid).collection('notes')
      .withConverter<Note>(
        fromFirestore: (snap, _) => Note.fromJson({...snap.data()!, 'id': snap.id}),
        toFirestore: (note, _) => note.toJson()..remove('id'),
      );

  @override
  Stream<List<Note>> watchAll(String uid) => _notes(uid)
      .orderBy('updatedAt', descending: true)
      .snapshots()
      .map((s) => s.docs.map((d) => d.data()).toList());

  @override
  Future<void> upsert(Note note) => _notes(note.ownerId).doc(note.id).set(note, SetOptions(merge: true));
}
```

- **Auth**: expose `authStateChanges()` as a provider; derive a sealed `AuthStatus` from it and
  drive router redirects from that, not from widgets checking `currentUser` directly.
- **Firestore**: always `withConverter`; model documents with freezed; keep per-user data under
  `users/{uid}/...` so security rules stay simple; paginate with `limit` + `startAfterDocument`;
  never query unbounded collections into memory.
- **Security rules are the real authorisation layer.** Client checks are UX, not security. Test
  rules with the emulator.
- **Cloud Functions**: call via `FirebaseFunctions.instance.httpsCallable('name')`, wrap the
  result in a typed model, and map `FirebaseFunctionsException` codes to your own exceptions.
- **Emulators** for local development and integration tests: `firebase emulators:start`, then
  `useAuthEmulator`, `useFirestoreEmulator` behind a `kDebugMode` or `--dart-define` flag.
- **Crashlytics**: hook `FlutterError.onError` and `PlatformDispatcher.instance.onError` in
  `main()`; record handled exceptions from the repository layer with context.

## Navigation

Use `go_router` with a `refreshListenable` bound to the auth stream so redirects re-evaluate
when auth changes. Typed routes (`go_router_builder`) remove string paths from widgets. Keep
route definitions in one file; widgets call `context.go`/`context.push` with route objects.

## Error handling

```dart
void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    crashReporter.recordFlutterError(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    crashReporter.record(error, stack, fatal: true);
    return true;
  };
  runApp(const ProviderScope(child: App()));
}
```

- Repository layer converts SDK exceptions into a small app exception hierarchy.
- State layer turns exceptions into `Failed` states; the UI renders them.
- Replace the red `ErrorWidget` in release builds with `ErrorWidget.builder`.

## Testing

```dart
// Unit: pure logic and notifiers, with fakes
test('cart total sums price times quantity', () {
  final cart = Cart()..add(product(price: 250))..add(product(price: 250));
  expect(cart.total, 500);
});

// Widget: render with overrides, assert on what the user sees
testWidgets('shows item count badge', (tester) async {
  await tester.pumpWidget(ProviderScope(
    overrides: [cartProvider.overrideWith(() => FakeCart(count: 3))],
    child: const MaterialApp(home: CartBadge()),
  ));
  expect(find.text('3'), findsOneWidget);
});

// Golden: visual regression for design-system widgets
await expectLater(find.byType(PrimaryButton), matchesGoldenFile('primary_button.png'));

// Integration (integration_test/app_test.dart): a real flow on a device or emulator
testWidgets('sign in and reach home', (tester) async {
  await tester.pumpWidget(const App());
  await tester.enterText(find.byKey(const Key('email')), 'user@example.com');
  await tester.tap(find.text('Sign in'));
  await tester.pumpAndSettle();
  expect(find.text('Home'), findsOneWidget);
});
```

- Fakes over mocks: a `FakeNotesRepository` backed by a list is easier to reason about than a
  mock with stubbed calls.
- `pumpAndSettle` for animations; plain `pump` with a duration when something never settles
  (an infinite spinner).
- Find by `Key`, semantics label, or visible text; not by widget type when the type is generic.
- Firebase-backed flows run against the emulator suite in `integration_test`, never production.
- Commands: `flutter test`, `flutter test --coverage`, `flutter test integration_test`,
  `flutter test --update-goldens`.

## Anti-patterns

- `user!.name` where `user?.name ?? default` was possible.
- Widget-returning private methods instead of widget classes.
- `setState` for anything read outside the widget.
- `context` used after an `await` without a `mounted` check.
- Firestore queries or `FirebaseAuth.instance` calls inside `build`.
- A `Column` inside `SingleChildScrollView` for a long list.
- Catching `Exception` in the UI and showing `e.toString()`.
- Tests that pump the whole app to check one widget.

## Review checklist

- [ ] `dart analyze` clean; `flutter test` green.
- [ ] State is sealed/immutable; updates create new objects.
- [ ] Rebuild scope is minimal; `const` used where possible.
- [ ] Every `await` in a widget is followed by a `mounted` guard before using `context`.
- [ ] Firebase is behind repositories with `withConverter`; rules cover every access.
- [ ] Subscriptions and controllers are disposed.
- [ ] New behaviour has a widget or unit test that fails without it.

## Related skills

- `opm:tdd-workflow` - `flutter test` RED/GREEN cycle.
- `opm:verification-loop` - `dart analyze` and `flutter test` as the release gate.

<!-- Adapted from affaan-m/ecc (MIT) -->

Attribution

harshadmadayeharshadmadaye
View sourceMore from harshadmadaye →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1074701 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

695601 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

691 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →