Agent skill

Flutter Idioms

by irahardianto in irahardianto/awesome-agv

Flutter idioms: Riverpod 3 state management, Freezed immutable models, gorouter navigation, const widget optimization, and repository patterns.

MITAuto-check passedMobile

Install Flutter Idioms

skills CLI
$ npx skills add irahardianto/awesome-agv --skill flutter-idioms -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install irahardianto/awesome-agv flutter-idioms --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/flutter-idioms .claude/skills/flutter-idioms && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
flutter-idioms
GitHub stars
156
Token cost
~7.3k tokens
SKILL.md length
1,523 words
Files
2 (incl. references)
Skills in repo
34
Repo updated
First seen
Licence
MIT

At a glance

Flutter idioms: Riverpod 3 state management, Freezed immutable models, gorouter navigation, const widget optimization, and repository patterns.

  • Works in 3 steps: Extract a new widget when a subtree has… → Never use builder methods… → Keep build methods under ~30 lines — if…
  • Reviewing Flutter mobile applications
  • Calls dart and flutter
  • Tasks that involve Cross-platform mobile apps

What it does

Flutter Idioms is an agent skill from irahardianto/awesome-agv. Flutter idioms: Riverpod 3 state management, Freezed immutable models, gorouter navigation, const widget optimization, and repository patterns. Use when developing, refactoring, or reviewing Flutter mobile applications.

Its SKILL.md is about 7.3k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files, including reference files (for example `references/project-structure.md`).

It sits in Mobile, covering Cross-platform mobile apps, State management and Design patterns. It works with Flutter. The repository describes itself as: Comprehensive sets of standards and practices designed to elevate the capabilities of AI coding agents. The licence is MIT.

When your agent uses it

  • Reviewing Flutter mobile applications
  • Tasks that involve Cross-platform mobile apps
  • Tasks that involve State management

Example prompts

  • “/flutter-idioms”

Workflow steps

3 steps, taken from the first numbered list in SKILL.md.

  1. Extract a new widget when a subtree has distinct responsibilities
  2. Never use builder methods (_buildHeader()) as a substitute for extracting widgets
  3. Keep build methods under ~30 lines — if longer, decompose

What it can do on your machine

Read from SKILL.md and the folder at commit 9e997ba. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • dart
    • flutter

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Flutter Idioms loads about 7.3k tokens when it runs, and up to ~8.6k if it reads all its reference files. Until then it costs about 59 tokens; SKILL.md has 1,523 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~59
When it runs · the whole SKILL.md, loaded when a task matches
~7.3k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~8.6k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from irahardianto/awesome-agv at commit 9e997ba, republished under its MIT licence (© irahardianto). 1,523 words, ~7,288 tokens.

Download SKILL.mdSave it as .claude/skills/flutter-idioms/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.
name
flutter-idioms
description
Flutter idioms: Riverpod 3 state management, Freezed immutable models, go_router navigation, const widget optimization, and repository patterns. Use when developing, refactoring, or reviewing Flutter mobile applications.

Flutter Idioms and Patterns (Riverpod 3)

Core Philosophy

Flutter is a UI toolkit first — performance is a first-class concern. const widgets and immutable data keep the render tree efficient. Riverpod 3 is the canonical state management solution: compile-safe, testable without BuildContext, no implicit global state, and with automatic retry and pause/resume built in.

Code generation is mandatory. All providers must use @riverpod / @Riverpod(keepAlive: true) annotations with riverpod_generator and build_runner. This catches type errors, missing overrides, and broken provider graphs at compile time — bugs are caught before they reach users.

Required dependencies:

yaml
# pubspec.yaml
dependencies:
  flutter_riverpod: 3.2.1
  riverpod_annotation: 4.0.2

dev_dependencies:
  riverpod_generator: 4.0.3
  build_runner: # latest
  riverpod_lint: # latest

Scope: This file covers Flutter/Dart coding idioms. For file and folder layout, see references/project-structure.md. For test naming, see testing-strategy.md. For general error handling principles, see error-handling-principles.md.


const Constructors — Everywhere

Make every widget const when possible. const widgets are created once and never rebuilt unless their inputs change — this is Flutter's most impactful performance optimization.

dart
// ✅ const constructor — widget is rebuild-safe
class TaskCard extends StatelessWidget {
    const TaskCard({super.key, required this.task});
    final Task task;
    // ...
}

// Usage — compile-time constant
const TaskCard(task: myTask)

