diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..7b844e71 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "mobx-dart", + "owner": { + "name": "MobX.dart Contributors" + }, + "metadata": { + "description": "Claude Code plugins for MobX.dart — reactive state management for Dart and Flutter", + "version": "1.0.0", + "pluginRoot": "." + }, + "plugins": [ + { + "name": "mobx-dart", + "version": "2.6.0", + "description": "MobX.dart agent skills — observables, actions, reactions, Observer widget, store patterns, and code generation", + "source": "." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 00000000..f645096c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "mobx-dart", + "version": "2.6.0", + "description": "MobX.dart agent skills for Claude Code — reactive state management with observables, actions, reactions, and Flutter integration", + "author": { + "name": "MobX.dart Contributors", + "url": "https://github.com/mobxjs/mobx.dart" + }, + "homepage": "https://mobx.netlify.app", + "repository": "https://github.com/mobxjs/mobx.dart", + "license": "MIT", + "keywords": ["dart", "flutter", "mobx", "state-management", "reactive"] +} diff --git a/README.md b/README.md index 1ae605d7..75c44b46 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,47 @@ class _CounterExampleState extends State { } ``` +## Working with AI + +This repository includes [Agent Skills](https://agentskills.io) that give AI coding assistants deep knowledge of MobX.dart APIs, patterns, and best practices. Skills work with Claude Code, Cursor, Windsurf, and [35+ other agents](https://skills.sh). + +### Installation + +```bash +npx skills add mobxjs/mobx.dart +``` + +Target a specific agent with `--agent`: + +```bash +npx skills add mobxjs/mobx.dart --agent cursor +npx skills add mobxjs/mobx.dart --agent claude-code +``` + +Or install globally so the skill is available across all your projects: + +```bash +npx skills add mobxjs/mobx.dart --global +``` + +For Claude Code, you can also install via the plugin system: + +``` +/plugin marketplace add mobxjs/mobx.dart +/plugin install mobx-dart@mobx-dart +``` + +### What's Included + +The `mobx-dart` skill covers: + +- **Core APIs** — Store class pattern, `@observable`, `@computed`, `@action`, `@readonly` annotations, code generation with `mobx_codegen` +- **Reactions** — `autorun`, `reaction`, `when`, `asyncWhen`, custom schedulers +- **Flutter Integration** — `Observer` widget, `Observer.withBuiltChild`, `ReactionBuilder` +- **Reactive Collections** — `ObservableList`, `ObservableMap`, `ObservableSet`, `ObservableFuture`, `ObservableStream`, `Atom` +- **Best Practices** — Widget-Store-Service triad, store organization, reactivity rules, JSON serialization +- **Advanced** — `ReactiveContext`, `ReactiveConfig`, read/write policies, Spy debugging + ## Contributing If you have read up till here, then 🎉🎉🎉. There are couple of ways in which you can contribute to diff --git a/docs/docs/guides/working-with-ai.mdx b/docs/docs/guides/working-with-ai.mdx new file mode 100644 index 00000000..0e7207d4 --- /dev/null +++ b/docs/docs/guides/working-with-ai.mdx @@ -0,0 +1,117 @@ +--- +slug: /guides/working-with-ai +title: Working with AI +--- + +import { PubBadge } from '../../src/components/Shield'; + +AI coding assistants like Claude Code, GitHub Copilot, and Cursor can be powerful +allies when building MobX.dart applications. This guide covers tips for getting +the most out of AI tools with MobX. + +## Tips for Prompting AI + +### Be Explicit About the MobX Triad + +When asking AI to create stores, mention the specific MobX concepts you want: + +- **"Create a store with `@observable` fields, `@computed` getters, and `@action` methods"** +- **"Add a `reaction` that validates the email field when it changes"** +- **"Wrap the counter display in an `Observer` widget"** + +AI tools work best when you use MobX-specific terminology rather than generic +descriptions. + +### Remind AI About Code Generation + +MobX.dart relies on `mobx_codegen`. When asking AI to create a store, remind it +about the boilerplate: + +```dart +// Include the part directive and class structure +import 'package:mobx/mobx.dart'; + +part 'counter.g.dart'; + +class Counter = _Counter with _$Counter; + +abstract class _Counter with Store { + // ... store body +} +``` + +### Common Pitfalls to Watch For + +When reviewing AI-generated MobX code, watch for these issues: + +1. **Missing `part` directive** — AI may forget `part 'filename.g.dart';` +2. **Deep observability assumption** — AI may assume nested objects are automatically + tracked. Dart MobX does **not** support deep observability. +3. **Observer tracking scope** — AI may place observable reads inside nested + functions within `Observer.builder`, where they won't be tracked. +4. **Plain collections** — AI may use `List` instead of `ObservableList` + when item-level tracking is needed. + +## Agent Skills + +This repository includes [Agent Skills](https://agentskills.io) that give AI +coding assistants deep knowledge of MobX.dart APIs, patterns, and best practices. +Skills work with Claude Code, Cursor, Windsurf, and other AI tools that support them. + +### Installation + +```bash +npx skills add mobxjs/mobx.dart +``` + +Target a specific agent with `--agent`: + +```bash +npx skills add mobxjs/mobx.dart --agent cursor +npx skills add mobxjs/mobx.dart --agent claude-code +``` + +Or install globally so the skill is available across all your projects: + +```bash +npx skills add mobxjs/mobx.dart --global +``` + +For Claude Code, you can also install via the plugin system: + +``` +/plugin marketplace add mobxjs/mobx.dart +/plugin install mobx-dart@mobx-dart +``` + +### What's Included + +The `mobx-dart` skill covers: + +- **Core APIs** — Store class pattern, `@observable`, `@computed`, `@action`, `@readonly`, + code generation with +- **Reactions** — `autorun`, `reaction`, `when`, `asyncWhen`, custom schedulers +- **Flutter Integration** — `Observer` widget, `Observer.withBuiltChild`, `ReactionBuilder` +- **Reactive Collections** — `ObservableList`, `ObservableMap`, `ObservableSet`, + `ObservableFuture`, `ObservableStream`, `Atom` +- **Best Practices** — Widget-Store-Service triad, store organization, reactivity rules, + JSON serialization +- **Advanced** — `ReactiveContext`, `ReactiveConfig`, read/write policies, Spy debugging + +## Custom Instructions for AI Tools + +If your AI tool supports custom instructions (e.g., `.cursorrules`, `CLAUDE.md`, +`.github/copilot-instructions.md`), consider adding MobX-specific guidance: + +```markdown +## MobX.dart Conventions + +- Use `mobx_codegen` annotations (`@observable`, `@computed`, `@action`) +- Always include `part 'filename.g.dart';` in store files +- Use `ObservableList`/`ObservableMap`/`ObservableSet` for reactive collections +- Wrap UI in `Observer` widget and read observables in immediate builder context +- Follow Widget-Store-Service triad for architecture +- Do not mutate observables outside of actions +``` + +This ensures consistent, correct MobX code regardless of which AI tool you use. diff --git a/docs/sidebars.js b/docs/sidebars.js index 9f4df51d..c8ea191a 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -29,6 +29,7 @@ module.exports = { 'guides/when-does-mobx-react', 'guides/mobx-vs-inherited-model', 'guides/mobx-utils', + 'guides/working-with-ai', ], }, 'community', diff --git a/skills/mobx-dart/GENERATION.md b/skills/mobx-dart/GENERATION.md new file mode 100644 index 00000000..a4252f31 --- /dev/null +++ b/skills/mobx-dart/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/mobx.dart` +- **Git SHA:** `39a1f18c2e76a564b5e5358426d335569bc332ec` +- **Generated:** 2026-03-30 diff --git a/skills/mobx-dart/SKILL.md b/skills/mobx-dart/SKILL.md new file mode 100644 index 00000000..3e270646 --- /dev/null +++ b/skills/mobx-dart/SKILL.md @@ -0,0 +1,110 @@ +--- +name: mobx-dart +description: >- + This skill should be used when working with MobX.dart for Dart/Flutter state management, + when the user asks to "create a store", "add observable", "add action", "add computed", + "use Observer widget", "set up reactions", "organize stores", + or when code imports `package:mobx/mobx.dart`, `package:flutter_mobx/flutter_mobx.dart`, + or `package:mobx_codegen/mobx_codegen.dart`. + Covers observables, actions, reactions, Observer widget, store patterns, + code generation with mobx_codegen, and reactive collections. +--- + +# MobX.dart + +MobX.dart is a reactive state management library for Dart and Flutter built around three core concepts: **Observables** (reactive state), **Actions** (state mutations), and **Reactions** (side-effects). It uses `mobx_codegen` for annotation-based code generation to minimize boilerplate. + +## Packages + +| Package | Purpose | +|---------|---------| +| `mobx` | Core library: Observables, Actions, Reactions | +| `flutter_mobx` | Flutter integration: Observer widget, ReactionBuilder | +| `mobx_codegen` | Code generation: `@observable`, `@computed`, `@action` annotations | + +## Store Declaration Pattern + +Every store follows this boilerplate: + +```dart +import 'package:mobx/mobx.dart'; + +part 'counter.g.dart'; + +class Counter = _Counter with _$Counter; + +abstract class _Counter with Store { + @observable + int value = 0; + + @computed + bool get isPositive => value > 0; + + @action + void increment() { + value++; + } +} +``` + +Run code generation with: +```bash +flutter pub run build_runner watch --delete-conflicting-outputs +``` + +## Key Annotations + +| Annotation | Target | Purpose | +|---|---|---| +| `@observable` | Field | Make field reactive | +| `@readonly` | Private field | Observable with auto-generated public getter; mutations only via `@action` | +| `@computed` | Getter | Derived state that auto-updates when dependencies change | +| `@action` | Method | Wrap mutations in a transaction; supports async | +| `@MakeObservable()` | Field | Advanced config (e.g., `useDeepEquals: true`) | + +## Observer Widget + +Wrap reactive UI in `Observer` from `flutter_mobx`: + +```dart +Observer(builder: (_) => Text('${counter.value}')) +``` + +**Critical**: Only observables read in the **immediate execution context** of the builder are tracked. Observables read inside nested functions or callbacks are NOT tracked. + +## Reactive Collections + +Use `ObservableList`, `ObservableMap`, `ObservableSet` instead of plain Dart collections for item-level tracking. Use `ObservableFuture` and `ObservableStream` for async state. + +## Reference Files + +For detailed API documentation, patterns, and best practices, consult: + +### Core + +- **[`references/core-store-and-codegen.md`](references/core-store-and-codegen.md)** — Store class pattern, annotations, build_runner, generated output +- **[`references/core-observables.md`](references/core-observables.md)** — Observable, Computed, @observable, @readonly, @computed, reactive extensions +- **[`references/core-actions.md`](references/core-actions.md)** — @action, runInAction, untracked, transaction, async actions +- **[`references/core-reactions.md`](references/core-reactions.md)** — autorun, reaction, when, asyncWhen, custom schedulers + +### Flutter Integration + +- **[`references/features-observer-widget.md`](references/features-observer-widget.md)** — Observer, Observer.withBuiltChild, ReactionBuilder +- **[`references/features-reactive-collections.md`](references/features-reactive-collections.md)** — ObservableList/Map/Set/Future/Stream, Atom + +### Best Practices + +- **[`references/best-practices-store-organization.md`](references/best-practices-store-organization.md)** — Widget-Store-Service triad, store hierarchy, Provider integration +- **[`references/best-practices-reactivity-rules.md`](references/best-practices-reactivity-rules.md)** — When MobX reacts, tracking pitfalls, form validation +- **[`references/best-practices-json-serialization.md`](references/best-practices-json-serialization.md)** — json_serializable integration, custom converters + +### Advanced + +- **[`references/advanced-context-and-config.md`](references/advanced-context-and-config.md)** — ReactiveContext, ReactiveConfig, read/write policies +- **[`references/advanced-spy.md`](references/advanced-spy.md)** — Spy API for tracing and debugging reactive events + +## Important Notes + +- **No deep observability**: Unlike JS MobX, marking a complex object as `@observable` only tracks reference reassignment, not internal field changes. Mark individual fields with `@observable`. +- **Action enforcement**: By default, mutating an observed observable outside an action throws. Single-property setters in codegen stores are auto-wrapped. +- **Computed caching**: `.value` always re-evaluates, but notifications only fire when the result differs from the previous value. diff --git a/skills/mobx-dart/references/advanced-context-and-config.md b/skills/mobx-dart/references/advanced-context-and-config.md new file mode 100644 index 00000000..6d1f54d3 --- /dev/null +++ b/skills/mobx-dart/references/advanced-context-and-config.md @@ -0,0 +1,72 @@ +--- +name: advanced-context-and-config +description: ReactiveContext and ReactiveConfig for custom MobX contexts, read/write policies, and error boundaries +--- + +# ReactiveContext and Configuration + +MobX operates within a `ReactiveContext` that manages observables and reactions. By default, the singleton `mainContext` is used. Custom contexts are an advanced feature for isolating reactive systems. + +## ReactiveContext + +Create a custom context for isolated reactivity (e.g., a library using MobX internally that shouldn't share context with the host app): + +```dart +final myContext = ReactiveContext(config: ReactiveConfig( + writePolicy: ReactiveWritePolicy.always, +)); + +final counter = Observable(0, context: myContext); +``` + +## ReactiveConfig + +```dart +ReactiveConfig({ + bool disableErrorBoundaries = false, + ReactiveWritePolicy writePolicy = ReactiveWritePolicy.observed, + ReactiveReadPolicy readPolicy = ReactiveReadPolicy.never, + int maxIterations = 100, +}) +``` + +### Write Policy + +Controls enforcement of mutations inside actions: + +| Policy | Behavior | +|---|---| +| `observed` (default) | Throws only if the mutated observable is currently being observed | +| `always` | Always requires mutations inside an action | +| `never` | No enforcement (discouraged) | + +### Read Policy + +Controls enforcement of reading observables inside reactive contexts: + +| Policy | Behavior | +|---|---| +| `never` (default) | Reads allowed anywhere | +| `always` | Reads must happen inside an Action or Reaction | + +### Error Boundaries + +`disableErrorBoundaries: true` makes MobX not catch exceptions in reactions (useful for debugging). Default is `false` — MobX catches and logs unhandled exceptions. + +### Max Iterations + +`maxIterations` (default: 100) limits reaction cycles. If reactions keep triggering more reactions beyond this limit, MobX throws to prevent infinite loops from cyclical dependencies. + +## Modifying mainContext + +```dart +mainContext.config = mainContext.config.clone( + writePolicy: ReactiveWritePolicy.always, + isSpyEnabled: true, +); +``` + + diff --git a/skills/mobx-dart/references/advanced-spy.md b/skills/mobx-dart/references/advanced-spy.md new file mode 100644 index 00000000..56751dec --- /dev/null +++ b/skills/mobx-dart/references/advanced-spy.md @@ -0,0 +1,68 @@ +--- +name: advanced-spy +description: MobX Spy API for debugging, tracing reactive events, and inspecting observable/action/reaction activity +--- + +# Spy (Debugging) + +Spy provides visibility into MobX internals by emitting events for all reactive activity. + +## Setup + +Enable spying and register a listener: + +```dart +import 'package:mobx/mobx.dart'; + +void main() { + mainContext.config = mainContext.config.clone( + isSpyEnabled: true, // must enable first + ); + + mainContext.spy(print); // simple logging + // or: mainContext.spy((event) { /* custom handling */ }); + + runApp(MyApp()); +} +``` + +`spy()` returns a `Dispose` function to stop listening. + +## SpyEvent Types + +| Event Class | What it captures | +|---|---| +| `ObservableValueSpyEvent` | Observable value changes (old/new value) | +| `ComputedValueSpyEvent` | Computed re-evaluations | +| `ReactionSpyEvent` | Reaction execution start | +| `ReactionErrorSpyEvent` | Errors inside reactions | +| `ReactionDisposedSpyEvent` | Reaction disposal | +| `ActionSpyEvent` | Action execution start | +| `EndedSpyEvent` | End of action/reaction/observable event | + +## Example Output + +For a counter increment: + +``` +action(START) _Counter.increment +observable(START) _Counter.value=1, previously=0 +observable(END) _Counter.value +action(END after 1ms) _Counter.increment +reaction(START) Observer +reaction(END after 0ms) Observer +``` + +This trace shows: action fired -> observable updated -> Observer widget rebuilt. + +## Use Cases + +- Debugging why a reaction isn't firing +- Tracing the chain of events leading to a state change +- Integrating with external logging/monitoring tools +- Understanding execution order of nested actions and reactions + + diff --git a/skills/mobx-dart/references/best-practices-json-serialization.md b/skills/mobx-dart/references/best-practices-json-serialization.md new file mode 100644 index 00000000..091c5fa1 --- /dev/null +++ b/skills/mobx-dart/references/best-practices-json-serialization.md @@ -0,0 +1,97 @@ +--- +name: best-practices-json-serialization +description: JSON serialization of MobX stores using json_serializable with custom converters for ObservableList +--- + +# JSON Serialization of Stores + +MobX stores can be serialized to/from JSON using `json_serializable`. The code generators coexist — both `mobx_codegen` and `json_serializable` write to the same `*.g.dart` file. + +## Setup + +```yaml +dependencies: + json_annotation: ^4.0.0 + +dev_dependencies: + json_serializable: ^6.0.0 +``` + +## Annotate the Store + +Add `@JsonSerializable()` to the **public** class (not the abstract base): + +```dart +import 'package:json_annotation/json_annotation.dart'; +import 'package:mobx/mobx.dart'; + +part 'todo.g.dart'; + +@JsonSerializable() +class Todo extends _Todo with _$Todo { + Todo(String description) : super(description); + + factory Todo.fromJson(Map json) => _$TodoFromJson(json); + Map toJson() => _$TodoToJson(this); +} + +abstract class _Todo with Store { + _Todo(this.description); + + @observable + String description = ''; + + @observable + bool done = false; +} +``` + +## Custom Converters for Observable Collections + +`ObservableList` is not directly JSON-serializable. Write a `JsonConverter`: + +```dart +class ObservableTodoListConverter + extends JsonConverter, Iterable>> { + const ObservableTodoListConverter(); + + @override + ObservableList fromJson(Iterable> json) => + ObservableList.of(json.map(Todo.fromJson)); + + @override + Iterable> toJson(ObservableList object) => + object.map((e) => e.toJson()); +} +``` + +Apply it to the field: + +```dart +abstract class _TodoList with Store { + @observable + @ObservableTodoListConverter() + ObservableList todos = ObservableList(); +} +``` + +## Excluding Computed Properties + +Use `@JsonKey(ignore: true)` on computed properties that shouldn't be serialized (or `@JsonKey(includeFromJson: false, includeToJson: false)` in newer `json_annotation` versions where `ignore` is deprecated): + +```dart +@computed +@JsonKey(ignore: true) +ObservableList get visibleTodos { /* ... */ } +``` + +## Regenerate + +```bash +flutter pub run build_runner watch --delete-conflicting-outputs +``` + + diff --git a/skills/mobx-dart/references/best-practices-reactivity-rules.md b/skills/mobx-dart/references/best-practices-reactivity-rules.md new file mode 100644 index 00000000..2595094a --- /dev/null +++ b/skills/mobx-dart/references/best-practices-reactivity-rules.md @@ -0,0 +1,119 @@ +--- +name: best-practices-reactivity-rules +description: Rules of MobX reactivity, common tracking pitfalls, and when to use Observable collections vs plain Dart types +--- + +# When Does MobX React? + +Understanding MobX's tracking rules prevents common bugs where reactions don't fire as expected. + +## Rules of Reactivity + +### 1. Notifications fire when an observable changes value + +Every observable notifies linked reactions on value change. If no reactions observe it, the notification is lost. + +### 2. Tracking is automatic when a read happens + +Reading an observable inside a reaction's tracking function is enough — no explicit subscription needed: + +```dart +reaction((_) => person.name, (name) => print(name)); +// person.name is automatically tracked +``` + +### 3. Read the Observable, not the value + +If you extract a value before the reaction, MobX cannot track it: + +```dart +// WRONG: 'value' is a plain int, not an observable +final count = Observable(10); +var value = count.value; +reaction((_) => value, (v) => print(v)); // never re-executes! + +// CORRECT: read the observable inside the tracking function +reaction((_) => count.value, (v) => print(v)); // works! +``` + +### 4. Observer builder must read in immediate context + +`Observer` only tracks observables read in the **immediate execution** of its `builder` function. Observables read in nested functions, callbacks, or passed-down closures are **not** tracked. + +```dart +// NOT tracked: observable read in nested closure +Observer(builder: (_) { + return GestureDetector( + onTap: () => doSomething(store.value), // not tracked + child: Text('tap'), + ); +}) + +// Tracked: read in immediate context +Observer(builder: (_) { + final v = store.value; // tracked! + return Text('$v'); +}) +``` + +## List vs ObservableList + +`List` has no notion of observability. Adding/removing items won't notify MobX. Use `ObservableList` for reactive collections: + +```dart +// NOT reactive +@observable +List items = []; // reassignment tracked, but add/remove NOT tracked + +// Reactive +final items = ObservableList(); // add/remove/modify all tracked +``` + +Same applies to `Map` vs `ObservableMap`, `Set` vs `ObservableSet`, `Future` vs `ObservableFuture`, `Stream` vs `ObservableStream`. + +## Form Validation Pattern + +Use `reaction()` as side-effects for field validation: + +```dart +abstract class _FormStore with Store { + @observable String name = ''; + final error = FormErrorState(); + + late List _disposers; + + void setupValidations() { + _disposers = [ + reaction((_) => name, validateName), + reaction((_) => email, validateEmail), + ]; + } + + @action + void validateName(String value) { + error.name = value.isEmpty ? 'Cannot be blank' : null; + } + + void dispose() { + for (final d in _disposers) { d(); } + } +} +``` + +## Nested Stores + +Stores are regular Dart classes. Compose them freely: + +```dart +abstract class _FormStore with Store { + final FormErrorState error = FormErrorState(); + // error.username, error.email etc. are all observable +} +``` + + diff --git a/skills/mobx-dart/references/best-practices-store-organization.md b/skills/mobx-dart/references/best-practices-store-organization.md new file mode 100644 index 00000000..a1f26e3d --- /dev/null +++ b/skills/mobx-dart/references/best-practices-store-organization.md @@ -0,0 +1,144 @@ +--- +name: best-practices-store-organization +description: Store hierarchy, Widget-Store-Service triad, inter-store communication, lifetimes, and Provider integration +--- + +# Organizing Stores + +As applications scale, break down state into a hierarchy of stores with clear conceptual boundaries. + +## Widget-Store-Service Triad + +Three layers with top-down dependency: + +| Layer | Responsibility | +|---|---| +| **Widget** | Renders reactive state using `Observer` widgets. Primarily stateless. | +| **Store** | Holds `@observable` and `@computed` fields, exposes `@action` methods. No heavy work — just reactive state. | +| **Service** | Performs actual work: API calls, data transformations, validation. Completely stateless — all inputs passed in. | + +> Single Responsibility is the most important attribute of this pattern. + +## Store Hierarchy + +Start with one store. When it grows, split along **conceptual boundaries** (cohesiveness): + +```dart +// Before: everything in one store +abstract class _MainStore with Store { + @observable String title; + @observable String name; + @observable String email; + @observable String phone; +} + +// After: split into cohesive sub-stores +abstract class _MainStore with Store { + @observable String title; + final details = PersonDetails(); // not @observable — reference doesn't change +} + +abstract class _PersonDetails with Store { + @observable String name; + @observable String email; + @observable String phone; +} +``` + +## Inter-Store Communication + +**Option 1: Pass parent to child** — child accesses parent's public interface: + +```dart +class Parent { + Parent() { child = Child(parent: this); } + late Child child; +} + +class Child { + Child({required this.parent}); + Parent parent; +} +``` + +**Option 2: Callbacks** — looser coupling, child-to-parent direction: + +```dart +class Child { + void Function(String)? onChange; + + void perform() { + onChange?.call('value changed'); + } +} +``` + +For complex dependency graphs (shared `ThemeStore`, `AuthStore`, etc.), use a **Service Locator** like `get_it`. + +## Store Lifetimes + +- **App-level stores**: Create before rendering UI (preferences, auth, themes) +- **Screen-level stores**: Create in `initState()`, dispose in `dispose()` + +```dart +class _FormWidgetState extends State { + late FormStore store; + + @override + void initState() { + super.initState(); + store = FormStore(); + store.setupValidations(); + } + + @override + void dispose() { + store.dispose(); + super.dispose(); + } +} +``` + +## Provider Integration + +Use the `provider` package to make stores available throughout the widget tree: + +```dart +// Provide at app level +MultiProvider( + providers: [ + Provider(create: (_) => MultiCounterStore()), + ], + child: MaterialApp(/* ... */), +) + +// Consume in widgets +Widget build(BuildContext context) { + final store = Provider.of(context); + return Observer(builder: (_) => Text('${store.count}')); +} +``` + +Use `ProxyProvider` when stores depend on services: + +```dart +MultiProvider( + providers: [ + Provider(create: (_) => PreferencesService(prefs)), + ProxyProvider( + update: (_, service, __) => SettingsStore(service), + ), + ], +) +``` + +## Store Design Principles + +- Keep stores **independent** with dependencies fed via constructors +- Use **callbacks** for external communication from stores +- This improves **portability** and simplifies **testing** (pass mocks via constructor) + + diff --git a/skills/mobx-dart/references/core-actions.md b/skills/mobx-dart/references/core-actions.md new file mode 100644 index 00000000..c84fe932 --- /dev/null +++ b/skills/mobx-dart/references/core-actions.md @@ -0,0 +1,98 @@ +--- +name: core-actions +description: Action API for mutating observables including @action, runInAction, untracked, transaction, and async actions +--- + +# Actions + +Actions encapsulate mutations on observables, providing semantic naming and batched notifications. + +## Action + +```dart +// Direct API +final counter = Observable(0); +final increment = Action(() { counter.value++; }); +increment([]); // invoke with empty args list + +// With annotations (preferred) +abstract class _Counter with Store { + @observable + int value = 0; + + @action + void increment() { + value++; + } +} +``` + +Constructor: `Action(Function fn, {ReactiveContext? context, String? name})` + +### Guarantees + +- **Atomic notifications**: Changes are only notified at the end of the action +- **Nested actions**: For nested action calls, notifications sent only when the top-most action completes +- **Deferred reactions**: Linked reactions run only after the action finishes + +### Enforcement + +By default, MobX throws an exception if you mutate an observed observable outside an action. This is controlled by `ReactiveWritePolicy` in `ReactiveConfig`. Single-property setters in codegen stores are auto-wrapped in actions. + +### Async Actions + +Action methods can be `async`. The code generator ensures all mutations are wrapped in actions using Dart **zones**: + +```dart +@action +Future> fetchRepos() async { + repositories = []; + final future = client.repositories.listUserRepositories(user).toList(); + fetchReposFuture = ObservableFuture(future); + return repositories = await future; +} +``` + +## runInAction + +One-off action wrapper for ad-hoc mutations: + +```dart +runInAction(() { + counter.value = 10; + name.value = 'MobX'; +}); +``` + +Signature: `T runInAction(T Function() fn, {String? name, ReactiveContext? context})` + +## untracked + +Read observables inside a reaction without MobX tracking them: + +```dart +final x = Observable(0); + +autorun((_) { + untracked(() => print(x.value)); +}); + +x.value++; // autorun will NOT re-execute +``` + +## transaction + +Low-level batching primitive (used internally by Action). Guarantees no notifications until the function completes: + +```dart +transaction(() { + counter.value = 10; + name.value = 'MobX'; +}); +``` + + diff --git a/skills/mobx-dart/references/core-observables.md b/skills/mobx-dart/references/core-observables.md new file mode 100644 index 00000000..9e68edeb --- /dev/null +++ b/skills/mobx-dart/references/core-observables.md @@ -0,0 +1,123 @@ +--- +name: core-observables +description: Observable and Computed APIs including @observable, @readonly, @computed annotations and reactive extensions +--- + +# Observables and Computed Properties + +Observables are the reactive state of a MobX application. State divides into **core state** (inherent to domain) and **derived state** (computed from core state). + +## Observable + +```dart +// Direct API +final counter = Observable(0); +counter.value = 1; // fires notification + +// With annotations in a Store +abstract class _Todo with Store { + @observable + String description = ''; + + @observable + bool done = false; +} +``` + +Constructor: `Observable(T initialValue, {String? name, ReactiveContext? context})` + +### Reactive Extensions + +Convert primitives to observables with `.obs()`: + +```dart +var name = ''.obs(); // ObservableString +var counter = 0.obs(); // ObservableInt +var flag = true.obs(); +flag.toggle(); // flips boolean value +``` + +### @readonly Annotation + +Creates a public getter for a private observable field. The field can only be mutated inside `@action` methods: + +```dart +abstract class _Counter with Store { + @readonly + int _value = 0; // must be private + + @action + void increment() { + _value++; // only actions can mutate + } +} +// Usage: counter.value (read-only from outside) +``` + +### Deep Equality for Collections + +Use `@MakeObservable(useDeepEquals: true)` to compare collections by element equality instead of reference: + +```dart +abstract class _Todos with Store { + @MakeObservable(useDeepEquals: true) + List _todos = []; +} +``` + +## Computed + +Derived state that auto-updates when underlying observables change. Computed values are **cached** — notifications only fire when the computed value actually differs from the previous one. + +```dart +// Direct API +final first = Observable('Jane'); +final last = Observable('Doe'); +final fullName = Computed(() => '${first.value} ${last.value}'); + +// With annotations +abstract class _Contact with Store { + @observable + String first = ''; + + @observable + String last = ''; + + @computed + String get fullName => '$first $last'; +} +``` + +Constructor: `Computed(T Function() fn, {String name, ReactiveContext context, EqualityComparer? equals, bool? keepAlive})` + +### Key Behavior + +- Calling `.value` always re-evaluates the function (the result is not cached between reads) +- **Notification caching**: the previous value is cached; notifications only fire when the computed value differs from the cached value +- `keepAlive: true` prevents suspension when unobserved (risk of memory leaks) +- Use `@computed` to move conditional logic out of widgets into the store + +### Power of @computed + +Move business logic from widgets into computed properties: + +```dart +// Instead of checking in widget: +// if (store.loadOperation != null && store.loadOperation.status == FutureStatus.fulfilled) + +// Create a computed: +@computed +bool get hasResults => + loadOperation != null && + loadOperation.status == FutureStatus.fulfilled; + +// Widget becomes simple: +Observer(builder: (_) => store.hasResults ? ContactView(store) : Container()) +``` + + diff --git a/skills/mobx-dart/references/core-reactions.md b/skills/mobx-dart/references/core-reactions.md new file mode 100644 index 00000000..7ffa2dd4 --- /dev/null +++ b/skills/mobx-dart/references/core-reactions.md @@ -0,0 +1,123 @@ +--- +name: core-reactions +description: Reaction APIs (autorun, reaction, when, asyncWhen) for responding to observable changes with custom schedulers +--- + +# Reactions + +Reactions are the observer side of the MobX reactive system. They automatically track observables read during execution and re-run when those observables change. All reactions return a `ReactionDisposer` function. + +## autorun + +Runs immediately and re-runs whenever any tracked observable changes. + +```dart +final greeting = Observable('Hello World'); + +final dispose = autorun((_) { + print(greeting.value); +}); + +greeting.value = 'Hello MobX'; +dispose(); // stop tracking + +// Prints: Hello World, Hello MobX +``` + +Signature: `ReactionDisposer autorun(Function(Reaction) fn, {String? name, int? delay, ReactiveContext? context, Timer Function(void Function())? scheduler, void Function(Object, Reaction)? onError})` + +## reaction + +Monitors a tracking function and runs an effect only when the tracked value changes. Does **not** run the effect immediately (unlike autorun). + +```dart +final greeting = Observable('Hello World'); + +final dispose = reaction( + (_) => greeting.value, // tracking function + (msg) => print(msg), // effect +); + +greeting.value = 'Hello MobX'; // prints: Hello MobX +dispose(); +``` + +Signature: `ReactionDisposer reaction(T Function(Reaction) fn, void Function(T) effect, {String? name, int? delay, bool? fireImmediately, EqualityComparer? equals, ReactiveContext? context, Timer Function(void Function())? scheduler, void Function(Object, Reaction)? onError})` + +Key options: +- `fireImmediately`: Run effect on first evaluation too +- `equals`: Custom equality comparison for the tracked value +- `delay`: Throttle the effect in milliseconds + +## when + +One-time reaction that runs the effect when predicate becomes true, then auto-disposes. + +```dart +final greeting = Observable('Hello World'); + +final dispose = when( + (_) => greeting.value == 'Hello MobX', + () => print('Someone greeted MobX'), +); + +greeting.value = 'Hello MobX'; // runs effect and disposes +``` + +## asyncWhen + +Like `when` but returns a `Future` instead of taking an effect callback: + +```dart +final completed = Observable(false); + +Future waitForCompletion() async { + await asyncWhen(() => completed.value == true); + print('Completed'); +} +``` + +## Custom Scheduler + +Control when reactions re-execute using a scheduler: + +```dart +Timer customScheduler(void Function() fn) { + return Timer(Duration(milliseconds: 100), fn); +} + +final dispose = autorun( + (_) => print('Counter: ${counter.value}'), + scheduler: customScheduler, +); + +// Rapid changes are batched; only final value printed after delay +counter.value = 1; +counter.value = 2; +counter.value = 3; +``` + +## Disposing Reactions + +Always dispose reactions when no longer needed (e.g., in `State.dispose()`): + +```dart +abstract class _MyStore with Store { + late ReactionDisposer _dispose; + + void setupReactions() { + _dispose = autorun((_) { /* ... */ }); + } + + void dispose() { + _dispose(); + } +} +``` + + diff --git a/skills/mobx-dart/references/core-store-and-codegen.md b/skills/mobx-dart/references/core-store-and-codegen.md new file mode 100644 index 00000000..aa469b72 --- /dev/null +++ b/skills/mobx-dart/references/core-store-and-codegen.md @@ -0,0 +1,91 @@ +--- +name: core-store-and-codegen +description: MobX Store class pattern with mobx_codegen annotations and build_runner setup +--- + +# Store Class and Code Generation + +MobX Dart uses a code-generation approach via `mobx_codegen` to eliminate boilerplate. Stores are declared with a fixed pattern that enables `@observable`, `@computed`, and `@action` annotations. + +## Required Packages + +```yaml +dependencies: + mobx: ^2.6.0 + flutter_mobx: ^2.0.0 + +dev_dependencies: + build_runner: ^2.0.0 + mobx_codegen: ^2.0.0 +``` + +## Store Declaration Pattern + +Every store follows this boilerplate (the only repetitive part): + +```dart +import 'package:mobx/mobx.dart'; + +part 'todo.g.dart'; + +class Todo = _Todo with _$Todo; + +abstract class _Todo with Store { + /* observable fields, computed getters, action methods */ +} +``` + +Key points: +- The part file name must match the containing file: `todo.dart` -> `todo.g.dart` +- The abstract class uses `_` prefix and mixes in `Store` +- The public class blends the abstract class with the generated mixin `_$Todo` + +## Running Code Generation + +```bash +# Continuous watch mode (recommended during development) +flutter pub run build_runner watch --delete-conflicting-outputs + +# One-time build +flutter pub run build_runner build --delete-conflicting-outputs + +# Clean generated files +flutter pub run build_runner clean +``` + +## Annotations Summary + +| Annotation | Target | Purpose | +|---|---|---| +| `@observable` | Field | Makes field reactive, tracked by MobX | +| `@readonly` | Private field | Like `@observable` but auto-generates public getter; mutations only via `@action` | +| `@computed` | Getter | Derived state that auto-updates when dependencies change | +| `@action` | Method | Wraps mutations in a transaction; supports async | +| `@MakeObservable()` | Field | Advanced observable config (e.g., `useDeepEquals: true`) | + +## What the Generated Code Contains + +The `_$Todo` mixin in `todo.g.dart` generates: +- **Atom-based observables**: Each `@observable` field gets a backing `Atom` that handles `reportObserved()` and `reportChanged()` calls +- **Action wrappers**: Each `@action` method is wrapped in an `Action` for batched notifications +- **Computed getters**: Each `@computed` getter becomes a `Computed` instance with caching +- **Auto-action setters**: Property setters for `@observable` fields are auto-wrapped in actions + +When debugging, you can inspect the `.g.dart` file to understand the reactive wiring. + +## Troubleshooting Build Output + +- If `build_runner` refuses to run, use `--delete-conflicting-outputs` +- If you get stale output, run `flutter pub run build_runner clean` first +- Ensure your SDK version is at least `2.12.0` in `pubspec.yaml` + +## Important: No Deep Observability + +Unlike JavaScript MobX, Dart MobX does **not** support deep observability. Marking a complex object as `@observable` only tracks reassignment of the reference, not changes to the object's fields. Mark individual fields with `@observable` if you need field-level tracking. + + diff --git a/skills/mobx-dart/references/features-observer-widget.md b/skills/mobx-dart/references/features-observer-widget.md new file mode 100644 index 00000000..ed4dad10 --- /dev/null +++ b/skills/mobx-dart/references/features-observer-widget.md @@ -0,0 +1,110 @@ +--- +name: features-observer-widget +description: Flutter Observer widget, Observer.withBuiltChild optimization, and ReactionBuilder for reactive UI rendering +--- + +# Observer Widget and ReactionBuilder + +The `Observer` widget from `flutter_mobx` is the primary way to connect MobX stores to Flutter UI. It rebuilds automatically when any observable read in its `builder` changes. + +## Observer + +```dart +import 'package:flutter_mobx/flutter_mobx.dart'; + +Observer( + builder: (_) => Text('${counter.value}'), +) +``` + +### Critical Gotcha: Immediate Execution Context + +The `builder` function only tracks observables read in its **immediate execution context**. Observables read inside nested functions, callbacks, or child widget constructors are **not** tracked. + +```dart +// WRONG: observable read inside nested function — NOT tracked +Observer(builder: (_) { + return GestureDetector( + onTap: () => print(store.value), // not tracked! + child: Text('tap me'), + ); +}) + +// CORRECT: read the observable directly in the builder +Observer(builder: (_) { + final val = store.value; // tracked! + return Text('$val'); +}) +``` + +If your `Observer` is not updating, check that observables are read in the immediate builder scope. + +## Observer.withBuiltChild + +Performance optimization that excludes a child subtree from rebuilds (same technique as `AnimatedBuilder`): + +```dart +final obsColor = Observable(Colors.green); + +Observer.withBuiltChild( + builder: (context, child) { + return Container( + color: obsColor.value, // rebuilds when color changes + child: child, // child is NOT rebuilt + ); + }, + child: ListView.builder( // expensive widget, built once + itemCount: 1000, + itemBuilder: (context, index) => ListTile(title: Text('Item $index')), + ), +) +``` + +## ReactionBuilder + +Run reactions tied to a widget's lifecycle without creating a `StatefulWidget`: + +```dart +ReactionBuilder( + builder: (context) { + return reaction( + (_) => store.connectivityStream.value, + (result) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(result == ConnectivityResult.none + ? 'You\'re offline' : 'You\'re online')), + ); + }, + delay: 4000, + ); + }, + child: Scaffold( + appBar: AppBar(title: const Text('Settings')), + body: const Text('Toggle connection to see response'), + ), +) +``` + +- The `builder` returns a `ReactionDisposer` which is auto-disposed when the widget unmounts +- Keeps the outer widget as a `StatelessWidget` — no need for `initState`/`dispose` ceremony + +## ObservableList in Observer + +When passing an `ObservableList` to a child widget, call `.toList()` so the Observer tracks mutations: + +```dart +Observer(builder: (_) { + return ChildWidget( + list: controller.observableList.toList(), // tracked! + ); +}) +``` + +The child widget should accept `List`, not `ObservableList`, to keep the tracking context in the parent's Observer. + + diff --git a/skills/mobx-dart/references/features-reactive-collections.md b/skills/mobx-dart/references/features-reactive-collections.md new file mode 100644 index 00000000..d8a85b66 --- /dev/null +++ b/skills/mobx-dart/references/features-reactive-collections.md @@ -0,0 +1,136 @@ +--- +name: features-reactive-collections +description: ObservableList, ObservableMap, ObservableSet, ObservableFuture, ObservableStream and Atom for reactive data structures +--- + +# Reactive Collections and Wrappers + +Dart's built-in `List`, `Map`, `Set`, `Future`, and `Stream` are not reactive. MobX provides observable wrappers that participate in the reactive system. + +## ObservableList + +Tracks additions, removals, and modifications of items: + +```dart +final todos = ObservableList(); +todos.add('Buy milk'); // notifies observers +todos.removeAt(0); // notifies observers + +// From existing list +final items = ['a', 'b'].asObservable(); +``` + +## ObservableMap + +Tracks key additions, removals, and value modifications: + +```dart +final settings = ObservableMap(); +settings['theme'] = 'dark'; // notifies observers +``` + +## ObservableSet + +Tracks value additions and removals: + +```dart +final tags = ObservableSet(); +tags.add('flutter'); // notifies observers +``` + +## ObservableFuture + +Reactive wrapper around `Future` exposing `status`, `result`, and `error` as observables: + +```dart +abstract class _GithubStore with Store { + static ObservableFuture> emptyResponse = + ObservableFuture.value([]); + + @observable + ObservableFuture> fetchReposFuture = emptyResponse; + + @action + Future fetchRepos() async { + final future = client.repositories.listUserRepositories(user).toList(); + fetchReposFuture = ObservableFuture(future); + await future; + } +} + +// In widget — show loading state +Observer( + builder: (_) => store.fetchReposFuture.status == FutureStatus.pending + ? const LinearProgressIndicator() + : Container(), +) +``` + +`FutureStatus` values: `pending`, `fulfilled`, `rejected` + +## ObservableStream + +Reactive wrapper around `Stream` exposing `data`, `error`, and `status`: + +```dart +ObservableStream( + myStream, + initialValue: defaultValue, + cancelOnError: false, + equals: customEqualityFn, // optional +) +``` + +## asObservable() Extension + +Convert plain collections to observable versions: + +```dart +final list = [1, 2, 3].asObservable(); // ObservableList +final map = {'a': 1}.asObservable(); // ObservableMap +final set = {1, 2, 3}.asObservable(); // ObservableSet +``` + +## Atom + +Low-level reactive primitive at the core of MobX. Does not store a value — only tracks observation and change notifications. `Observable` extends `Atom`. Rarely used directly. + +```dart +class Clock { + Clock() { + _atom = Atom( + name: 'Clock Atom', + onObserved: _startTimer, + onUnobserved: _stopTimer, + ); + } + + DateTime get now { + _atom.reportObserved(); // tell MobX this is being read + return DateTime.now(); + } + + late Atom _atom; + Timer? _timer; + + void _startTimer() { + _timer?.cancel(); // guard against re-entrance + _timer = Timer.periodic(Duration(seconds: 1), (_) { + _atom.reportChanged(); // tell MobX value changed + }); + } + + void _stopTimer() { + _timer?.cancel(); + } +} +``` + +Use cases: custom reactive data sources (clocks, sensors, external event streams). + +