flutter-web
Skill12 starsChanged 55 days agoMITView on GitHub
Use when working on a Flutter web build - code touching dart:js_interop, package:web or dart:ui_web; tests that will not compile on the VM runner; a blank white page after a build; or QA of a running web app in a browser. Triggers on "works on mobile but not on web", a suite that breaks after a web import is added, or a screen that looks frozen in the browser.
- Deletes or force-pushes
In this repository15 other files it gives its agents.
- flutter-accessibilitySkill
- flutter-cachingSkill
- flutter-clean-archSkill
- flutter-concurrencySkill
- flutter-dart-styleSkill
- flutter-design-systemSkill
- flutter-diSkill
- flutter-formsSkill
- flutter-monorepoSkill
- flutter-new-moduleSkill
- flutter-runSkill
- flutter-stateSkill
- flutter-store-cdSkill
- flutter-testingSkill
- flutter-themingSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Before blaming the app:
- Very specific: line 27
If it reports hidden, bring the browser window forward and make the tab active.
- Very specific: line 53
Both commands run over
test/: Do not move browser tests to a directory of their own — the web runner builds its entrypoint with test/ as … - Distinctive: line 86
When a widget holds a browser resource, release it on every way out — not only the happy one:
- Distinctive: line 109
Do that before concluding anything from a blank screen.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
You are working on a Flutter web build. Everything here is a lesson that cost real hours — each one looks like an application bug and is not.
Browser QA only counts in a visible tab
Flutter web drives its entire frame pipeline from requestAnimationFrame,
and Chrome throttles rAF to near zero in a tab that is not the active one. In a
background tab the app still runs — events are processed, network calls fire —
but it barely paints. So route transitions appear frozen for tens of seconds,
and click coordinates read off a screenshot land nowhere, because the painted
frame no longer matches the real hit-test layout.
That looks exactly like two bugs that do not exist: a slow splash screen and broken Tab traversal.
Before blaming the app:
document.visibilityState // must be "visible"If it reports hidden, bring the browser window forward and make the tab
active. Two things stay reliable either way, and are the better evidence:
performance.getEntriesByType('resource')for real timings — in one case it showed auth resolving at 1.4s while the screen looked stuck for 20s.document.activeElement, and the hidden<input>Flutter creates for the focused text field, which proves where focus actually is.
Each CDP screenshot forces one frame, so consecutive screenshots step a stalled transition forward — useful, and itself a hint that you are in a throttled tab.
Browser-layer code needs the Chrome test runner
Anything touching dart:js_interop or package:web — getUserMedia,
MediaRecorder, an <audio> element — cannot even compile under the
default VM test runner.
@TestOn('browser') is what makes that survivable: flutter test skips an
annotated file instead of compiling it. The browser layer is untested by
default, not untestable:
flutter test # VM: controllers, repositories, pure widgets
flutter test --platform chrome # the browser layerBoth commands run over test/. Do not move browser tests to a directory of
their own — the web runner builds its entrypoint with test/ as the root, so a
path outside it becomes a ../ that does not resolve and nothing compiles at
all. The annotation is the separator, not the directory.
The trap that makes this dangerous: flutter test alone reports green over
a browser test that never compiled. Run both before pushing, and run both in
CI as two separate steps.
--platform chrome is also flaky or unavailable on some machines (a file with
@TestOn('browser') hanging at loading until the test timeout is the usual
symptom). Where that happens, CI is the only place the browser layer runs — so
prefer pulling the logic out of the browser-bound file into a plain Dart
one and testing that on the VM runner.
Make it testable: contract first, js_interop behind it
Declare the minimum contract as an abstract class, keep the js_interop
implementation behind it, and inject it so a widget test can pass a fake:
abstract class AudioRecorder {
Future<void> start();
Future<Uint8List?> stop();
void dispose();
}
// browser_audio_recorder.dart — the only file importing package:web
class BrowserAudioRecorder implements AudioRecorder { ... }Every exit path must release a browser resource
When a widget holds a browser resource, release it on every way out — not only the happy one:
dispose()unconditionally, rather than only when a_recordingflag flipped.- A release when the screen turns out to be unmounted after an
await. - A guard so a second tap during the first
awaitis a no-op.
A real leak was exactly the gap between those: permission was still pending, so
the flag had not flipped, so dispose() released nothing — and an orphaned
ticker wrote to a closed StreamController every 200 ms for the life of the tab.
Blank white page after changing a path dependency
Path dependencies (a design_system/ or calendar/ package in the same repo)
are not always picked up by the incremental web build. It emits a bundle that
compiles, loads every asset, throws no JS error — and renders a blank white page
with an empty <flutter-view>. Hours disappear looking for a bug in code that
is fine.
rm -rf build/web && flutter build web --release --dart-define=BASE_URL=...Do that before concluding anything from a blank screen.
Watch the import chain, not just the import
A web-only import poisons every file that reaches it transitively, and the compiler names the file that failed — not the file at fault.
The classic shape: a app_routes.dart holding both the route-name constants
and the route table. The table imports every screen, one of which imports
dart:ui_web. A sign-out use case importing that file for one string
constant then makes every VM test that touches it fail to compile, taking the
whole suite with it.
The fix is to split the constants into a file that imports nothing:
// app_route_names.dart — imports nothing
class AppRouteNames {
static const login = '/login';
static const home = '/home';
}
// app_routes.dart — re-exports the names, and owns the table
export 'app_route_names.dart';
class AppRoutes { /* onGenerateRoute + the table */ }Re-export at the library level, with export. Do not try to inherit the
names (class AppRoutes extends AppRouteNames): Dart does not inherit static
members, so AppRoutes.login would not resolve even when it compiles — and if
the names class is final, extending it is a compile error outright. Callers
say AppRouteNames.login; the export is what saves them a second import.
Screens and the root widget may import the table. A use case, a repository or an interceptor may not — they take the names file.
So when the VM suite fails to load a file rather than fail an assertion, read which file it named, then walk the import chain back to the web-only leaf.
Checklist before shipping a web change
flutter analyze
flutter test # VM
flutter test --platform chrome # browser layer
rm -rf build/web
flutter build web --release --dart-define=BASE_URL=<api-url>Common mistakes
- Debugging a "frozen" screen in a background tab. Check
document.visibilityStatefirst. - Trusting a green
flutter testover code that only compiles on the browser runner. - Moving browser tests out of
test/—--platform chromecannot resolve a path outside it. - Calling
js_interopstraight from a widget instead of behind an injectable interface. - Releasing a browser resource only on the happy path. Unmount during an
awaitis a real exit. - Concluding "blank page = my bug" without a clean
build/webrebuild first. - Importing the route table from a use case or interceptor just to read one route name.
- Forgetting
--dart-define=BASE_URL, so the compile-time constant is""and every call fails. See flutter-run. - Leaving semantics off — Flutter web disables them by default. See flutter-accessibility.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