// ❌ Missing const — rebuilt on every parent rebuild
TaskCard(task: myTask)

Rules:

  • Every StatelessWidget that has no mutable state must have a const constructor
  • Pass const keyword at the call site, not just the definition
  • Lint rule prefer_const_constructors must be enabled in analysis_options.yaml

Widget Decomposition

Large build methods are the primary source of performance problems and unmaintainable UI code.

  1. Extract a new widget when a subtree has distinct responsibilities

    dart
    // ❌ Everything in one build method
    @override
    Widget build(BuildContext context) {
        return Column(children: [
            // 30 lines of header...
            // 50 lines of list...
            // 20 lines of footer...
        ]);
    }
    
    // ✅ Each subtree is a named widget with a const constructor
    @override
    Widget build(BuildContext context) {
        return Column(children: [
            const TaskHeader(),
            const TaskList(),
            const TaskFooter(),
        ]);
    }
  2. Never use builder methods (_buildHeader()) as a substitute for extracting widgets

    • Builder methods do not benefit from const and always rerun on parent rebuild
    • Extract a proper StatelessWidget or ConsumerWidget instead
  3. Keep build methods under ~30 lines — if longer, decompose


Immutable Data with freezed

All domain models must be immutable. Use the freezed package for:

  • Immutable value objects with copyWith
  • Union/sealed types (loading, success, error states)
  • Generated ==, hashCode, and toString
dart
// task/models/task.dart
@freezed
class Task with _$Task {
    const factory Task({
        required String id,
        required String title,
        @Default(TaskStatus.pending) TaskStatus status,
        DateTime? dueDate,
    }) = _Task;

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

// Usage — immutable update via copyWith
final updated = task.copyWith(status: TaskStatus.done);

// ❌ Never mutate a model directly
task.status = TaskStatus.done; // compile error — field is final

Rules:

  • All domain models use @freezed
  • Never expose mutable fields on domain models
  • Run dart run build_runner build after changing freezed models

Provider Decision Tree

Use this tree to pick the correct provider pattern. Follow it top-to-bottom — the first match wins.

Does the provider have side-effects (create/update/delete)? ──> YES ──┐
    │                                                                  │
    NO                                                                 │
    │                                                             Is it async?
    │                                                              │       │
Is it async?                                                      YES     NO
    │       │                                                      │       │
   YES     NO                                                     ▼       ▼
    │       │                                              @riverpod   @riverpod
    ▼       ▼                                              class       class
@riverpod  @riverpod                                       (AsyncNotifier) (Notifier)
function   function
(FutureProvider) (Provider)

Decision summary:

Has side-effects?Async?PatternProvider type generated
NoNo@riverpod functionProvider
NoYes@riverpod async functionFutureProvider
NoStream@riverpod Stream functionStreamProvider
YesNo@riverpod classNotifierProvider
YesYes@riverpod class with Future buildAsyncNotifierProvider
YesStream@riverpod class with Stream buildStreamNotifierProvider

Riverpod — State Management

Riverpod 3 is the only state management solution used in this project. Do not introduce BLoC, Cubit, Provider (package:provider), or GetX.

For file layout of state/ directories, see references/project-structure.md.

App Entry Point — ProviderScope

Every Flutter app using Riverpod must wrap the root widget in ProviderScope. This creates the ProviderContainer that powers all providers in the widget tree.

dart
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
    runApp(const ProviderScope(child: MyApp()));
}

Rules:

  • Exactly one ProviderScope at the app root — never nest ProviderScope widgets
  • Use overrides: parameter only in tests (see Testing section)
  • For retry configuration, pass retry: to the root ProviderScope (see Runtime Behaviors)
Provider Definition — Class-Based (Side-Effects)

Use class-based providers when the provider needs methods to modify state.

dart
// Synchronous notifier — e.g., a filter or toggle
@riverpod
class TaskFilter extends _$TaskFilter {
    @override
    TaskFilterState build() {
        return const TaskFilterState();
    }

    void setStatus(TaskStatus? status) {
        state = state.copyWith(status: status);
    }

    void toggleShowCompleted() {
        state = state.copyWith(showCompleted: !state.showCompleted);
    }
}
dart
// Async notifier — e.g., CRUD operations
@riverpod
class TaskList extends _$TaskList {
    @override
    Future<List<Task>> build() async {
        return ref.watch(taskRepositoryProvider).getTasks();
    }

