Skip to content

Instantly share code, notes, and snippets.

@damienissa
Created October 13, 2025 18:53
Show Gist options
  • Select an option

  • Save damienissa/4219d377b773763a701941ef2cca1698 to your computer and use it in GitHub Desktop.

Select an option

Save damienissa/4219d377b773763a701941ef2cca1698 to your computer and use it in GitHub Desktop.

Flutter Project Rules (.mdc)

0) Scope

These rules govern architecture, code style, and file layout for this Flutter repo. They are enforceable guidelines for PR review and AI-assisted edits in Cursor.


1) Architecture & Folder Structure (Feature-first, Clean-ish)

Goal: Separate concerns by feature, keep UI and data apart, and share only stable cross-cutting utilities.

lib/
  app/
    core/
      constants/          # spacing, durations, zIndex, config, api timeouts
      routing/            # AppRoutes (constants), router init, nav helpers
      di/                 # get_it registrations
      error/              # Failure + typed exceptions
      l10n/               # gen-l10n outputs + context extensions
    bootstrap.dart        # runApp + DI + router
  features/
    <feature>/
      data/
        models/
        dtos/
        sources/          # api/local data sources
        repositories/     # impl only; throws typed Failures
      domain/
        entities/
        repositories/     # abstract contracts only
        usecases/         # thin orchestration; may throw Failures
      presentation/
        cubit/            # Cubit + State (Freezed allowed)
        widgets/
        pages/

Import boundaries (must):

  • presentation â�� domain â��
  • data â�� domain â��
  • presentation â�� data â�� (UI must never import data)
  • domain â�� presentation â�� (no upward deps)
  • Cross-feature imports go only through app/core/* shared utilities.

File naming (should):

  • snake_case files; *_page.dart, *_widget.dart, *_cubit.dart, *_state.dart, *_usecase.dart, *_repository.dart, *_entity.dart.

2) Error Handling (Throw-first, No Either)

Policy:

  • Use throw/try-catch with typed Failure exceptions. No Either, no Result.
// app/core/error/failures.dart
sealed class Failure implements Exception {
  final String message;
  const Failure(this.message);
}
class NetworkFailure extends Failure { const NetworkFailure(super.message); }
class ApiFailure extends Failure     { const ApiFailure(super.message); }
class CacheFailure extends Failure   { const CacheFailure(super.message); }
  • data/* converts low-level errors â�� typed Failure.
  • domain/* may rethrow Failure after domain guards.
  • presentation/* catches Failure and maps to user-friendly states (use l10n keys for messages).

3) State Management (Cubit)

Rules:

  • One Cubit per page/flow.
  • One sealed State class per Cubit (idle|loading|success|error|empty minimal set).
  • Expose side-effects via methods; keep constructors DI-friendly.
class ExampleCubit extends Cubit<ExampleState> {
  ExampleCubit(this._getData) : super(const ExampleState.idle());
  final GetData _getData;

  Future<void> load() async {
    emit(const ExampleState.loading());
    try {
      final data = await _getData();
      emit(ExampleState.success(data));
    } on Failure catch (e) {
      emit(ExampleState.error(e.message)); // message = l10n key preferred
    } catch (_) {
      emit(const ExampleState.error('unexpected_error'));
    }
  }
}

4) Routing (go_router + constants only)

Must:

  • All paths live in AppRoutes constants. No route enums. No magic strings.
// app/core/routing/app_routes.dart
abstract final class AppRoutes {
  static const splash  = '/';
  static const home    = '/home';
  static const details = '/details/:id';

  static String detailsPath(String id) => '/details/$id';
}
  • Router defined once in app/core/routing/router.dart.
  • Each feature exposes its own featureXRoutes: List<RouteBase> and is registered in the root.
final GoRouter appRouter = GoRouter(
  routes: [
    GoRoute(path: AppRoutes.splash, builder: (_, __) => const SplashPage()),
    ...featureXRoutes, // provided by features/<feature>/presentation/<feature>_routes.dart
  ],
);
  • Prefer helper for navigation (avoid inline path formatting):
abstract final class AppNav {
  static void toDetails(BuildContext c, String id) => c.go(AppRoutes.detailsPath(id));
}

5) Localization (gen-l10n, zero inline strings)

Must:

  • Every user-visible string is localized via gen-l10n.
  • Keys are feature-scoped: home_title, details_buy_button, auth_error_network.

ARB files: app/l10n/arb/app_en.arb, app_uk.arb, etc.
Accessor:

extension L10nX on BuildContext {
  AppLocalizations get l10n => AppLocalizations.of(this)!;
}

Usage: Text(context.l10n.home_title)

Lint (regex suggestions for reviews):

  • Disallow raw strings in widgets:
    • Flag: Text\(['"].+?['"]\) (allowlist: test files, debug-only).
    • Flag any SnackBar\(.*?content:\s*Text\(['"] not using l10n.

6) Constants (No magic numbers or strings)

Must:

  • UI numbers go in app/core/constants/ui.dart.
  • Config/timeouts/limits go in app/core/constants/config.dart.
  • Colors, spacing, radii, durations, z-indices centralised.
abstract final class Gaps { 
  static const xs = 4.0;  static const s = 8.0; 
  static const m  = 16.0; static const l = 24.0; static const xl = 32.0;
}
abstract final class DurX {
  static const fast = Duration(milliseconds: 150);
  static const normal = Duration(milliseconds: 250);
}

Review checklist:

  • â�� No numeric literals in widgets/layout except 0 or 1 for index/opacity toggles.
  • â�� No route strings outside AppRoutes.
  • â�� Prefer const constructors and final fields.

7) Dependency Injection (get_it)

Policy:

  • Use plain get_it (no injectable unless explicitly added).
  • Register interfaces â�� implementations; presentation requests use cases or repositories via DI.
final getIt = GetIt.instance;

void setupDi() {
  // data
  getIt.registerLazySingleton<HttpClient>(() => HttpClientImpl());
  getIt.registerLazySingleton<ExampleRepository>(() => ExampleRepositoryImpl(getIt()));

  // domain
  getIt.registerFactory(() => GetData(getIt<ExampleRepository>()));

  // presentation
  getIt.registerFactory(() => ExampleCubit(getIt<GetData>()));
}

8) Testing

Must:

  • domain: unit test use cases & repository contracts (throws Failure correctly).
  • data: mock IO/http; JSON fixtures for models; mapping & error translation.
  • presentation: Cubit tests for state sequences (success/typed failure).
  • widget: critical pages (golden optional).

9) Code Style & Lints

Required lints (suggested):

  • flutter_lints + custom rules: prefer_final_locals, exhaustive_switch, avoid_print, unawaited_futures, no_logic_in_build, always_use_package_imports.

Style:

  • Small widgets as classes (not build methods).
  • No business logic in build().
  • Use sealed classes where appropriate (states/failures).
  • Avoid dynamic; prefer explicit types.
  • async APIs return Future<T>; cancel long ops in dispose.

10) Anti-Patterns (Do Not)

  • ❌�� UI importing data/*
  • ❌�� Raw strings in widgets/snackbars/dialogs
  • ❌�� Route strings outside AppRoutes
  • ❌�� Magic numbers for padding/sizes/durations
  • ❌�� Business logic inside build() / Widgets
  • ❌�� Swallowing exceptions (always map to Failure or rethrow)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment