Solid (Flutter)
Solid is a tiny framework on top of Flutter. You write reactive state directly on a StatelessWidget in source/; the solid_generator build_runner builder transpiles each source/<x>.dart into lib/<x>.dart, turning your class into a StatefulWidget with Signal/Computed/Effect/Resource plumbing from flutter_solidart. Inspired by SwiftUI (@Environment) and SolidJS (fine-grained reactivity).
User-facing docs: https://solid.mariuti.com.
Step 0 — Is this project Solid?
Before doing anything, decide if the project actually uses Solid. The user may not say "Solid" anywhere — and an existing lib/ directory plus solid_annotations/solid_generator not being in pubspec means this is not a Solid project.
Run this check first:
grep -E '^\s*(solid_annotations|solid_generator):' pubspec.yaml
- Match found → this is a Solid project. Follow this skill.
- No match → not a Solid project. Don't apply Solid conventions. Skip this skill.
The same check via Read tool works too: read pubspec.yaml and look for either package name under dependencies: or dev_dependencies:. A Solid setup typically has solid_annotations (runtime) under dependencies and solid_generator plus build_runner under dev_dependencies.
Cardinal rule
Edit source/<x>.dart. Never edit lib/<x>.dart.
In Flutter, lib/ is where you write code. In Solid, lib/ is generated output — every time build_runner runs, it overwrites lib/<x>.dart from source/<x>.dart. Hand-edits to lib/ are lost on the next build. The user-facing entry point is source/main.dart; lib/main.dart is generated.
This inverts Flutter muscle memory. The rest of the Flutter ecosystem (flutter run, pub packages, IDE templates, every tutorial on the internet) assumes lib/ is the source of truth. In this project it isn't. Apply the substitution everywhere.
Decision shortcuts
| Situation | What to do |
|---|---|
About to write to a file under lib/ |
Stop. Find or create the matching source/<x>.dart and write there. |
| User asks to "add a widget / page / button / form" | Create source/<snake_name>.dart. Use @SolidState for fields you'll mutate. |
| User asks to "fetch X when Y changes" | @SolidState for the input Y, @SolidQuery (no parameters) for the fetch. Body reads Y to register the dependency. |
User asks to install a pub package whose README says "create lib/<x>.dart" |
Substitute source/ for lib/ in every file-creation step. See references/third-party-packages.md. |
User says "I edited lib/foo.dart and the change disappeared" |
The lib/ write is the bug, not build_runner. Migrate the change to source/foo.dart, then regenerate. |
build_runner output looks unpolished (no const, unused imports) |
Run scripts/verify.sh, which chains dart fix --apply after build_runner. |
| build_runner fails | Run scripts/verify.sh from the package root — it surfaces the first [SEVERE] line. |
Third-party packages: substitute source/ for lib/
When the user (or another AI) installs a new pub package, the package's README, examples, and any AI-generated setup instructions will all assume lib/ is the source of truth. In a Solid project that assumption is wrong.
Rule of thumb: the package itself stays in pubspec.yaml as the docs describe. Only the files you write that import it move from lib/ to source/. Examples:
go_routerREADME says "createlib/router.dart" → createsource/router.dartinstead, and import it fromsource/main.dartvia a relative path.freezedsays "createlib/models/user.dart" → createsource/models/user.dart. Freezed's own*.freezed.dartgenerated output still lands whereverbuild.yamlputs it (typically next to the source file underlib/, since freezed reads fromlib/); but in a Solid project you want freezed to read fromsource/too. Addsource/**to freezed'sbuild.yamlsourceslist (Solid's setup already does this — keep it).riverpodgenerator says "createlib/providers/..." → createsource/providers/....driftsays "createlib/database.dart" → createsource/database.dart.
For the full list and per-package gotchas, read references/third-party-packages.md.
The key thing to tell yourself: "the docs say lib/. In this project, that means source/."
Same-package imports must be relative
Inside a source/ file, reference other source files via relative paths (../controllers/foo.dart), never via package:<self>/foo.dart. The package: form resolves to lib/ (the generated realm), pointing your source file at lowered Signal types — the generator now rejects it at build time. Cross-package imports (flutter, solid_annotations, provider, third-party) keep the package: form as usual.
Annotation cheat sheet
Each annotation goes on a class member of a StatelessWidget (or any class — @SolidEnvironment also works in State<X>). The generator turns the widget into a StatefulWidget under the hood.
@SolidState()— reactive state. Docs: https://solid.mariuti.com/guides/state.- Valid on: instance field with initializer,
latenon-nullable instance field, nullable instance field, instance getter (derived state). - Invalid:
final,const,static, setter, method, top-level. - Example:
@SolidState() int counter = 0;or@SolidState() int get doubleCounter => counter * 2;.
- Valid on: instance field with initializer,
@SolidEffect()— side effect that re-runs whenever its tracked dependencies change. Docs: https://solid.mariuti.com/guides/effect.- Valid on: instance method returning
void. - Example:
@SolidEffect() void logCounter() { print('Counter: $counter'); }.
- Valid on: instance method returning
@SolidQuery()— reactive async/stream resource. Docs: https://solid.mariuti.com/guides/query.- Valid on: instance method returning
Future<T>orStream<T>. No parameters. - Call site
fetchData()returns aResource<T>exposing.when(ready:, loading:, error:),.maybeWhen(...),.isRefreshing,.refresh(). - Options:
debounce: Duration(...),useRefreshing: false. - Read
@SolidStatefields from the body to make the query react to them.
- Valid on: instance method returning
@SolidEnvironment()— inject a value from the widget tree (SwiftUI-style). Docs: https://solid.mariuti.com/guides/environment.- Valid on:
latefield on aStatelessWidgetorState<X>. - Bound on first access to the nearest ancestor
Provider<T>. - Provide via
.environment<T>()extension shipped bysolid_annotations, orProvider<T>frompackage:provider.
- Valid on:
For full target rules per annotation, read references/annotation-contract.md. For canonical idioms, read references/patterns.md.
Untracked reads
By default every read of a @SolidState field inside build, @SolidEffect, or @SolidQuery registers a dependency. Two opt-outs:
- Automatic: reads inside callback parameters whose name starts with
on(onPressed,onTap,onChanged, …) are untracked — Solid recognizes user-interaction handlers and doesn't subscribe. - Manual read: append
.untrackedto the field. Common use:key: ValueKey(counter.untracked).
In string interpolations, only the long form works: '${counter.untracked}'. The short form '$counter.untracked' parses as ${counter} followed by a literal suffix (still tracked).
To write a signal inside a @SolidEffect without the write re-triggering the effect (required for collection signals, whose element-writes self-subscribe), read the deps first, then wrap the write in the untracked(() => …) function: final c = counter; untracked(() => history = [...history, c]);. Don't wrap the whole body — that untracks the dependency reads too. See references/patterns.md §8.
Docs: https://solid.mariuti.com/guides/untracked.
Setup checklist (fresh project)
If pubspec.yaml doesn't yet declare Solid, install it:
flutter pub add solid_annotations flutter_solidart providerdart pub add --dev solid_generator build_runner- Create
build.yamlat the project root (or extend the existing one):targets: $default: sources: - source/** - lib/** - $package$ - In
source/main.dart, setSolidartConfig.autoDispose = false;beforerunApp(...). (Temporary — will become the default in a futureflutter_solidartmajor release.) - In
analysis_options.yaml, addanalyzer.errors.must_be_immutable: ignore(your source widgets are mutable; the generated ones are immutable). - Run
dart run build_runner watchduring development.
Verify your changes
After writing or editing source/, regenerate lib/ and apply lint fixes:
scripts/verify.sh— run from any package root. Runsdart run build_runner build, thendart fix --applyon the package (addsconst, removes unused imports, applies relative-import lints). Prints PASS/FAIL plus the first[SEVERE]error on failure. Exit code reflects build_runner success;dart fixfailure is non-fatal.
Why dart fix --apply matters: the generator prioritises correct, runnable code over polish. The emitted lib/ may miss const opportunities, leave unused imports, or pick a non-preferred import form. dart fix --apply cleans this up using the project's lint rules (prefer_const_constructors, unnecessary_import, prefer_relative_imports, …). Always run it after generation — in CI too.
Hot reload
flutter run does not auto-reload when build_runner rewrites lib/ (no IDE save event fires for filesystem changes). Two workflows:
- Press
rin theflutter runterminal after build_runner emits. - Use
dashmonx— wrapsflutter runand triggers hot reload onlib/changes. Anyflutter runflag passes through, e.g.dashmonx -d chrome.
Common mistakes
- Writing to a file under
lib/. Always undersource/. (The single hardest rule to internalise.) - Following a pub-package README literally when it says
lib/. Substitutesource/. Seereferences/third-party-packages.md. - Adding
finalorstaticto a@SolidStatefield. The generator rejects it. - Giving
@SolidQueryparameters. Use@SolidStatefields as inputs — the query re-runs when they change. - Forgetting
SolidartConfig.autoDispose = falseinsource/main.dart— atoms leak in tests and long sessions. - Importing same-package files via
package:<self>/...from insidesource/. Use relative paths. - Expecting
flutter runto pick up build_runner output withoutrordashmonx. - Treating
must_be_immutablelint as a real error — your widgets are mutable; the generated ones are immutable. Setmust_be_immutable: ignore.
Helper scripts
scripts/verify.sh— described above.scripts/scaffold-widget.sh <PascalName> [--state|--query|--env]— writes a startersource/<snake_case>.dartwith the right boilerplate. Refuses to overwrite.
Where to read more
- Canonical docs: https://solid.mariuti.com
- Annotation valid/invalid targets:
references/annotation-contract.md - Canonical idioms (counter, computed, effect, query, environment, untracked):
references/patterns.md - Error symptom → cause → fix:
references/troubleshooting.md - Third-party package redirect catalogue:
references/third-party-packages.md - Working example: https://github.com/nank1ro/solid/tree/main/example/source
Source: nank1ro/solid — distributed by TomeVault.