    Future<void> addTask(CreateTaskRequest request) async {
        state = const AsyncLoading();
        state = await AsyncValue.guard(() async {
            final repo = ref.read(taskRepositoryProvider);
            await repo.createTask(request);
            // REQUIRED: check ref.mounted after every await
            if (!ref.mounted) return state.requireValue;
            return repo.getTasks();
        });
    }

    Future<void> deleteTask(String id) async {
        state = const AsyncLoading();
        state = await AsyncValue.guard(() async {
            final repo = ref.read(taskRepositoryProvider);
            await repo.deleteTask(id);
            if (!ref.mounted) return state.requireValue;
            return repo.getTasks();
        });
    }
}
Provider Definition — Functional (Read-Only / Computed)

Use functional providers for derived values with no side-effects.

dart
// Computed value — filtered task list
@riverpod
List<Task> filteredTasks(Ref ref) {
    final tasks = ref.watch(taskListProvider).valueOrNull ?? [];
    final filter = ref.watch(taskFilterProvider);
    return tasks.where((t) => filter.matches(t)).toList();
}

// Async one-shot read — e.g., fetch a single task
@riverpod
Future<Task> taskDetail(Ref ref, String id) async {
    return ref.watch(taskRepositoryProvider).getById(id);
}

// Stream — real-time data
@riverpod
Stream<List<Task>> taskStream(Ref ref) {
    return ref.watch(taskRepositoryProvider).watchAll();
}

Family providers (parameterized): When a provider takes extra arguments beyond Ref, Riverpod creates a family — each unique argument combination gets its own independent provider instance with its own cache and disposal. With code generation (which we mandate), any number of parameters are supported, including named, optional, and default values. Constraints:

  • All parameters must implement == and hashCode (primitives, freezed models, Dart records all work)
  • Each family member is disposed independently when no listener remains
dart
// ✅ Family with a single key
@riverpod
Future<Task> taskDetail(Ref ref, String id) async {
    return ref.watch(taskRepositoryProvider).getById(id);
}

// ✅ Multiple parameters — fully supported with code generation
@riverpod
Future<List<Task>> projectTasks(Ref ref, String projectId, {TaskStatus? status}) async {
    return ref.watch(taskRepositoryProvider).getByProject(projectId, status: status);
}
// Usage: ref.watch(projectTasksProvider('proj-1', status: TaskStatus.done))
ref.watch vs ref.read
dart
// ✅ ref.watch — subscribes to changes, use inside build() or widget build
final tasks = ref.watch(taskListProvider);

// ✅ ref.read — one-time read, use inside event handlers / notifier actions
Future<void> onSubmit() async {
    await ref.read(taskListProvider.notifier).addTask(request);
}

// ❌ Never use ref.watch inside async functions or event handlers
Future<void> onSubmit() async {
    final tasks = ref.watch(taskListProvider); // WRONG — causes errors
}
Ref.mounted — Mandatory After Awaits

Riverpod 3 throws if you interact with a disposed Ref or Notifier. Always check ref.mounted after any await in a notifier.

dart
Future<void> updateTask(Task task) async {
    final repo = ref.read(taskRepositoryProvider);
    await repo.update(task);

    // REQUIRED: provider may have been disposed during await
    if (!ref.mounted) return;

    state = await AsyncValue.guard(() => repo.getTasks());
}
Auto-Dispose and keepAlive
dart
// ✅ autoDispose is the DEFAULT with code generation (@riverpod)
// Provider is disposed when no consumers are listening
@riverpod
Future<Task> taskDetail(Ref ref, String id) async {
    return ref.watch(taskRepositoryProvider).getById(id);
}

// ✅ Opt into keepAlive explicitly for app-wide, long-lived state
@Riverpod(keepAlive: true)
class AuthState extends _$AuthState {
    @override
    Future<User?> build() async {
        return ref.watch(authRepositoryProvider).getCurrentUser();
    }
}

// ✅ Repositories should also be keepAlive — they hold connection state and
//    should not be re-initialized every time a screen rebuilds
@Riverpod(keepAlive: true)
TaskRepository taskRepository(Ref ref) {
    return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
}

// ❌ Do not set keepAlive: false — that is the default
// ❌ Do not set keepAlive: true for screen-scoped state
Repository Interface Pattern

All data access goes through an abstract repository interface. This is the Flutter expression of the Testability-First architecture (see @.agents/rules/architectural-pattern.md).

