Run a Flutter web app in a browser Flutter refuses to launch (Arc, Safari, Orion, anything that isn't Chrome or Edge) with hot reload, release mode and a clean quit.
flutter-browser # debug, opens in Arc
flutter-browser --release # release build, same flow
flutter-browser -b Safari # any macOS browsermacOS only for now.
Point CHROME_EXECUTABLE at Arc and flutter run -d chrome dies with:
Failed to launch browser after 3 tries.
Failed to launch browser. Make sure you are using an up-to-date Chrome or Edge.
The advice is wrong. Arc is up to date. Flutter's chrome device doesn't just
open a URL, it remote-controls the browser over the Chrome DevTools Protocol: it
launches the binary with --remote-debugging-port and --user-data-dir, then
blocks on the child's stderr waiting for a line starting with DevTools listening.
Arc never prints it. It isn't a rebranded Chrome binary, it's a native Swift app
that embeds Chromium as ArcCore.framework and owns main(), so Chromium
switches never reach the engine. With Flutter's exact flags:
| Arc | Chrome, same conditions | |
|---|---|---|
--version |
prints nothing | prints version |
DevTools listening on stderr |
never | printed |
| CDP port responds | nothing listening | serves /json/version |
--user-data-dir honoured |
ignored, dir left empty | full profile written |
No configuration fixes this, and no upstream patch could either, since the
capability isn't Flutter's to add. Flutter closed the request as r: invalid
(flutter/flutter#148131).
The web-server device needs none of that. Instead of Flutter reaching into the
browser, the page reaches out: DWDS talks to the app over its own injected
WebSocket client. No debugging port, no handshake, no CDP. The browser only has
to do the one thing every browser does, which is load a URL.
In Arc: hot reload 56-139ms, hot restart 40-171ms, Dart VM Service and the DevTools debugger/profiler attached, release builds running.
flutter-browser wraps that device and fixes the two things it gets wrong on its
own, see docs/HOW-IT-WORKS.md.
git clone <your-fork-url> flutter-browser
cd flutter-browser
./install.shSymlinks flutter-browser and flutter-browser-shim into ~/.local/bin, so
git pull updates them in place.
flutter-browser [options] [-- flutter run args]
-b, --browser NAME browser to open (default: Arc, or $FLUTTER_BROWSER)
-p, --port PORT port to serve on (default: first free from 8899)
--keep-tab leave the browser tab open after the run ends
--open-as MODE tab (default) or window
--tab force a real tab - shorthand for --open-as tab
--window open a separate window instead
--no-console don't mirror the browser console into this terminal
--dry-run print the flutter command that would run, then exit
-h, --help
print(), console.log, warnings and uncaught errors land in the terminal, the
way they do with flutter run -d chrome:
$ flutter-browser
→ Arc will open at http://localhost:8899 once the build is ready
lib/main.dart is being served at http://localhost:8899
Starting application from main method in: org-dartlang-app:/web_entrypoint.dart.
counter is now 1
Bad state: something went wrong
at Object.throw_ [as throw] (http://localhost:8899/dart_sdk.js:3803:11)The chrome device gets those over CDP, which is exactly what web-server
doesn't have, so this wrapper injects a console hook into the served page and
prints what the page posts back. Errors are red, console.debug is dropped -
both match what DWDS does for the chrome device.
Stack traces are the raw JavaScript ones. The chrome device maps them back to
Dart source through DWDS; nothing here does that.
--no-console (or FLUTTER_BROWSER_CONSOLE=0) turns the whole thing off and
serves the app directly.
open -a Arc <url>, the obvious way to open a URL, makes Arc spawn a Little Arc
window: a throwaway popup outside its normal window/tab model. They pile up one
per run, and Arc's own AppleScript can't enumerate them, so nothing can clean
them up afterwards.
Tab mode is the default and creates a real sidebar tab via AppleScript, which can be reused next run and closed on quit. If Arc refuses the script (it can, while launching or busy) the tool retries, and only then falls back to a window with a warning on stderr. Silence there was what let stray Little Arc windows pile up unexplained.
--window opts into the old behaviour deliberately.
Anything unrecognised is passed straight to flutter run, so every flutter run
flag works unchanged, --dart-define included:
flutter-browser --release
flutter-browser --dart-define=API=staging
flutter-browser --dart-define=API=staging --dart-define=DEBUG=true
flutter-browser --dart-define="MSG=hello world"
flutter-browser --dart-define-from-file=config/dev.json
flutter-browser -b "Google Chrome" --profile
flutter-browser -- --web-renderer canvaskitValues with spaces and = survive intact. Use --dry-run to see exactly what
gets forwarded:
$ flutter-browser --dry-run --dart-define="MSG=hello world"
browser: Arc
open-as: tab
console: on (flutter serves on 8900)
url: http://localhost:8899
command: flutter run -d web-server --web-hostname=localhost --web-port=8900 --dart-define=MSG=hello\ world-p is always the port you browse. With the console mirror on, that port is the
mirror's and Flutter serves behind it on the next free one.
Use -- when a value would otherwise be eaten as one of this tool's own flags.
r, R and q work as normal. q stops the server and closes the tab.
Works: running the app, hot reload, hot restart, release mode, print() and
console output in the terminal, the browser's own DevTools console, clean quit.
Untested: IDE breakpoint debugging. The web-server device prints a warning about needing the Dart Debug Chrome extension, and while the debug service attached without any extension in every run here, breakpoints from VS Code were never verified. Treat that as unknown rather than working.
If something insists on -d chrome, like a VS Code or Android Studio run button,
use the shim instead:
export CHROME_EXECUTABLE="$HOME/.local/bin/flutter-browser-shim"A headless Chrome absorbs the debug protocol while your real browser shows the
app, and flutter run -d chrome works untouched. It costs one hidden Chrome
process, which is why it isn't the default.
Tab reuse and auto-close need an AppleScript tab model:
| browser | opens | closes tab on quit |
|---|---|---|
| Arc, Chrome, Edge, Brave, Vivaldi, Chromium | yes | yes |
| Safari | yes | yes |
| everything else (Firefox, Orion, ...) | yes | no, tab stays open |
The tab is matched on localhost:<port>, never on localhost alone, so
unrelated local work is never closed.
MIT