diet-guard/docs/flutter-rules-state-and-data.md
Krzysztof kuhy Rudnicki 17a2c62f26 Automate CLAUDE.md's structural rules, then cut it to 189 lines
CLAUDE.md was 1101 lines and loaded into every session in this repo. Three
passes, in order: automate what a gate can check, rewrite what is left as
instructions rather than rationale, extract what still does not fit.

Automated. Five prohibitions were enforced by nothing but a paragraph asking a
future reader not to do something; they are now repo_invariants_test.dart:

  - no app/linux/ (a GTK embedder target manages ~20fps at 4K)
  - dart:io only in *_io.dart seams and lib/desktop/ -- this one is why the
    test exists at all, since dart:io does not fail a web *compile*. It throws
    at runtime, so a bad import is a blank white window that the widget tests
    happily pass through.
  - no meal builder / repeat-last-meal / reward prompt
  - no image_picker, no CAMERA permission
  - wrapper port 8732 and the diet-guard-desktop Chrome profile, both pinned
    because IndexedDB is keyed by origin and moving either hides the whole
    local food log

Each guard was canary-tested by breaking the invariant and confirming the
suite goes red -- a guard that cannot fail is decoration.

Rules whose behaviour was already covered (budget epoch seeding, sync merge,
food bank) shrink to a pointer at the test that owns them.

Where a rule's entire value was its rationale ("the removal is the feature"),
the why moved into the name and reason string of the gate that now enforces
it, so it lives somewhere executable instead of somewhere quotable.

Extracted. The vendored flutter/flutter rules were 789 of the 1101 lines --
72% of the file, none of it about this repo. Split into five docs/ topic files
under the cap and linked. Verified content-complete: 664 non-blank lines in,
664 out, zero missing.

Python 692 tests / 100% coverage unchanged; Flutter 548 -> 555 (+7 new).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK3HCZrM75sRHL1gL8usve
2026-08-15 16:39:57 +02:00

6.0 KiB

Architecture, lint rules, state and data

Vendored Flutter/Dart AI rules. See CLAUDE.md.

Application Architecture

  • Separation of Concerns: Aim for separation of concerns similar to MVC/MVVM, with defined Model, View, and ViewModel/Controller roles.
  • Logical Layers: Organize the project into logical layers:
    • Presentation (widgets, screens)
    • Domain (business logic classes)
    • Data (model classes, API clients)
    • Core (shared classes, utilities, and extension types)
  • Feature-based Organization: For larger projects, organize code by feature, where each feature has its own presentation, domain, and data subfolders. This improves navigability and scalability.

Lint Rules

Include the package in the analysis_options.yaml file. Use the following analysis_options.yaml file as a starting point:

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    # Add additional lint rules here:
    # avoid_print: false
    # prefer_single_quotes: true

State Management

  • Built-in Solutions: Prefer Flutter's built-in state management solutions. Do not use a third-party package unless explicitly requested.

  • Streams: Use Streams and StreamBuilder for handling a sequence of asynchronous events.

  • Futures: Use Futures and FutureBuilder for handling a single asynchronous operation that will complete in the future.

  • ValueNotifier: Use ValueNotifier with ValueListenableBuilder for simple, local state that involves a single value.

    // Define a ValueNotifier to hold the state.
    final ValueNotifier<int> _counter = ValueNotifier<int>(0);
    
    // Use ValueListenableBuilder to listen and rebuild.
    ValueListenableBuilder<int>(
      valueListenable: _counter,
      builder: (context, value, child) {
        return Text('Count: $value');
      },
    );
    
  • ChangeNotifier: For state that is more complex or shared across multiple widgets, use ChangeNotifier.

  • ListenableBuilder: Use ListenableBuilder to listen to changes from a ChangeNotifier or other Listenable.

  • MVVM: When a more robust solution is needed, structure the app using the Model-View-ViewModel (MVVM) pattern.

  • Dependency Injection: Use simple manual constructor dependency injection to make a class's dependencies explicit in its API, and to manage dependencies between different layers of the application.

  • Provider: If a dependency injection solution beyond manual constructor injection is explicitly requested, provider can be used to make services, repositories, or complex state objects available to the UI layer without tight coupling (note: this document generally defaults against third-party packages for state management unless explicitly requested).

Data Flow

  • Data Structures: Define data structures (classes) to represent the data used in the application.
  • Data Abstraction: Abstract data sources (e.g., API calls, database operations) using Repositories/Services to promote testability.

Routing

  • GoRouter: Use the go_router package for declarative navigation, deep linking, and web support.

  • GoRouter Setup: To use go_router, first add it to your pubspec.yaml using the pub tool's add command.

    // 1. Add the dependency
    // flutter pub add go_router
    
    // 2. Configure the router
    final GoRouter _router = GoRouter(
      routes: <RouteBase>[
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
          routes: <RouteBase>[
            GoRoute(
              path: 'details/:id', // Route with a path parameter
              builder: (context, state) {
                final String id = state.pathParameters['id']!;
                return DetailScreen(id: id);
              },
            ),
          ],
        ),
      ],
    );
    
    // 3. Use it in your MaterialApp
    MaterialApp.router(
      routerConfig: _router,
    );
    
  • Authentication Redirects: Configure go_router's redirect property to handle authentication flows, ensuring users are redirected to the login screen when unauthorized, and back to their intended destination after successful login.

  • Navigator: Use the built-in Navigator for short-lived screens that do not need to be deep-linkable, such as dialogs or temporary views.

    // Push a new screen onto the stack
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const DetailsScreen()),
    );
    
    // Pop the current screen to go back
    Navigator.pop(context);
    

Data Handling & Serialization

  • JSON Serialization: Use json_serializable and json_annotation for parsing and encoding JSON data.

  • Field Renaming: When encoding data, use fieldRename: FieldRename.snake to convert Dart's camelCase fields to snake_case JSON keys.

    // In your model file
    import 'package:json_annotation/json_annotation.dart';
    
    part 'user.g.dart';
    
    @JsonSerializable(fieldRename: FieldRename.snake)
    class User {
      final String firstName;
      final String lastName;
    
      User({required this.firstName, required this.lastName});
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
      Map<String, dynamic> toJson() => _$UserToJson(this);
    }
    

Logging

  • Structured Logging: Use the log function from dart:developer for structured logging that integrates with Dart DevTools.

    import 'dart:developer' as developer;
    
    // For simple messages
    developer.log('User logged in successfully.');
    
    // For structured error logging
    try {
      // ... code that might fail
    } catch (e, s) {
      developer.log(
        'Failed to fetch data',
        name: 'myapp.network',
        level: 1000, // SEVERE
        error: e,
        stackTrace: s,
      );
    }