dart
// repository/task_repository.dart — Abstract interface (contract)
abstract class TaskRepository {
    Future<List<Task>> getTasks();
    Future<Task> getById(String id);
    Future<void> createTask(CreateTaskRequest request);
    Future<void> deleteTask(String id);
}
dart
// repository/task_repository_impl.dart — Production adapter
class TaskRepositoryImpl implements TaskRepository {
    const TaskRepositoryImpl({required this.apiClient});
    final ApiClient apiClient;

    @override
    Future<List<Task>> getTasks() async {
        final response = await apiClient.get('/tasks');
        return (response.data as List)
            .map((e) => Task.fromJson(e as Map<String, dynamic>))
            .toList();
    }

    @override
    Future<Task> getById(String id) async {
        final response = await apiClient.get('/tasks/$id');
        return Task.fromJson(response.data as Map<String, dynamic>);
    }

    @override
    Future<void> createTask(CreateTaskRequest request) async {
        await apiClient.post('/tasks', data: request.toJson());
    }

    @override
    Future<void> deleteTask(String id) async {
        await apiClient.delete('/tasks/$id');
    }
}
dart
// repository/task_repository_mock.dart — Test adapter
class MockTaskRepository implements TaskRepository {
    final List<Task> _tasks = [];

    @override
    Future<List<Task>> getTasks() async => List.unmodifiable(_tasks);

    @override
    Future<Task> getById(String id) async =>
        _tasks.firstWhere((t) => t.id == id);

    @override
    Future<void> createTask(CreateTaskRequest request) async {
        _tasks.add(Task(id: 'mock-id', title: request.title));
    }

    @override
    Future<void> deleteTask(String id) async {
        _tasks.removeWhere((t) => t.id == id);
    }
}
dart
// Wiring — provider that the rest of the app depends on
@Riverpod(keepAlive: true)
TaskRepository taskRepository(Ref ref) {
    return TaskRepositoryImpl(apiClient: ref.watch(apiClientProvider));
}

// In tests, override with mock:
// taskRepositoryProvider.overrideWith((_) => MockTaskRepository())
ConsumerWidget vs ConsumerStatefulWidget
dart
// ✅ Prefer ConsumerWidget — stateless, simpler
class TaskListView extends ConsumerWidget {
    const TaskListView({super.key});

    @override
    Widget build(BuildContext context, WidgetRef ref) {
        final asyncTasks = ref.watch(taskListProvider);
        return asyncTasks.when(
            data: (tasks) => TaskListBody(tasks: tasks),
            loading: () => const LoadingIndicator(),
            error: (e, _) => ErrorView(error: e),
        );
    }
}

// Use ConsumerStatefulWidget only when local widget state + riverpod is needed

Riverpod 3 — Runtime Behaviors
Automatic Retry

Riverpod 3 automatically retries providers that throw, using exponential backoff. This improves resilience against transient network failures.

dart
// ✅ Default behavior — providers auto-retry on failure
// No action needed for standard use

// ✅ Disable retry for a specific provider when failure is non-transient
@Riverpod(retry: null)
Future<Config> appConfig(Ref ref) async {
    return ref.watch(configRepositoryProvider).load();
}

// ✅ Disable globally via ProviderScope
ProviderScope(
    retry: (_, __) => null, // disable for all providers
    child: const MyApp(),
)

Rules:

  • Leave auto-retry enabled for network/IO operations (transient failures)
  • Disable for operations where retry is unsafe (non-idempotent writes) or pointless (validation errors)
Pause / Resume (Out-of-View Providers)

Riverpod 3 automatically pauses provider listeners when the consuming widget is no longer visible. When the widget becomes visible again, listeners resume.

This is automatic — no action needed. Be aware of it when debugging.

ProviderException Wrapping

When a provider fails, reading it in Riverpod 3 throws a ProviderException wrapping the original error, not the raw exception.

dart
// ✅ Catch ProviderException when reading providers that may fail
try {
    final value = container.read(myProvider);
} on ProviderException catch (e) {
    // e.exception contains the original error
    // e.provider contains which provider failed
}
dart
// ✅ Assert on provider failure in tests
expect(
    () => container.read(myProvider),
    throwsA(isA<ProviderException>()),
);
State Change Detection (==)

Riverpod 3 uses the == operator (not identical) to determine if state changed and rebuilds are needed. This means:

  • freezed models work correctly out of the box (generated ==)
  • Custom models must implement == / hashCode or use freezed
  • Override updateShouldNotify on a Notifier for custom comparison logic

