> For the complete documentation index, see [llms.txt](https://affordant.gitbook.io/chassis/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://affordant.gitbook.io/chassis/00_quick_start.md).

# Quick Start

This guide builds a complete todo list application to introduce the Chassis framework. You'll hand-write the pieces that carry decisions — the domain model, the repository, the handlers, the ViewModel, and the UI — and let `chassis_builder` generate the wiring between them: the mediator that registers every handler with the dependencies it needs. Expect to complete this tutorial in approximately 15 minutes, ending with a working application that demonstrates the core architectural patterns.

## Installation

### Adding Dependencies

Chassis consists of three core packages that work together. The `chassis` package provides pure Dart primitives for Commands, Queries, and the Mediator. The `chassis_flutter` package integrates with Flutter's widget tree through ViewModels and reactive widgets. The `chassis_builder` package generates the mediator wiring from annotations — we'll use it to produce the application's mediator.

Add these dependencies to your `pubspec.yaml`:

```yaml
dependencies:
  flutter:
    sdk: flutter
  chassis: ^1.0.0
  chassis_flutter: ^1.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  chassis_builder: ^1.0.0
  build_runner: ^2.15.0
```

No `build.yaml` is required — `chassis_builder` applies itself automatically to any package that lists it as a dev dependency.

### Installing the LLM Skills (optional)

If you code with an AI assistant, chassis ships a set of [LLM skills](https://github.com/pierremrtn/chassis/tree/main/chassis/skills/README.md) — DO/DON'T rules and workflow checklists that keep the agent on the framework's rails. After `pub get`, install them into the project with:

```bash
dart run chassis:install_skills
```

This symlinks each skill from the resolved chassis package (in your local pub cache) into `.claude/skills/`, so the skills always match the chassis version the project pins — re-run the command after upgrading chassis. Pass a target directory for other agents, or `--copy` to copy instead of symlinking.

## The Todo List Example

### Project Layout

A Chassis application is organized in four layers:

* **Domain** (`domain/`) — Entities, value objects, repository interfaces, domain errors. Depends on nothing inside the project.
* **Application** (`application/`) — Commands, Queries, Handlers. Depends on repository interfaces from domain.
* **Infrastructure** (`infrastructure/`) — Repository implementations, third-party adapters. Depends on external SDKs; wired at the composition root.
* **Presentation** (`presentation/`) — Widgets, ViewModels, State, Events. Depends on message types (application) and chassis\_flutter.

For an app this small, one folder per layer is fine; larger applications should go feature-first, each feature owning its four layers. Here is everything we'll write (plus one file the generator writes for us):

```
lib/
├── domain/
│   ├── todo.dart                       # The Todo entity
│   └── todo_repository.dart            # Repository interface
├── application/
│   └── todo_handlers.dart              # Commands, queries, and their handlers
├── infrastructure/
│   └── in_memory_todo_repository.dart  # Repository implementation
├── presentation/
│   ├── todo_view_model.dart            # State, events, ViewModel
│   └── todo_screen.dart                # UI
├── mediator.dart                       # Composition root (@ChassisApp)
├── mediator.chassis.dart               # Generated by chassis_builder
└── main.dart                           # Entry point
```

All snippets use package imports and assume the package is named `todo_app` — substitute the `name:` from your `pubspec.yaml`.

### Domain: The Model and Its Repository

In the simplest terms, a repository defines what data operations are possible without specifying how they're implemented. This abstraction enables testing and allows you to swap implementations — in-memory for development, Firebase for production, or a mock for tests — without changing business logic or UI code.

First, create the entity in `lib/domain/todo.dart`:

```dart
class Todo {
  const Todo({
    required this.id,
    required this.title,
    required this.isCompleted,
  });

  final String id;
  final String title;
  final bool isCompleted;

  Todo copyWith({
    String? id,
    String? title,
    bool? isCompleted,
  }) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }
}
```

Then create the repository interface in `lib/domain/todo_repository.dart`:

```dart
import 'package:todo_app/domain/todo.dart';

abstract interface class TodoRepository {
  Stream<List<Todo>> watchTodos();
  Future<void> addTodo(String title);
  Future<void> toggleTodo(String id);
}
```

The interface declares what operations are available (watchTodos, addTodo, toggleTodo) without specifying how they work — the implementation lives in the infrastructure layer, written below. The `Todo` model uses the `copyWith` pattern to ensure immutability — rather than modifying todos in place, we create new instances with updated values. By programming to the interface, your application can work with any implementation — swap the in-memory version for a Firebase one without changing your business logic.

### Application: Messages and Handlers

Now that the domain is defined, it's time to implement the business logic — the code that decides what happens when users interact with your application. This is where you define the actual behavior: what to do when a user adds a todo, what validation to apply before persisting, or how to transform data before presenting it to the UI.

Business logic should be independent of Flutter widgets, making it fast to test and easy to reason about. By isolating this code from UI concerns, you can verify behavior without rendering widgets, navigate complex scenarios with simple unit tests, and refactor with confidence knowing tests will catch breaking changes.

#### Commands and queries

Chassis organizes business logic using Command-Query Responsibility Segregation (CQRS), distinguishing between operations that read data (Queries) and operations that change state (Commands). This separation clarifies intent — when you see a Query, you know it's safe to call repeatedly without side effects. When you see a Command, you know state will change.

The benefits become evident as applications grow:

* **Queries** return data without side effects, making them safe to cache, retry, or call in parallel
* **Commands** represent intent to change state, making it clear where mutations occur and enabling audit logging or undo functionality

This separation allows different optimization strategies: aggressive caching for Queries, transaction handling for Commands.

See [Core Architecture](/chassis/01_core_architecture.md#command-query-separation) for deeper exploration of CQRS principles.

#### Implementing Handlers

In Chassis, business logic lives in stateless handler classes that receive messages from the Mediator and coordinate with repositories to fulfill requests. Each handler focuses on a single responsibility: receive a message, execute business logic, call repositories as needed, and return results.

Messages are pure data containers that carry intent. The `WatchTodosQuery` message says "I want to watch the todo list," while the `AddTodoCommand` says "I want to add a todo." The actual implementation lives in the corresponding handler.

> **Note:** Handlers are always written by hand — they are where your business logic lives. What Chassis generates is the wiring around them: the `@chassisHandler` annotation below marks a handler so `chassis_builder` can register it in the generated mediator. See [Code Generation](/chassis/03_code_generation.md) for everything the generator enforces.

Create `lib/application/todo_handlers.dart`:

```dart
import 'package:chassis/chassis.dart';

import 'package:todo_app/domain/todo.dart';
import 'package:todo_app/domain/todo_repository.dart';

// Query to reactively watch the todo list
final class WatchTodosQuery extends WatchQuery<List<Todo>> {}

@chassisHandler // Marks this handler for wiring by chassis_builder
class WatchTodosHandler implements WatchHandler<WatchTodosQuery, List<Todo>> {
  WatchTodosHandler({required this.repository});

  final TodoRepository repository;

  @override
  Stream<List<Todo>> watch(WatchTodosQuery query) => repository.watchTodos();
}

// Command to add a new todo
final class AddTodoCommand extends Command<void> {
  AddTodoCommand({required this.title});

  final String title;

  @override
  Map<String, Object?> get params => {'title': title};
}

@chassisHandler
class AddTodoHandler implements CommandHandler<AddTodoCommand, void> {
  AddTodoHandler({required this.repository});

  final TodoRepository repository;

  @override
  Future<void> run(AddTodoCommand command) => repository.addTodo(command.title);
}

// Command to toggle a todo's completion status
final class ToggleTodoCommand extends Command<void> {
  ToggleTodoCommand({required this.id});

  final String id;

  @override
  Map<String, Object?> get params => {'id': id};
}

@chassisHandler
class ToggleTodoHandler implements CommandHandler<ToggleTodoCommand, void> {
  ToggleTodoHandler({required this.repository});

  final TodoRepository repository;

  @override
  Future<void> run(ToggleTodoCommand command) => repository.toggleTodo(command.id);
}
```

Notice the dependency injection pattern — each handler receives its repository through the constructor, typed as the `TodoRepository` interface from the domain layer, never as a concrete implementation. This ensures testability and loose coupling, enabling testing handlers in isolation. The commands carry the data they need — `AddTodoCommand` has a title, and `ToggleTodoCommand` has an id to identify which todo to toggle. The `params` override gives each message a loggable identity: `LoggingMiddleware` prints it, and two messages of the same type with equal params represent the same operation (never put secrets in `params`).

While this todo example shows simple pass-through handlers, real applications contain validation, transformation, and coordination logic here. You might validate that the title isn't empty before persisting, combine data from multiple repositories, or apply business rules before returning results. This is where business complexity lives — not scattered across widgets, but concentrated in testable, framework-independent handlers.

The handler's logic is pure business code with no Flutter dependencies, making it fast and easy to test. See [Business Logic](/chassis/02_business_logic.md#unit-testing-handlers) for detailed testing strategies and examples of more complex handler implementations.

### Infrastructure: An In-Memory Repository

The infrastructure layer provides the concrete implementations of the domain's interfaces. For this tutorial an in-memory implementation is enough — this is also the layer where a Firebase or REST adapter would live.

Create `lib/infrastructure/in_memory_todo_repository.dart`:

```dart
import 'dart:async';

import 'package:todo_app/domain/todo.dart';
import 'package:todo_app/domain/todo_repository.dart';

class InMemoryTodoRepository implements TodoRepository {
  final _controller = StreamController<List<Todo>>.broadcast();
  final List<Todo> _todos = [];
  int _nextId = 0;

  @override
  Stream<List<Todo>> watchTodos() async* {
    // A broadcast stream drops emissions made before a listener subscribes,
    // so each subscriber first gets a snapshot of the current list, then the
    // live updates.
    yield List.unmodifiable(_todos);
    yield* _controller.stream;
  }

  @override
  Future<void> addTodo(String title) async {
    final todo = Todo(
      id: (_nextId++).toString(),
      title: title,
      isCompleted: false,
    );
    _todos.add(todo);
    _controller.add(List.unmodifiable(_todos));
  }

  @override
  Future<void> toggleTodo(String id) async {
    final index = _todos.indexWhere((t) => t.id == id);
    if (index != -1) {
      _todos[index] = _todos[index].copyWith(
        isCompleted: !_todos[index].isCompleted,
      );
      _controller.add(List.unmodifiable(_todos));
    }
  }

  void dispose() {
    _controller.close();
  }
}
```

The implementation uses a `StreamController` to broadcast todo list changes reactively; because a broadcast stream has no memory, `watchTodos()` is an `async*` generator that yields the current list before forwarding live updates — every subscriber gets an immediate value, no matter when it subscribes.

### Generating the Mediator

Your handlers now exist, but nothing routes messages to them yet — this is the Mediator's job. It is the single dispatch point of the application: every message goes through it, so middleware (logging, caching, crash reporting) applies everywhere, and it wires handlers to their dependencies at startup. Crucially, the rest of the application never talks to a concrete mediator class — ViewModels dispatch message objects, and the installed mediator routes them.

The concrete mediator — handler registrations plus dependency wiring — is pure transcription, so Chassis generates it. Annotate a library with `@ChassisApp` on its library directive — this library is the composition root of the message graph. We'll use a dedicated `lib/mediator.dart`, so everything mediator-related lives in one file:

```dart
// lib/mediator.dart
@ChassisApp(mediatorName: 'AppMediator')
library;

import 'package:chassis/chassis.dart';

// The generator collects every @chassisHandler reachable from this
// library's imports — this import is what makes the handlers visible.
import 'package:todo_app/application/todo_handlers.dart';

// Re-export the generated mediator so main.dart imports only this file.
export 'package:todo_app/mediator.chassis.dart';
```

Then run the generator:

```bash
dart run build_runner build --delete-conflicting-outputs
```

(`--delete-conflicting-outputs` lets the build overwrite stale generated files without asking; it's safe to pass always. Until the first run, the export of `mediator.chassis.dart` shows an analyzer error — the file doesn't exist yet.)

The generator emits `lib/mediator.chassis.dart` next to the annotated library, containing the concrete mediator:

```dart
// mediator.chassis.dart (generated — never edit by hand)
import 'package:chassis/chassis.dart' as _i1;
import 'package:todo_app/domain/todo_repository.dart' as _i2;
import 'package:todo_app/application/todo_handlers.dart' as _i3;

class AppMediator extends _i1.Mediator {
  AppMediator({required _i2.TodoRepository todoRepository}) {
    registerQueryHandler(_i3.WatchTodosHandler(repository: todoRepository));
    registerCommandHandler(_i3.AddTodoHandler(repository: todoRepository));
    registerCommandHandler(_i3.ToggleTodoHandler(repository: todoRepository));
  }
}
```

The constructor is the entire generated API — and it doubles as the application's dependency manifest: the generator collects the dependencies of all handlers, deduplicates them, and requires each one as a named parameter. There are no per-message methods; dispatch happens with the message objects themselves, through the `run`/`read`/`watch` the class inherits from `Mediator`, so middleware always applies. The generator also enforces completeness: a concrete Command or Query reachable from the `@ChassisApp` import graph with no handler fails the build — annotate the message `@unhandledMessage` to opt out while its handler is still being written.

Wiring mistakes — a missing dependency, a message without a handler, two handlers for the same message — surface at build time, not at runtime. See [Code Generation](/chassis/03_code_generation.md) for the full guarantees and the module system that shares handlers across applications.

### Presentation: The ViewModel

The ViewModel transforms domain data into UI-ready state and handles user interactions by dispatching messages. It sits between the message layer and the widget tree, translating business operations into state changes that widgets can observe. Note what it does *not* know: it imports the message types from the application layer and nothing mediator-related — no generated class, no global.

Create `lib/presentation/todo_view_model.dart`:

```dart
import 'package:chassis_flutter/chassis_flutter.dart';

import 'package:todo_app/application/todo_handlers.dart';
import 'package:todo_app/domain/todo.dart';

class TodoState {
  const TodoState({required this.todos});

  // Async<T> represents an asynchronous value as loading / data / error,
  // so the UI always knows which of the three states to render.
  final Async<List<Todo>> todos;

  TodoState copyWith({Async<List<Todo>>? todos}) {
    return TodoState(todos: todos ?? this.todos);
  }

  static TodoState initial() => const TodoState(todos: Async.loading());
}

sealed class TodoEvent {}

final class TodoAdded implements TodoEvent {
  const TodoAdded();
}

final class TodoOpFailed implements TodoEvent {
  const TodoOpFailed(this.error);

  // The error OBJECT, never error.toString(): listeners can still
  // pattern-match on the error type to choose their reaction.
  final Object error;
}

class TodoViewModel extends ViewModel<TodoState, TodoEvent> {
  TodoViewModel({super.mediator}) : super(TodoState.initial()) {
    // Start watching the todo list immediately.
    watch(
      WatchTodosQuery(),
      onState: (todos) => setState(state.copyWith(todos: todos)),
    );
  }

  void addTodo(String title) => run(
        AddTodoCommand(title: title),
        onSuccess: (_) => sendEvent(const TodoAdded()),
        onError: (error, stack) => sendEvent(TodoOpFailed(error)),
      );

  void toggleTodo(String id) => run(
        ToggleTodoCommand(id: id),
        onError: (error, stack) => sendEvent(TodoOpFailed(error)),
      );
}
```

The ViewModel demonstrates Chassis's complete data flow cycle. The `watch()` call in the constructor dispatches `WatchTodosQuery` and establishes a subscription: when the repository emits a new list, the ViewModel receives it wrapped in `Async<T>` and updates its state, and the UI rebuilds. The command methods are synchronous and expression-bodied — they hand a message to `run` and describe what each outcome means for the UI; all awaiting happens inside the dispatch machinery.

Who routes the messages? The mediator installed by `Chassis.initialize` in `main.dart` (written in the last section). The `{super.mediator}` constructor parameter is the testing seam: a test constructs `TodoViewModel(mediator: fakeMediator)` and the override wins over the global — production code just calls `TodoViewModel()`.

Both `run()` and `watch()` follow the same callback contract:

* `onState` (if provided) fires for **every** transition — loading, data, and error — with the corresponding `Async<T>` value.
* `onSuccess` (on `run`) / `onData` (on `watch`) and `onError` are **additive** conveniences, invoked *after* `onState` for their respective transition. Providing them never suppresses `onState`.
* **Always cover the error path**: every `run` must provide `onState` or `onError`. `onSuccess` alone is the invisible-failure anti-pattern — the command fails and the user never learns why. That's why `addTodo` pairs its success event with `onError`: the todo list itself arrives through the `watch` subscription, so the command needs no `onState`, but its failure must surface — here as a `TodoOpFailed` event carrying the error object.
* Passing `current:` (the current `Async<T>` state) makes the loading and error emissions carry the existing data, so a refetch never blanks the UI — see [UI Integration](/chassis/04_ui_integration.md#anti-flickering-with-maintainstate).
* Operations are keyed, and the key defaults to the message's runtime type. For `watch`, a re-dispatch of the same query class **replaces** the previous subscription — the canonical way to re-watch with new arguments. For example, if the app later grows a filtered query:

```dart
void selectFilter(TodoFilter filter) => watch(
      // Same query class = same default key: this cancels and replaces the
      // previous WatchTodosQuery subscription instead of adding a second one.
      WatchTodosQuery(filter: filter),
      current: state.todos, // Loading/error emissions keep the current list
      onState: (todos) => setState(state.copyWith(todos: todos)),
    );
```

When a user adds a todo, the flow is:

1. UI calls `context.read<TodoViewModel>().addTodo(title)`
2. The ViewModel dispatches `AddTodoCommand(title: title)` through `run`
3. The Mediator routes the message to `AddTodoHandler`
4. The handler calls `repository.addTodo(title)`
5. The repository emits the new todo list through its stream
6. The ViewModel's `watch` callback receives the update and calls `setState`
7. UI rebuilds with the new todo list

State immutability ensures predictable behavior — the `copyWith` pattern creates new state objects rather than mutating existing ones. The `Async<List<Todo>>` wrapper makes loading, data, and error states explicit — and because it's sealed, the UI can pattern-match on it exhaustively. Events provide a channel for one-time occurrences like clearing the input field or showing a snackbar, separate from persistent state.

### Presentation: The Screen

The UI layer observes state changes and dispatches user interactions to the ViewModel. Following Flutter best practices, the screen is split into small, focused widget classes rather than one large build method: `TodoScreen` owns the `Scaffold` and provides the ViewModel, `_TodoComposer` owns the text input, `_TodoList` renders the async list, and `_TodoTile` renders a single row.

`TodoScreen` injects the ViewModel with `ViewModelProvider.withEventListener`, placed just below the `Scaffold`. This ties the ViewModel's lifecycle to the screen, and co-locates event side-effects — the snackbar notifications — with the screen they concern. Note that `TodoViewModel()` is constructed with no arguments: it resolves the application mediator on its own.

Create `lib/presentation/todo_screen.dart`:

```dart
import 'package:chassis_flutter/chassis_flutter.dart';
import 'package:flutter/material.dart';

import 'package:todo_app/domain/todo.dart';
import 'package:todo_app/presentation/todo_view_model.dart';

class TodoScreen extends StatelessWidget {
  const TodoScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Todo List')),
      // The provider sits below the Scaffold: the ViewModel lives exactly as
      // long as this screen, and event side-effects stay next to the UI they
      // affect instead of leaking into main.dart.
      body: ViewModelProvider.withEventListener<TodoViewModel, TodoEvent>(
        create: (_) => TodoViewModel(),
        onEvent: (context, viewModel, event) {
          switch (event) {
            case TodoAdded():
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Todo added')),
              );
            case TodoOpFailed():
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Something went wrong')),
              );
          }
        },
        child: const Column(
          children: [
            _TodoComposer(),
            Expanded(child: _TodoList()),
          ],
        ),
      ),
    );
  }
}

// Stateful only because it owns the TextEditingController. It never watches
// the ViewModel, so it doesn't rebuild when the todo list changes.
class _TodoComposer extends StatefulWidget {
  const _TodoComposer();

  @override
  State<_TodoComposer> createState() => _TodoComposerState();
}

class _TodoComposerState extends State<_TodoComposer> {
  final _textController = TextEditingController();

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  void _submit() {
    final title = _textController.text.trim();
    if (title.isEmpty) return;
    context.read<TodoViewModel>().addTodo(title);
    _textController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _textController,
              decoration: const InputDecoration(
                hintText: 'Enter todo title',
                border: OutlineInputBorder(),
              ),
              onSubmitted: (_) => _submit(),
            ),
          ),
          const SizedBox(width: 8),
          ElevatedButton(
            onPressed: _submit,
            child: const Text('Add'),
          ),
        ],
      ),
    );
  }
}

class _TodoList extends StatelessWidget {
  const _TodoList();

  @override
  Widget build(BuildContext context) {
    // select subscribes this widget to just the field it renders; when the
    // todo list changes, only _TodoList rebuilds — not the whole screen.
    final asyncTodos = context.select(
      (TodoViewModel vm) => vm.state.todos,
    );

    // Async<T> is sealed, so a switch expression covers loading, error, and
    // data exhaustively — the compiler rejects a missing case.
    return switch (asyncTodos) {
      AsyncLoading() => const Center(child: CircularProgressIndicator()),
      AsyncError(:final error) => Center(child: Text('Error: $error')),
      AsyncData(value: final todos) when todos.isEmpty => const Center(
          child: Text('No todos yet. Add one above!'),
        ),
      AsyncData(value: final todos) => ListView.builder(
          itemCount: todos.length,
          itemBuilder: (context, index) => _TodoTile(todo: todos[index]),
        ),
    };
  }
}

class _TodoTile extends StatelessWidget {
  const _TodoTile({required this.todo});

  final Todo todo;

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Checkbox(
        value: todo.isCompleted,
        // Callbacks use context.read to call methods without subscribing.
        onChanged: (_) => context.read<TodoViewModel>().toggleTodo(todo.id),
      ),
      title: Text(
        todo.title,
        style: TextStyle(
          decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
        ),
      ),
    );
  }
}
```

Because `Async<T>` is a sealed class, a switch expression over it is checked for exhaustiveness: the compiler forces `_TodoList` to handle `AsyncLoading`, `AsyncError`, and `AsyncData`, and pattern destructuring (`value: final todos`) extracts the payload in the same line. This inline switch is the preferred style for simple rendering like this. When you need more — keeping the previous list on screen during a refetch, for instance — reach for the `AsyncBuilder` widget and its `maintainState` support instead (see [UI Integration](/chassis/04_ui_integration.md#anti-flickering-with-maintainstate)). Note the two access patterns: `context.select` subscribes a widget to exactly the field it renders, while callbacks use `context.read` to call ViewModel methods without subscribing.

Splitting the screen into private widget classes pays off in rebuild scope: `_TodoComposer` never rebuilds when todos change, and `_TodoList` is the only widget subscribed to `state.todos`. The `onEvent` callback runs with the provider's own context — below the `Scaffold`, so `ScaffoldMessenger.of(context)` resolves naturally — keeping the notification logic in the same file as the screen it belongs to. The snackbar text here is generic, but because `TodoOpFailed` carries the error object, a real app can pattern-match on it (`case TodoOpFailed(error: TitleTooLongError())`) to pick the right message.

### Putting It All Together

The composition root wires the dependency tree from the bottom up: it constructs the infrastructure (repositories have no dependencies), passes it to the generated mediator constructor, and installs that mediator with `Chassis.initialize` before `runApp`. From then on, every ViewModel resolves it lazily at its first dispatch — there is no global mediator variable to declare, and no mediator to thread through the widget tree.

`lib/main.dart` is the entry point and the only file in the application that ever names `AppMediator`:

```dart
import 'package:chassis_flutter/chassis_flutter.dart';
import 'package:flutter/material.dart';

import 'package:todo_app/infrastructure/in_memory_todo_repository.dart';
import 'package:todo_app/mediator.dart';
import 'package:todo_app/presentation/todo_screen.dart';

void main() {
  // The generated constructor is the dependency manifest: it requires
  // exactly the repositories the handlers need.
  Chassis.initialize(
    AppMediator(todoRepository: InMemoryTodoRepository())
      // Traces every dispatch with its params, outcome, and duration.
      ..addMiddleware(LoggingMiddleware()),
  );

  runApp(
    const MaterialApp(
      title: 'Todo List',
      home: TodoScreen(),
    ),
  );
}
```

Notice how narrow each file's knowledge is: `lib/mediator.dart` declares *what exists* (the `@ChassisApp` graph of handlers), `main.dart` decides *what's real* (which repository implementation backs them), and the presentation layer knows neither — it imports message types and nothing else. The import graph stays strictly one-way: `main.dart` → screens → ViewModels → messages → domain, with infrastructure attached only at the root.

Run the app:

```bash
flutter run
```

You should see the empty state — "No todos yet. Add one above!" — immediately, with no spinner: the repository yields its (empty) snapshot as soon as the ViewModel subscribes. Type a title, press Add, and the todo appears in the list with a "Todo added" snackbar; tapping the checkbox strikes it through. If the analyzer complains about `mediator.chassis.dart` instead, re-run the generator from the [Generating the Mediator](#generating-the-mediator) section.

## What You Just Built

You've created a complete Chassis application with clear separation of concerns. The architecture flows naturally through the four layers:

* **Domain**: The `Todo` entity and the `TodoRepository` interface — pure Dart, depending on nothing
* **Application**: Messages that name every operation, and handlers that implement them against the repository interface
* **Infrastructure**: `InMemoryTodoRepository`, the concrete implementation chosen at the composition root
* **Presentation**: A ViewModel that dispatches messages and models UI state as `Async<T>`, and widgets that render it

Between application and presentation sits the generated mediator — `chassis_builder` derives its registration constructor from the `@chassisHandler` annotations, and `Chassis.initialize` installs it once at startup.

The key benefits of this architecture:

* **Testability**: Each layer can be tested in isolation — handlers with a fake repository, ViewModels with a fake mediator (`TodoViewModel(mediator: fakeMediator)`)
* **Discoverability**: The messages in `lib/application/todo_handlers.dart` — `WatchTodosQuery`, `AddTodoCommand`, `ToggleTodoCommand` — are an explicit catalog of everything the application can do
* **Maintainability**: Business logic lives in handlers, not spread across widgets
* **Scalability**: Adding features follows the same pattern, maintaining consistency

## Next Steps

Notice how little wiring you maintain: the `@chassisHandler` annotations and a single `@ChassisApp` declaration. Everything between them — handler registration and dependency injection — is derived by `chassis_builder`, and every wiring mistake it can detect, down to a message with no handler, fails the build instead of surfacing at runtime.

For deeper understanding of the architectural principles guiding these patterns, explore [Core Architecture](/chassis/01_core_architecture.md). To learn testing strategies for handlers, see [Business Logic](/chassis/02_business_logic.md). To go further with the generator — build-time guarantees, and the `@chassisModule` system that shares feature packages across applications — see [Code Generation](/chassis/03_code_generation.md). To learn advanced UI patterns like anti-flickering, concurrency policies, and event handling, proceed to [UI Integration](/chassis/04_ui_integration.md).
