flutter-ai-rules

repository·main·Indexed 20 days ago

https://github.com/evanca/flutter-ai-rules

A collection of Flutter and Dart rules and 'skills' designed to enhance AI coding assistants such as Cursor, Windsurf, and Claude Code. It provides modular, documentation-based instructions and curated rule sets to ensure AI agents follow official best practices, including Effective Dart usage, Dart 3 patterns, Bloc architecture, and Mocktail testing.

Tokens
108.5K
Snippets
229
Records
415
Agent score
68%

What's inside flutter-ai-rules

  1. Genkit Dart Core Features and API Reference

    main

    Genkit Dart is an AI SDK for Dart providing a unified interface for AI agents, code generation, and structured outputs.

    Core capabilities include:

    • Initialization: Genkit()
    • Generation: ai.generate
    • Tooling: ai.defineTool
    • Flows: ai.defineFlow
    • Embeddings: ai.embedMany
    • Streaming and remote flow endpoint calls.

    For detailed API signatures and usage patterns, refer to the core framework reference at references/genkit.md.

  2. Manage Crashlytics collection and opt-in reporting

    main

    Crashlytics collects reports automatically by default. You can control data collection to implement opt-in/opt-out flows.

    Disable in Debug Builds

    It is best practice to disable collection during development to avoid polluting your production data:

    if (kReleaseMode) {
      await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
    } else {
      await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false);
    }

    Opt-in/Opt-out Logic

    • Use setCrashlyticsCollectionEnabled(true) to enable reporting (e.g., after a user grants permission).
    • The setting persists across app launches.
    • When disabled, crash info is stored locally. If later enabled, these locally stored crashes will be sent to Crashlytics.
  3. Use Dart 3 Records for data aggregation

    main

    Records are anonymous, immutable, and strongly typed aggregate types used to bundle multiple objects. They are ideal for functions returning multiple values.

    • Syntax: Use parentheses for positional and named fields: ('first', a: 2, b: true).
    • Access: Positional fields are accessed via $1, $2, etc. Named fields are accessed by name (e.g., .a).
    • Equality: Records are structurally typed; they are equal if they have the same shape and all corresponding field values are equal.
    • Destructuring: Use pattern matching to extract values: var (name, age) = userInfo; or final (:name, :age) = userInfo; for named fields.
    • Best Practice: Use records for simple, immutable data tuples. Use classes when you need abstraction, encapsulation, or behavior.
    // Creating a record
    var userInfo = ('Alice', age: 30, isAdmin: true);
    
    // Accessing fields
    print(userInfo.$1); // Alice
    print(userInfo.age); // 30
    
    // Destructuring
    var (name, age: userAge) = userInfo;
    
    // Using type aliases for readability
    typedef UserRecord = ({String name, int age});
    UserRecord user = (name: 'Bob', age: 25);
  4. Manage state disposal and lifecycle in Riverpod

    main

    Riverpod provides several mechanisms to control when provider state is destroyed or cleaned up.

    Disposal Strategies:

    • Automatic Disposal: With code generation, state is destroyed when no longer listened to for a full frame. For non-codegen providers, use .autoDispose.
    • Manual Opt-out: Use keepAlive: true (codegen) or ref.keepAlive() (manual) to prevent automatic disposal.
    • Manual Invalidation: Use ref.invalidate(provider) to force destruction of state. If the provider is still being listened to, a new state will be created. Use ref.invalidateSelf() inside a provider to recreate itself.
    • Cleanup Logic: Use ref.onDispose to register cleanup code (e.g., closing a controller). Use ref.onCancel when the last listener is removed and ref.onResume when a new listener is added.

    Important: Always enable autoDispose for providers that receive parameters to prevent memory leaks from unused parameter combinations.

    final myProvider = FutureProvider.autoDispose((ref) async {
      final controller = SomeController();
      
      // Register cleanup
      ref.onDispose(() {
        controller.dispose();
      });
    
      return controller.fetchData();
    });
  5. Manage Shared Code Between Features

    main

    To prevent tight coupling, features should never depend on each other's internals.

    • Shared Code: Common components (design systems, utilities, shared domain models) should live in lib/common/ or in dedicated packages.
    • Cross-Feature Communication: If Feature A needs to interact with Feature B, either promote the required logic to shared code or communicate via events (refer to enterprise-scale.md for patterns).
  6. Design for older users' age-related ability changes

    main

    Age-related decline often overlaps with established disability access needs. Instead of creating a separate "senior mode," implement standard accessibility practices to cover the four primary areas of age-related change:

    1. Vision: Address reduced contrast sensitivity, color perception, and near-focus by providing solid contrast and readable text.
    2. Physical / Motor: Address reduced dexterity and fine motor control by providing large, well-spaced click/tap targets.
    3. Hearing: Address difficulty hearing higher-pitched sounds or separating sounds by providing clear audio and managing background music.
    4. Cognitive: Address reduced short-term memory and difficulty concentrating by providing simple navigation and low memory load.

    Following WCAG (Web Content Accessibility Guidelines) is the standard way to address most of these needs.

  7. Follow Bloc Naming Conventions

    main

    Standardize your Bloc/Cubit implementation using these naming rules:

    Events

    • Use past tense to reflect actions that have occurred.
    • Format: [BlocSubject][Optional Noun][Verb] (e.g., UserLoginPressed).
    • For initial loads, use [BlocSubject]Started (e.g., UserStarted).

    States

    • Use nouns for state names.
    • Subclass Pattern: Use [BlocSubject] + Initial | Success | Failure | InProgress (e.g., UserSuccess).
    • Single Class Pattern: Use [BlocSubject]State with a Status enum (initial, success, failure, loading).
    • Implementation Details:
      • Prefer sealed classes for exclusive states.
      • State classes should extend Equatable, use @immutable, implement a copyWith method, and use const constructors.
      • Crucial: Always include all relevant properties in the props getter when using Equatable to ensure correct equality checks.
  8. Configure In-App Messaging campaigns

    main

    Campaigns are managed in the Firebase console under Messaging > In-App Messaging.

    Supported Message Types

    • modal
    • banner
    • card
    • image-only

    Campaign Configuration Workflow

    1. Design: Select type and customize appearance (title, body, image, button).
    2. Target: Define audience (app version, language, user segment, or Analytics conditions).
    3. Schedule: Set start/end dates and frequency caps (e.g., once per session).
    4. Trigger: Choose the Analytics event (e.g., app_open, purchase_complete).
    5. Test: Use device-specific testing.
    6. Publish: Launch and monitor performance via Firebase analytics.
  9. Manage authentication state with Streams

    main

    Firebase Authentication provides different streams to observe changes in user state. Choose the stream that matches your requirements:

    StreamFires when
    authStateChanges()User signs in or out
    idTokenChanges()ID token changes (including custom claims)
    userChanges()User data changes (e.g. profile updates)

    Example: Listening for sign-in/sign-out events

    FirebaseAuth.instance
      .authStateChanges()
      .listen((User? user) {
        if (user == null) {
          print('User is currently signed out!');
        } else {
          print('User is signed in!');
        }
      });

    Note: Listen to these streams immediately upon app startup to handle the initial authentication state. Custom claims are only available after sign-in, re-authentication, token expiration, or manual token refresh.

  10. Component Responsibilities in Flutter Architecture

    main

    Each component in the layered architecture has a specific role:

    • View: Responsible for data presentation. It should contain minimal logic and only UI-related code. It passes user interaction events to the ViewModel.
    • ViewModel: Converts raw data from Repositories into UI state. It maintains the current state needed by the View and exposes callbacks (commands) for the View to trigger.
    • Repository (SSOT): The only class allowed to mutate data. It handles caching, error handling, and data refresh logic. It transforms raw data from Services into domain models.
    • Service: A stateless wrapper around external data sources (like API endpoints). It isolates data-loading logic and holds no state.
  11. Core Architecture Rules for Flutter

    main

    Follow these architectural principles to ensure maintainability:

    1. Separate UI from data: Maintain two broad layers: UI (views + view models/cubits) and Data (repositories + services). Dependencies must flow one way: View → ViewModel → Repository → Service. Lower layers must never import upper layers, and repositories should not depend on each other.
    2. Views hold no business logic: Widgets should only handle UI concerns (show/hide, animation, layout, simple routing). All data transformation and decision logic must reside in the view model (cubit/bloc), which must have no access to BuildContext.
    3. Organize by feature, not by layer: Group everything a feature needs (state management, widgets, models) under a single feature directory. Avoid top-level blocs/, widgets/, or models/ folders that scatter feature code.
    4. State is immutable and explicit: Model UI state using sealed/union types (e.g., initial, inProgress, failure, ready) to ensure exhaustive handling. Treat one-off effects (snackbars, navigation) as events rather than state.
    5. Add layers only when necessary: Start with view-model → API client. Only introduce a Repository when caching, offline support, or merging sources is required. Only introduce a Use Case when logic is complex, reused by multiple view models, or merges multiple repositories.
  12. Optimize performance and security in Firebase Data Connect

    main

    Performance Best Practices

    • Field Selection: Design efficient queries that request only the specific fields required by the UI to minimize data transfer.
    • Pagination: For large datasets, implement pagination using limit and offset in your GraphQL queries.
    • Listener Usage: Use real-time listeners judiciously to avoid excessive network consumption.
    • Caching: Implement local caching for critical data to support offline functionality.

    Security Best Practices

    • Access Control: Use the @auth directive in your schema to enforce access levels (PUBLIC, USER, NO_ACCESS).
    • Authentication: Integrate Firebase Authentication to manage user-based access.
    • Validation: Validate data on both the client and the server side.