Async Patterns
  1. Always handle all three AsyncValue states: data, loading, error

    dart
    // ✅ Exhaustive
    asyncValue.when(
        data: (data) => DataWidget(data: data),
        loading: () => const CircularProgressIndicator(),
        error: (err, stack) => ErrorText(err.toString()),
    );
  2. Surfacing notifier errors to UI — use when(error:...) for exhaustive handling; use hasError only for conditional checks alongside a separate data display:

    dart
    // ✅ Exhaustive — covers all states, preferred for full-screen states
    asyncTasks.when(
        data: (tasks) => TaskListBody(tasks: tasks),
        loading: () => const LoadingIndicator(),
        error: (e, _) => ErrorView(error: e),
    );
    
    // ✅ Conditional — show inline error banner while keeping stale data visible
    if (asyncTasks.hasError) {
        // show snackbar or inline error
    }
    final tasks = asyncTasks.valueOrNull ?? const [];
  3. Use safe AsyncValue accessors to prevent runtime crashes

    dart
    // ✅ Safe — returns null if state is loading or error
    final tasks = ref.watch(taskListProvider).valueOrNull;
    
    // ⚠️ Unsafe — throws StateError if state is not AsyncData
    // Only use when you have already confirmed the state is loaded
    final tasks = ref.watch(taskListProvider).requireValue;
  4. Use AsyncValue.guard inside notifier actions to wrap async calls

    • It catches exceptions and wraps them in AsyncError automatically
  5. Use StreamProvider for real-time data — never poll manually with Timer

  6. Always check ref.mounted after await — see Ref.mounted section above

  7. Force a provider to re-fetch with ref.invalidate

    dart
    // ✅ Invalidate from outside a notifier (e.g., after a form submit in a widget)
    ref.invalidate(taskListProvider);
    // The next watch/read will trigger a fresh build()
    
    // ✅ Invalidate from inside a notifier after a mutation
    Future<void> addTask(CreateTaskRequest request) async {
        final repo = ref.read(taskRepositoryProvider);
        await repo.createTask(request);
        if (!ref.mounted) return;
        ref.invalidateSelf(); // triggers build() to re-run and return fresh list
    }

    Decision: invalidateSelf() vs manual state assignment

    After a mutation:
    ├── Need optimistic UI (instant visual update before server confirms)?
    │   └── YES → Set state manually via AsyncValue.guard
    │             (e.g., remove item from list immediately, then call API)
    └── NO  → ref.invalidateSelf()  (simpler, always correct — re-runs build())

Show full SKILL.md (523 more words)Show less
Error Handling in Flutter/Riverpod

Define a typed exception hierarchy using sealed classes (Dart 3+). Map infrastructure exceptions to domain exceptions inside notifier actions.

dart
// core/errors/app_exception.dart — Typed exception hierarchy
sealed class AppException implements Exception {
    const AppException(this.message);
    final String message;

    @override
    String toString() => message;
}

class NetworkException extends AppException {
    const NetworkException(super.message, {this.statusCode});
    final int? statusCode;
}

class ValidationException extends AppException {
    const ValidationException(super.message, {required this.field});
    final String field;
}

class NotFoundException extends AppException {
    const NotFoundException(super.message);
}
dart
// ✅ Map infrastructure exceptions to domain exceptions inside notifier actions
Future<void> addTask(CreateTaskRequest request) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
        try {
            final repo = ref.read(taskRepositoryProvider);
            await repo.createTask(request);
            if (!ref.mounted) return state.requireValue;
            return repo.getTasks();
        } on DioException catch (e) {
            // Transform infrastructure error → domain error
            throw NetworkException(
                'Failed to create task: ${e.message}',
                statusCode: e.response?.statusCode,
            );
        }
    });
}
dart
// ✅ Handle typed errors in UI
asyncTasks.when(
    data: (tasks) => TaskListBody(tasks: tasks),
    loading: () => const LoadingIndicator(),
    error: (error, _) => switch (error) {
        NetworkException() => ErrorView(message: 'Network error: ${error.message}'),
        NotFoundException() => const ErrorView(message: 'Not found'),
        _ => ErrorView(message: 'Unexpected error: $error'),
    },
);

Rules:

  • All custom exceptions extend AppException (sealed class)
  • Infrastructure exceptions (DioException, SocketException) are caught and re-thrown as domain exceptions
  • Never expose infrastructure types (Dio, HTTP status codes) to the UI layer
  • Use exhaustive switch on the sealed class in error display
  • See error-handling-principles.md for general error handling guidance

