flutter-patterns
Skill4 starsChanged 3 months agoMITView on GitHub
Flutter app conventions: Freezed models, Riverpod providers, a Dio service layer, typed Failures, pagination, and routing. Apply when writing or reviewing Flutter/Dart code.
6 other projects use a version of this.
Adopted by
Most recently taken up by
- qrvello/flutter-claude-codetheir own version, first seen Oct 9, 2026
- jahfaliabdulrahman-dev/hermes-skillstheir own version, first seen Oct 3, 2026
- HeyEddi-com/heyeddi-skillstheir own version, first seen Aug 25, 2026
- seek888/claude-skillstheir own version, first seen Apr 9, 2026
- terrene-foundation/pacttheir own version, first seen Apr 6, 2026
- FaisalAlqarni/sp-ecctheir own version, first seen Feb 6, 2026
Other versions6 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 6
- FaisalAlqarni/sp-ecc★ 9same name
- jahfaliabdulrahman-dev/hermes-skills★ 1same name
- qrvello/flutter-claude-code★ 1same name
- HeyEddi-com/heyeddi-skills★ 0same name
- seek888/claude-skills★ 0same name
- terrene-foundation/pact★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository53 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- llms.txtllms.txt
- actionableSkill
- astro-bootstrapSkill
- astro-patternsSkill
- autopilotSkill
- axon-code-intelSkill
- brainstormingSkill
- commit-conventionsSkill
- cross-repo-contextSkill
- django-bootstrapSkill
- django-patternsSkill
- executeSkill
- find-sessionSkill
- flutter-bootstrapSkill
- full-featureSkill
- intelSkill
- nextjs-bootstrapSkill
- nextjs-fullstack-bootstrapSkill
- nextjs-fullstack-patternsSkill
- nextjs-mui-bootstrapSkill
- nextjs-patternsSkill
- nextjs-shadcn-bootstrapSkill
- orchestratorSkill
- phase-breakdownSkill
- plan-and-scopeSkill
- prdSkill
- reactSkill
- recallSkill
- reflectSkill
- relaySkill
- rememberSkill
- scaffoldSkill
- scratchSkill
- tailwindSkill
- ux-specSkill
- ux-to-promptsSkill
- verify-astroSkill
- verify-djangoSkill
- verify-flutterSkill
- verify-nextjs-fullstackSkill
- verify-nextjs-muiSkill
- verify-nextjs-shadcnSkill
- verify-nextjsSkill
- verifySkill
- visual-feedbackSkill
- work-disciplineSkill
- worktreesSkill
- wrapSkill
- write-as-friendSkill
- write-as-humanSkill
- writing-clearlySkill
- writing-skillsSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
15 rules. 0 are common, 5 are distinctive, and 10 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
5
Distinctive
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 46
Use Freezed with json_serializable.
- Distinctive: line 86
Keep models flat — no nested domain/data split
- Very specific: line 157
Use @riverpod codegen everywhere.
- Very specific: line 268
After mutation, ref.invalidate() related list/detail providers
- Very specific: line 325
Use AsyncValue.when() to handle loading/error/data states
- Very specific: line 360
Prefer const constructors — all fields final
- Very specific: line 383
Use ConsumerWidget only when the widget needs ref
- Distinctive: line 384
Avoid deep nesting — extract sub-widgets at 3-4 levels
- Very specific: line 458
Use flutter_secure_storage for token persistence
- Very specific: line 597
Use image_picker for selection.
- Very specific: line 657
Use the infinite_scroll_pagination package (v5) with a Riverpod provider exposing the PagingController.
- Very specific: line 795
Use mocktail for mocking (not mockito)
- Distinctive: line 798
Test behavior, not implementation — assert what the user sees
- Distinctive: line 802
When starting Flutter frontend work from a backend integration summary:
- Distinctive: line 807
Verify against the real API before polishing UI
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Skill: Flutter Patterns
Purpose
Guide Claude Code when implementing Flutter features in a Django-paired project. Follow these conventions unless the project's CLAUDE.md overrides them.
Project Structure
lib/
├── main.dart
├── config/
│ ├── env.dart # Compile-time env (--dart-define)
│ └── constants.dart # App-wide constants
├── core/
│ ├── api/ # Dio client, interceptors
│ ├── error/ # Sealed Failure hierarchy
│ ├── router/ # GoRouter, auth guard
│ ├── theme/ # Material 3 ThemeData
│ ├── providers/ # App-level (session, auth state)
│ ├── utils/ # Validation, formatting
│ └── widgets/ # BaseScreen, shared dialogs
├── features/
│ └── {feature}/
│ ├── models/ # Freezed data classes
│ ├── services/ # Interface + Django impl
│ ├── providers/ # Riverpod state
│ ├── screens/ # Full-page widgets
│ └── widgets/ # Feature-specific widgetsNaming conventions:
- Files:
snake_case.dart(e.g.book_service_django.dart) - Classes:
PascalCase(e.g.BookServiceDjango) - Features: singular noun (
profile,book,auth) - Screens:
{feature}_{action}_screen.dart(book_list_screen.dart,book_edit_screen.dart) - Generated files: co-located (
book.freezed.dart,book.g.dart,book_providers.g.dart)
Models
Use Freezed with json_serializable. Models mirror API response shapes.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'book.freezed.dart';
part 'book.g.dart';
@freezed
abstract class Book with _$Book {
const Book._();
factory Book({
@JsonKey(includeToJson: false) String? uuid,
required String title,
required String author,
@JsonKey(name: 'published_date') required DateTime publishedDate,
String? description,
@JsonKey(name: 'cover_url') String? coverUrl,
}) = _Book;
factory Book.fromJson(Map<String, dynamic> json) => _$BookFromJson(json);
factory Book.empty() => Book(
title: '',
author: '',
publishedDate: DateTime.now(),
);
bool get exists => uuid != null;
}Conventions:
- Freezed 3.x: annotate data classes
abstract(unions usesealed);@freezedalone no longer generates the class body const Book._()private constructor enables custom getters/methods@JsonKey(name: ...)for snake_case API fields@JsonKey(includeToJson: false)for server-managed fields likeuuid(the public identifier — never an integerid)factory Book.empty()for form initializationbool get existsfor create-vs-update logic- Keep models flat — no nested domain/data split
Services
Abstract interface defines the contract. One concrete implementation per backend.
// book_service.dart
abstract class BookService {
Future<PaginatedResponse<Book>> list({required int page, int pageSize = 20});
Future<Book> retrieve(String uuid);
Future<Book> save(Book book);
Future<void> delete(String uuid);
}// book_service_django.dart
class BookServiceDjango implements BookService {
final DioClient client;
BookServiceDjango(this.client);
@override
Future<PaginatedResponse<Book>> list({required int page, int pageSize = 20}) async {
final response = await client.get(
'/api/v1/books/',
queryParameters: {'page': page, 'page_size': pageSize, 'ordering': '-created_at'},
);
final results = (response['results'] as List)
.map((json) => Book.fromJson(json))
.toList();
return PaginatedResponse(
page: response['page'],
count: response['count'],
numPages: response['num_pages'],
results: results,
);
}
@override
Future<Book> retrieve(String uuid) async {
final response = await client.get('/api/v1/books/$uuid/');
return Book.fromJson(response);
}
@override
Future<Book> save(Book book) async {
if (book.exists) {
final response = await client.patch('/api/v1/books/${book.uuid}/', data: book.toJson());
return Book.fromJson(response);
}
final response = await client.post('/api/v1/books/', data: book.toJson());
return Book.fromJson(response);
}
@override
Future<void> delete(String uuid) async {
await client.delete('/api/v1/books/$uuid/');
}
}Conventions:
- Services throw typed
Failureexceptions (see Error Handling) save()combines create + update usingmodel.exists- No
Either/fpdart— useAsyncValuefor error handling at the provider level - One service per feature, one implementation per backend
Providers
Use @riverpod codegen everywhere. AsyncNotifier for stateful operations. Simple functional providers for reads.
Service binding
// book_service_provider.dart
@riverpod
BookService bookService(Ref ref) {
return BookServiceDjango(ref.watch(dioClientProvider));
}Detail provider (functional)
// book_detail_provider.dart
@riverpod
Future<Book> bookDetail(Ref ref, String uuid) async {
return ref.watch(bookServiceProvider).retrieve(uuid);
}List provider (AsyncNotifier)
// book_list_provider.dart
@riverpod
class BookList extends _$BookList {
int _page = 1;
@override
Future<PaginatedResponse<Book>> build() => _fetch();
Future<PaginatedResponse<Book>> _fetch() {
return ref.read(bookServiceProvider).list(page: _page);
}
Future<void> loadPage(int page) async {
_page = page;
state = const AsyncLoading();
state = await AsyncValue.guard(() => _fetch());
}
Future<void> refresh() async {
_page = 1;
ref.invalidateSelf();
}
}Form provider (Notifier)
// book_form_provider.dart
@Riverpod(keepAlive: true)
class BookForm extends _$BookForm {
@override
Book build() => Book.empty();
final formKey = GlobalKey<FormState>();
final titleController = TextEditingController();
final authorController = TextEditingController();
void load(Book book) {
state = book;
titleController.text = book.title;
authorController.text = book.author;
}
void reset() {
state = Book.empty();
titleController.clear();
authorController.clear();
}
Future<bool> submit() async {
if (!formKey.currentState!.validate()) return false;
final book = state.copyWith(
title: titleController.text,
author: authorController.text,
);
try {
final saved = await ref.read(bookServiceProvider).save(book);
state = saved;
reset();
ref.invalidate(bookListProvider);
if (saved.uuid != null) ref.invalidate(bookDetailProvider(saved.uuid!));
return true;
} on Failure catch (e) {
// Surface error to UI via snackbar or dialog — don't swallow
rethrow;
}
}
Future<bool> delete() async {
if (state.uuid == null) return false;
await ref.read(bookServiceProvider).delete(state.uuid!);
reset();
ref.invalidate(bookListProvider);
return true;
}
}Conventions:
@riverpod(lowercase) for auto-dispose providers — most reads@Riverpod(keepAlive: true)for forms and auth state that survive navigationAsyncValue.guard()for wrapping async calls in list/detail providers- Form providers own
TextEditingControllers andGlobalKey<FormState> - After mutation,
ref.invalidate()related list/detail providers
Screens
Screens extend BaseScreen, which provides Scaffold with responsive breakpoint support.
class BookListScreen extends BaseScreen {
static String route() => '/books';
const BookListScreen({super.key});
@override
AppBar? appBar(BuildContext context, WidgetRef ref) {
return AppBar(
title: const Text('Books'),
actions: [
IconButton(
onPressed: () {
ref.read(bookFormProvider.notifier).reset();
context.push(BookEditScreen.routeNew());
},
icon: const Icon(Icons.add),
),
],
);
}
@override
Widget body(BuildContext context, WidgetRef ref) {
return const BookInfiniteListWidget();
}
}class BookDetailScreen extends BaseScreen {
final String bookUuid;
static String route([String? uuid]) => '/books/${uuid ?? ':uuid'}';
const BookDetailScreen({super.key, required this.bookUuid});
@override
Widget body(BuildContext context, WidgetRef ref) {
final data = ref.watch(bookDetailProvider(bookUuid));
return data.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text(error.toString())),
data: (book) => BookDetailWidget(book: book),
);
}
}Conventions:
- Static
route()method returns the path string - Consume providers with
ref.watch()inbody(),ref.read()for actions - Use
AsyncValue.when()to handle loading/error/data states - Override
bodyMd()/bodyLg()only when layout differs at breakpoints
BaseScreen
abstract class BaseScreen extends ConsumerWidget {
final double horizontalPadding;
final double verticalPadding;
const BaseScreen({super.key, this.horizontalPadding = 16, this.verticalPadding = 8});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: appBar(context, ref),
floatingActionButton: floatingActionButton(context, ref),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: verticalPadding),
child: body(context, ref),
),
);
}
AppBar? appBar(BuildContext context, WidgetRef ref) => null;
FloatingActionButton? floatingActionButton(BuildContext context, WidgetRef ref) => null;
Widget body(BuildContext context, WidgetRef ref) => const SizedBox.shrink();
}Widgets
Extraction rules
- Extract when a widget exceeds ~40 lines or is reused
- One widget per file in
widgets/ - Prefer
constconstructors — all fieldsfinal - Compose widgets, never inherit (except
BaseScreen)
Consuming providers in widgets
class BookListTile extends ConsumerWidget {
final Book book;
const BookListTile({super.key, required this.book});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => context.push(BookDetailScreen.route(book.uuid)),
);
}
}Conventions:
- Pass data down as constructor params when possible
- Use
ConsumerWidgetonly when the widget needsref - Avoid deep nesting — extract sub-widgets at 3-4 levels
API Client
Singleton Dio instance with interceptor chain: auth, token refresh, logging.
The canonical DioClient is defined in the flutter-bootstrap skill at lib/core/api/dio_client.dart — don't re-implement it here. It exposes get/post/patch/delete, injects the bearer token, refreshes an expired access token against /api/v1/auth/refresh/ (reusing the stored refresh token, since the contract's refresh returns {access} only), and maps every DioException to a typed Failure.
Conventions:
- Single
Dioinstance — no per-request construction - Timeouts configured at construction
- Token refresh in the auth interceptor, not scattered across services — and a failed refresh clears the session and rejects the request rather than sending it unauthenticated
LogInterceptorgated behindkDebugMode— no rawprint()DioClientmapsDioExceptionto a typedFailureviamapDioException(see Error Handling) — services and providers only ever seeFailures
Auth
JWT session with secure storage, auth state provider, and router guard.
Session provider
The canonical secureStorage provider and the keep-alive Session notifier live in the flutter-bootstrap skill at lib/core/providers/session_provider.dart — Session holds the SessionToken, persists it to flutter_secure_storage, and exposes initialize/setToken/clearToken. Reference it rather than re-declaring it.
Auth notifier
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
@override
FutureOr<User?> build() async {
await ref.read(sessionProvider.notifier).initialize();
if (ref.read(sessionProvider) == null) return null;
return ref.read(authServiceProvider).currentUser();
}
Future<void> login({required String email, required String password}) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final user = await ref.read(authServiceProvider).login(email: email, password: password);
return user;
});
}
Future<void> logout() async {
await ref.read(authServiceProvider).logout();
ref.read(sessionProvider.notifier).clearToken();
state = const AsyncData(null);
}
}Route guard
@riverpod
GoRouter router(Ref ref) {
final auth = ref.watch(authProvider);
return GoRouter(
initialLocation: '/books',
redirect: (context, routerState) {
final isLoggedIn = auth.valueOrNull != null;
final isAuthRoute = routerState.matchedLocation.startsWith('/auth');
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
if (isLoggedIn && isAuthRoute) return '/books';
return null;
},
routes: [/* ... */],
);
}Conventions:
- Use
flutter_secure_storagefor token persistence - Auth provider uses
AsyncValue<User?>— null means logged out - Router watches auth provider and rebuilds on auth state changes
- Session provider is keepAlive — survives navigation
Error Handling
Sealed Failure class hierarchy. Services throw typed failures. Providers surface via AsyncValue.error.
The canonical Failure hierarchy — sealed class Failure with ServerFailure/NetworkFailure/AuthFailure/ValidationFailure — is defined in the flutter-bootstrap skill at lib/core/error/failures.dart. Reference it rather than re-declaring the classes; the conversion helper below is the DioException→Failure mapping the client layer applies.
Converting DioException to Failure (in DioClient or a helper)
Failure mapDioException(DioException e) {
if (e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.connectionTimeout) {
return const NetworkFailure();
}
final statusCode = e.response?.statusCode;
if (statusCode == 401 || statusCode == 403) {
return AuthFailure(e.response?.data?['detail'] ?? 'Authentication failed');
}
if (statusCode == 400) {
return ValidationFailure(
'Validation error',
fieldErrors: _parseFieldErrors(e.response?.data),
);
}
return ServerFailure(
e.response?.data?['detail'] ?? 'Server error',
statusCode: statusCode,
);
}Surfacing errors in UI
// In a screen or widget:
ref.listen(bookListProvider, (prev, next) {
if (next is AsyncError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next.error.toString())),
);
}
});Conventions:
- Services catch
DioExceptionand throw typedFailure - Providers use
AsyncValue.guard()— failures surface asAsyncError - UI uses
ref.listen()orAsyncValue.when()to display errors - No
Either/fpdart—AsyncValuehandles the loading/error/data tri-state
Forms
AsyncNotifier-based form providers own controllers and validation. Submission returns bool for navigation.
Form provider pattern (see Providers section for full example)
Key points:
TextEditingController+GlobalKey<FormState>live on the notifierload(Model)populates controllers from existing datareset()clears everything for a fresh formsubmit()validates, calls service, invalidates related providers, returns successkeepAlive: trueso form state survives back-navigation during editing
Form widget
class BookFormWidget extends ConsumerWidget {
const BookFormWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.read(bookFormProvider.notifier);
final book = ref.watch(bookFormProvider);
return Form(
key: notifier.formKey,
child: Column(
children: [
TextFormField(
controller: notifier.titleController,
validator: (v) => v == null || v.isEmpty ? 'Title required' : null,
decoration: const InputDecoration(labelText: 'Title'),
),
TextFormField(
controller: notifier.authorController,
decoration: const InputDecoration(labelText: 'Author'),
),
],
),
);
}
}Edit screen with submit
class BookEditScreen extends BaseScreen {
final String? bookUuid;
static String route([String? uuid]) => '/books/edit/${uuid ?? ':uuid'}';
static String routeNew() => '/books/new';
const BookEditScreen({super.key, this.bookUuid});
@override
AppBar? appBar(BuildContext context, WidgetRef ref) {
return AppBar(
title: Text(bookUuid != null ? 'Edit Book' : 'New Book'),
actions: [
IconButton(
onPressed: () async {
final success = await ref.read(bookFormProvider.notifier).submit();
if (success && context.mounted) context.pop();
},
icon: const Icon(Icons.check),
),
],
);
}
@override
Widget body(BuildContext context, WidgetRef ref) {
return const SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: BookFormWidget(),
),
);
}
}Image upload
Use image_picker for selection. Upload to the backend's asset endpoint, receive a URL, store it on the model.
Future<void> pickAndUploadImage(WidgetRef ref) async {
final picked = await ImagePicker().pickImage(source: ImageSource.gallery);
if (picked == null) return;
final bytes = await picked.readAsBytes();
final url = await ref.read(assetServiceProvider).upload(
filename: picked.name,
bytes: bytes,
);
// Store url on the form model via the form provider
}Lists
Two patterns: paginated (page controls) and infinite scroll (load-more).
Paginated list provider
See the BookList AsyncNotifier in the Providers section. UI consumes with:
final data = ref.watch(bookListProvider);
data.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text(e.toString()),
data: (paginated) => Column(
children: [
ListView.builder(
shrinkWrap: true,
itemCount: paginated.results.length,
itemBuilder: (context, i) => BookListTile(book: paginated.results[i]),
),
// Page controls
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (paginated.page > 1)
TextButton(
onPressed: () => ref.read(bookListProvider.notifier).loadPage(paginated.page - 1),
child: const Text('Previous'),
),
Text('Page ${paginated.page} of ${paginated.numPages}'),
if (paginated.page < paginated.numPages)
TextButton(
onPressed: () => ref.read(bookListProvider.notifier).loadPage(paginated.page + 1),
child: const Text('Next'),
),
],
),
],
),
);Infinite scroll
Use the infinite_scroll_pagination package (v5) with a Riverpod provider exposing the PagingController. v5 drops appendPage/appendLastPage/addPageRequestListener — the controller is built from getNextPageKey + fetchPage, and the widget consumes a PagingState via PagingListener.
@Riverpod(keepAlive: true)
class BookInfiniteList extends _$BookInfiniteList {
@override
PagingController<int, Book> build() {
final controller = PagingController<int, Book>(
// Stop once a fetched page comes back empty; otherwise request page N+1.
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (page) async {
final data = await ref.read(bookServiceProvider).list(page: page);
return data.results;
},
);
ref.onDispose(controller.dispose);
return controller;
}
void refresh() => state.refresh();
}Widget:
class BookInfiniteListWidget extends ConsumerWidget {
const BookInfiniteListWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = ref.watch(bookInfiniteListProvider);
return RefreshIndicator(
onRefresh: () async => ref.read(bookInfiniteListProvider.notifier).refresh(),
child: PagingListener(
controller: controller,
builder: (context, state, fetchNextPage) => PagedListView<int, Book>(
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<Book>(
itemBuilder: (context, book, index) => BookListTile(book: book),
noItemsFoundIndicatorBuilder: (_) => const Center(child: Text('No books found')),
),
),
),
);
}
}PaginatedResponse model
The canonical PaginatedResponse<T> — page, count, numPages, results, and canLoadMore — is defined in the flutter-bootstrap skill at lib/core/models/paginated_response.dart. It maps the contract's {page, count, num_pages, results} response shape (query params page/page_size). Reference it rather than re-declaring it.
Filtering and ordering
Pass filter/order params through the service and into query parameters:
Future<PaginatedResponse<Book>> list({
required int page,
int pageSize = 20,
String? search,
String ordering = '-created_at',
}) async {
final response = await client.get('/api/v1/books/', queryParameters: {
'page': page,
'page_size': pageSize,
'ordering': ordering,
if (search != null) 'search': search,
});
// ... parse response
}Testing
Unit tests (services and providers)
// Mock the service
class MockBookService extends Mock implements BookService {}
void main() {
late MockBookService mockService;
setUp(() {
mockService = MockBookService();
});
test('retrieve returns book', () async {
final book = Book(uuid: 'book-uuid-1', title: 'Test', author: 'Author', publishedDate: DateTime.now());
when(() => mockService.retrieve('book-uuid-1')).thenAnswer((_) async => book);
final result = await mockService.retrieve('book-uuid-1');
expect(result.title, 'Test');
});
}Widget tests
void main() {
testWidgets('BookListTile displays title and author', (tester) async {
final book = Book(id: 1, title: 'My Book', author: 'Jane', publishedDate: DateTime.now());
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(home: Scaffold(body: BookListTile(book: book))),
),
);
expect(find.text('My Book'), findsOneWidget);
expect(find.text('Jane'), findsOneWidget);
});
}Provider tests
void main() {
test('bookDetail returns data', () async {
final container = ProviderContainer(
overrides: [
bookServiceProvider.overrideWithValue(MockBookService()),
],
);
// Set up mock responses, then test provider output
});
}Conventions:
- Use
mocktailfor mocking (notmockito) - Override service providers in tests — never mock Dio directly
- Widget tests wrap with
ProviderScope+MaterialApp - Test behavior, not implementation — assert what the user sees
Working from Integration Summaries
When starting Flutter frontend work from a backend integration summary:
- Generate Freezed model classes matching response shapes
- Build the service interface + Django implementation
- Wire up providers (service binding, list, detail, form)
- Build screens and widgets
- Verify against the real API before polishing UI
- Flag any discrepancies between the summary and actual API behavior
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