Navigation with go_router

go_router is the canonical navigation library.

dart
// core/router/app_router.dart
@riverpod
GoRouter appRouter(Ref ref) {
    return GoRouter(
        initialLocation: '/tasks',
        routes: [
            GoRoute(path: '/tasks', builder: (_, __) => const TaskListView()),
            GoRoute(
                path: '/tasks/:id',
                builder: (_, state) => TaskDetailView(
                    // state.pathParameters['id'] is guaranteed non-null by the :id
                    // route pattern — acceptable use of ! in route infrastructure code
                    id: state.pathParameters['id']!,
                ),
            ),
        ],
    );
}

// Navigate — always by path, never by widget reference
context.go('/tasks/$taskId');
context.push('/tasks/new'); // push adds to the back stack

Dart Language Idioms
  1. Null safety — use ?., ??, and ??= idiomatically

    dart
    final city = user?.address?.city ?? 'Unknown';
    cache ??= await compute(); // assign only if null
  2. Use late only for fields initialized before first use that cannot be final

    • Prefer final fields initialized in the constructor
    • late without initialization is an unsafe nullable escape hatch
  3. Extension methods for adding behaviour to types you don't own

    dart
    extension TaskStatusLabel on TaskStatus {
        String get label => switch (this) {
            TaskStatus.pending => 'Pending',
            TaskStatus.done => 'Done',
        };
    }
  4. Use switch expressions (Dart 3+) for exhaustive pattern matching

    dart
    final label = status switch {
        TaskStatus.pending => 'Pending',
        TaskStatus.done => 'Done',
        // Compiler error if a case is missing
    };
  5. Avoid dynamic — it is the Dart equivalent of TypeScript's any


Testing

Test naming and pyramid proportions are defined in testing-strategy.md. This section covers Flutter/Riverpod 3 test patterns.

Unit Test Providers with ProviderContainer.test

ProviderContainer.test creates an isolated container that auto-disposes after the test — no manual addTearDown needed.

dart
test('addTask updates state', () async {
    final container = ProviderContainer.test(overrides: [
        taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
    ]);

    await container.read(taskListProvider.notifier).addTask(request);
    expect(container.read(taskListProvider).value, hasLength(1));
});
Override Only the build Method with overrideWithBuild

When you need to control initial state but keep the notifier's methods intact:

dart
test('deleteTask removes item from pre-seeded list', () async {
    final container = ProviderContainer.test(overrides: [
        taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
        taskListProvider.overrideWithBuild((ref, notifier) {
            // Seed initial state — methods (addTask, deleteTask) still work
            return Future.value([mockTask1, mockTask2]);
        }),
    ]);

    await container.read(taskListProvider.notifier).deleteTask(mockTask1.id);
    expect(container.read(taskListProvider).value, hasLength(1));
});
Widget Tests with ProviderScope
dart
testWidgets('shows task list', (tester) async {
    await tester.pumpWidget(ProviderScope(
        overrides: [
            taskRepositoryProvider.overrideWith((_) => MockTaskRepository()),
        ],
        child: const MaterialApp(home: TaskListView()),
    ));
    expect(find.byType(TaskCard), findsWidgets);
});
Use mockito with @GenerateNiceMocks for Interface Mocks

Place the annotation on the library (top of the test file or on main) and add a part directive for the generated file:

dart
// task_notifier_test.dart
@GenerateNiceMocks([MockSpec<TaskRepository>()])
library;

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'task_notifier_test.mocks.dart'; // generated by build_runner

void main() {
    // tests use MockTaskRepository()
}

Anti-Patterns — NEVER DO THIS

These are common mistakes agents reproduce from Riverpod 2 training data. Every item below is a hard rule violation.

❌ Anti-Pattern✅ Correct Pattern
StateProvider@riverpod class with Notifier
StateNotifierProvider@riverpod class with Notifier
ChangeNotifierProvider@riverpod class with Notifier
import 'package:riverpod/legacy.dart'Never import legacy APIs
Typed ref subclass (TaskDetailRef ref)Ref ref — Riverpod 3 uses a single Ref type; typed subclasses were a Riverpod 2 codegen artifact that no longer exists
ref.watch inside async/event handlerref.read for one-shot reads in handlers
Accessing state/ref after await without ref.mounted checkAlways check ref.mounted after await
ProviderContainer() + addTearDown(container.dispose)ProviderContainer.test(overrides: [...])
Timer.periodic for polling dataStreamProvider or Stream return
keepAlive: false in annotationOmit — false is the default
Manual providers without @riverpod annotationAlways use code generation
Catching raw exceptions from provider readsCatch ProviderException
overrideWith((_) => MockNotifier()) when only initial state needs seedingoverrideWithBuild(...) — keeps notifier methods intact, only seeds initial state

Linting and Formatting
ToolPurposeConfig File
dart formatCanonical formatting— (built-in)
flutter analyzeStatic analysis + lintanalysis_options.yaml
riverpod_lintRiverpod-specific lint rulesdev_dependencies
dart pub depsDependency audit—
dart run build_runner buildGenerate provider code—

Mandatory analysis_options.yaml settings (Dart 3+):

yaml
analyzer:
  language:
    strict-casts: true
    strict-raw-types: true
  errors:
    invalid_assignment: error
  plugins:
    - riverpod_lint
linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - avoid_dynamic_calls
    - avoid_print
    - use_super_parameters

After any code change involving providers, run:

bash
dart run build_runner build --delete-conflicting-outputs
flutter analyze
dart format .

During active development, use watch mode to avoid re-running manually:

bash
dart run build_runner watch --delete-conflicting-outputs

  • Code Idioms and Conventions @code-idioms-and-conventions.md
  • Project Structure — Flutter Mobile @.agents/skills/flutter-idioms/references/project-structure.md
  • Security Principles @.agents/rules/security-principles.md
  • Accessibility Principles @.agents/rules/accessibility-principles.md
  • Architectural Patterns — Testability-First Design @.agents/rules/architectural-pattern.md
  • Testing Strategy @testing-strategy.md
  • Error Handling Principles @error-handling-principles.md
  • Dependency Management Principles @dependency-management-principles.md

© irahardianto, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 1 other file (references) in .agents/skills/flutter-idioms of irahardianto/awesome-agv.

  • SKILL.md
  • references/project-structure.md

Open the folder on GitHubat commit 9e997ba

Compare with similar skills

Flutter Idioms next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Flutter Idioms compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Flutter Idioms this skillirahardianto/awesome-agv156—~7.3kAutomated safety check: PassMIT
Flutter Initbear2u/my-skills932—~2.5kAutomated safety check: PassNone
Flutter Riverpod State Managementandrewyng/context-hub14k—~1.1kAutomated safety check: PassMIT
BlocVeryGoodOpenSource/vgv-ai-flutter-plugin170—~2kAutomated safety check: PassMIT
Flutter Dart Code Reviewaffaan-m/ECC277k2 repos~5.8kAutomated safety check: NotesMIT
Signals for Flutterrodydavis/signals.dart819—~1.9kAutomated safety check: PassApache-2.0

Similar skills

  • Flutter Init

    bear2u/my-skills

    A skill your agent uses when user wants to create a new Flutter project (Todo/Habit/Note/Expense/Custom domain) with Clean Architecture, Riverpod 3.0, Drift, and modern Flutter stack

    932 GitHub stars~2.5k tokensUpdated 3 mo ago
    MobileAuto-check passed
  • Guides Flutter state management with Riverpod: choosing provider types, wiring ConsumerWidgets, handling async state and lifecycle, and overriding providers in tests.

    14k GitHub stars~1.1k tokensUpdated 4 mo ago
    MobileAuto-check passed
  • Bloc

    VeryGoodOpenSource/vgv-ai-flutter-plugin

    Best practices for Bloc state management in Flutter/Dart, covering Cubit versus Bloc, event and state naming, sealed classes with Equatable, the Page/View split with BlocProvider, BlocBuilder…

    170 GitHub stars~2k tokensUpdated 5 days ago
    MobileAuto-check passed
  • Library-agnostic Flutter/Dart code review checklist covering widget best practices, state management patterns (BLoC, Riverpod, Provider, GetX, MobX, Signals), Dart idioms, performance…

    277k GitHub starsUsed in 2 repos~5.8k tokens
    MobileAuto-check: notes
  • Signals for Flutter

    rodydavis/signals.dart

    Reference for the signals_flutter package: reactive signals, computed values, effects, async signals and collection signals bound to Flutter widgets.

    819 GitHub stars~1.9k tokensUpdated 27 days ago
    MobileAuto-check passed
  • Flutter

    alinaqi/maggy

    Flutter development with Riverpod state management, Freezed, gorouter, and mocktail testing

    707 GitHub stars~3.5k tokensUpdated 16 days ago
    MobileAuto-check passed

More from irahardianto/awesome-agv

All 34 skills in this repo
  • Distinctive Frontend Design Builder

    irahardianto/awesome-agv

    Commits to one bold aesthetic direction, sets up a CSS token system for it, then builds the interface in Vue or plain HTML using those tokens.

    156 GitHub stars~2.4k tokensUpdated 6 days ago
    Auto-check passed
  • Perf Optimization

    irahardianto/awesome-agv

    Profile-driven performance optimization protocol. An agent skill from irahardianto/awesome-agv.

    156 GitHub stars~4.3k tokensUpdated 6 days ago
    Auto-check passed
  • Angular Idioms and Patterns

    irahardianto/awesome-agv

    Coding conventions for Angular 19 and later: standalone components, signals, OnPush change detection, lazy routes and where RxJS still belongs.

    156 GitHub stars~3.8k tokensUpdated 6 days ago
    Auto-check passed
  • CI/CD Pipeline Principles

    irahardianto/awesome-agv

    Rules for designing CI/CD pipelines in layers: universal lint, test and scan stages, container builds with SBOM attestation, and GitOps for orchestrated deployments.

    156 GitHub stars~2.7k tokensUpdated 6 days ago
    Auto-check: notes
  • Hono Idioms

    irahardianto/awesome-agv

    Hono lightweight web framework patterns: type-safe route handlers, middleware composition, Zod validation, and RPC clients for Cloudflare Workers, Node, or Bun.

    156 GitHub stars~3k tokensUpdated 6 days ago
    Auto-check passed
  • Mobile Testing

    irahardianto/awesome-agv

    Mobile E2E testing patterns — Flutter integrationtest, Patrol, Maestro, golden testing, device matrix, and test data management.

    156 GitHub stars~1.8k tokensUpdated 6 days ago
    Auto-check: notes

Works with

Categories

Questions about Flutter Idioms

What does Flutter Idioms do?

Flutter idioms: Riverpod 3 state management, Freezed immutable models, gorouter navigation, const widget optimization, and repository patterns. Flutter Idioms is an agent skill from irahardianto/awesome-agv. Flutter idioms: Riverpod 3 state management, Freezed immutable models, gorouter navigation, const widget optimization, and repository patterns.

When should I use Flutter Idioms?

Flutter Idioms fits situations like: reviewing Flutter mobile applications; tasks that involve Cross-platform mobile apps; tasks that involve State management.

How do I install Flutter Idioms in Claude Code?

Run `npx skills add irahardianto/awesome-agv --skill flutter-idioms -a claude-code`. Or copy the skill folder (.agents/skills/flutter-idioms in irahardianto/awesome-agv) into .claude/skills/flutter-idioms in your project. Claude Code loads it when a task matches its description.

How do I install Flutter Idioms in Codex?

Run `npx skills add irahardianto/awesome-agv --skill flutter-idioms -a codex`. Or copy the skill folder (.agents/skills/flutter-idioms in irahardianto/awesome-agv) into .agents/skills/flutter-idioms in your project. Codex loads it when a task matches its description.

Can I use Flutter Idioms in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add irahardianto/awesome-agv --skill flutter-idioms -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/flutter-idioms, .gemini/skills/flutter-idioms, .github/skills/flutter-idioms and .opencode/skills/flutter-idioms in your project.

What does Flutter Idioms need to run?

Going by SKILL.md and its folder, Flutter Idioms needs the command-line tools its instructions call (dart and flutter).

Does Flutter Idioms access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Flutter Idioms safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Flutter Idioms use?

Flutter Idioms is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Flutter Idioms use?

About 7.3k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 1.4k tokens, read only when the agent opens those files.

What are the alternatives to Flutter Idioms?

Skills that share tags, products or a category with Flutter Idioms: Flutter Init (bear2u/my-skills, 932 stars), Flutter Riverpod State Management (andrewyng/context-hub, 14k stars), Bloc (VeryGoodOpenSource/vgv-ai-flutter-plugin, 170 stars) and Flutter Dart Code Review (affaan-m/ECC, 277k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Flutter Idioms?

irahardianto (a GitHub user) maintains it in irahardianto/awesome-agv, which has 156 GitHub stars. The repository holds 34 skills in this directory. The repository was last updated on October 5, 2026.

Source: irahardianto/awesome-agv on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.