Hot reload
Fleury supports stateful hot reload: save a changed source file and the
running terminal app updates in place — widget state, focus, and scroll
positions survive. It uses the Dart VM’s own code reloading (the mechanism
Flutter developers rely on), driven by a dev supervisor rather than a file
watcher that restarts your process. On macOS and Linux, the supervisor works
with any editor, with no plugin. It doesn’t run on Windows, where fleury run
and dart run start the app without reload or restart; use an
editor debug session there.
Try it in the browser first. Press Run, write a draft in the notes app, then change the heading in the editor and press Hot reload (or ⌘/Ctrl+S). The draft stays in place; Restart runs the code again with fresh state.
This runs on Fleury Pad: the Dart development compiler builds your edit, and the app reassembles through the same framework hook a terminal reload uses. In your own project, the loop below does the same on save.
Start a reloadable session
Section titled “Start a reloadable session”From your project, run:
fleury runfleury run starts your entrypoint from bin/ under Fleury’s dev supervisor:
the only Dart file there, else main.dart, else run_app.dart, else the file
named after the package. Name another one with fleury run bin/other.dart. VM
options such as --enable-asserts go before the script, and your app’s own
arguments after it. Without the CLI on your path, use dart run fleury run
from the project directory.
fleury run --enable-asserts bin/run_app.dart --profile=localA compiled Fleury CLI supports the same development loop: the launcher starts your source app in the Dart SDK’s JIT VM. Keep the Dart SDK installed for this workflow. A compiled app itself has no hot reload.
The supervisor watches your package’s sources (lib/, bin/, and the lib/
of any local path dependency, a framework checkout included) and reloads on
save. Edit in vim, Zed, IntelliJ, VS Code, anything: saving is the trigger.
Try it: run any app, note some state (a counter, a scroll position, a focused input), change a color or a label in the source, and save. The frame updates with the new code, and the state stays where you left it.
Reload results appear in the debug shell
(Ctrl+G). “Reloaded N libraries in Xms” lands in the Logs tab. A failed
reload shows the compiler’s message in the Errors tab and as an error
banner in the app, which keeps running on the previous code. If the compiler
reports a syntax or type error, fix it and save again. The next successful
reload keeps your state and clears the failed-edit banner. Restarting does
not fix a compilation error.
A plain dart run
Section titled “A plain dart run”dart run bin/run_app.dart starts the same session, with the same reload and
restart, but your entrypoint runs in two processes and compiles twice on a
cold start: see Keep startup work inside the app.
If your app reads its command-line arguments, pass them to runApp:
Future<void> main(List<String> args) => runApp(const MyApp(), args: args);The app’s process can’t recover the original arguments by itself, so without
args: it sees an empty list from its first frame. fleury run passes the
arguments for you.
Reload or restart
Section titled “Reload or restart”Reload keeps state and is the default loop. Some valid edits cannot be migrated
into a running program, such as changing a class’s type parameters. Other
edits apply but don’t reach what has already run: main(), top-level
initializers, and initState for existing states keep the results of their
first run.
For valid changes the VM cannot migrate, or when you deliberately want fresh
state, use hot restart: it drops all state and runs main() again in the
same terminal session. Press Ctrl+G to open the debug shell, then F5; the
shell’s header shows F5 restart whenever a restart is available. Fix
compilation errors before restarting.
When F5 doesn’t restart
Section titled “When F5 doesn’t restart”-
On a Mac laptop, the top-row
F5needsFnunless the keyboard is set to use F1, F2, and so on as standard function keys. -
In VS Code’s integrated terminal, VS Code takes
F5before the app sees it: its defaultterminal.integrated.commandsToSkipShellincludesworkbench.action.debug.startandworkbench.action.debug.continue.F5then starts a debug session (in afleury createproject, a second copy of the app) instead of restarting this one. To sendF5to the app, remove those two defaults in.vscode/settings.json; a leading-removes an entry:.vscode/settings.json {"dart.cliConsole": "terminal","dart.hotReloadOnSave": "allIfDirty","terminal.integrated.commandsToSkipShell": ["-workbench.action.debug.start","-workbench.action.debug.continue"]}F5still starts debugging from the editor. Otherwise, quit withCtrl+Cand runfleury runagain, or run the app in another terminal. -
Under the VS Code debugger (the
F5launch configuration), the debug shell offers no restart, because the editor owns the run. Use the editor’s Restart instead. -
Where the supervisor steps aside, including Windows (see below), there is no restart: quit and run the app again.
What survives a reload
Section titled “What survives a reload”- Every field on your
Stateobjects (the object is preserved; only its code is swapped). - Focus — your text input stays focused, with its caret.
- Scroll offsets on
ListView,Tree,DataTable. - Subscriptions registered in
initState(they were never torn down). - A value
Animationsettles at its current target so no stale completion is left pending; aFrameTickerresets its phase and re-anchors its clock.
What doesn’t
Section titled “What doesn’t”- Anything computed in
main()beforerunApp, and top-level globals initialized at startup. - Work done in
initStatefor states that already exist. - Object identity for instances created in
build()(same as Flutter). - Valid changes the VM cannot migrate into existing objects. Hot restart applies those with fresh state; compilation errors still need fixing first.
Refreshing caches on reload
Section titled “Refreshing caches on reload”If a State caches an expensive computation (parsed config, fetched data)
and you want it recomputed on reload, override reassemble — the same hook,
name, and semantics as Flutter:
class _MyWidgetState extends State<MyWidget> { late ParsedConfig _config;
@override void initState() { super.initState(); _config = parseConfig(widget.configSource); }
@override void reassemble() { super.reassemble(); _config = parseConfig(widget.configSource); // re-parse with new code }}Keep startup work inside the app
Section titled “Keep startup work inside the app”With fleury run, your main() runs once, in the app’s process. A plain
dart run works differently: your entrypoint starts, reaches runApp, and
becomes the supervisor, which runs the same entrypoint again as the app.
Everything in main() before runApp runs in both processes, so work that
can happen only once — binding a port, taking a lock, reading stdin, writing a
pid file — fails or runs twice. The supervisor’s own call to runApp never
returns: when the session ends, that process exits with the app’s exit code,
so nothing after runApp (a finally included) runs there.
Code after an awaited runApp runs when the UI closes: once per app process,
and each hot restart ends one process and starts another. It cannot hold a
resource the UI needs. Put that work inside the app, where
only the app’s process runs it: the root widget’s initState, for example, or
a Scope.create that owns the resource and releases it in dispose: (see
State management). Alternatively, start
with fleury run, or run without the supervisor (FLEURY_HOT_RELOAD=0, or
runApp(enableHotReload: false)).
The generated scaffold’s main() is just the runApp call, so this only
matters once you add startup work. The supervisor prints a hint when the first
app process exits with an error within two seconds of starting.
In an editor debug session
Section titled “In an editor debug session”When you launch under a debugger, the editor owns the run and the supervisor
steps aside. Fleury follows the editor’s reloads instead: when the editor calls
reloadSources, Fleury picks up the VM’s reload event and reassembles the
widget tree automatically. The debug shell offers no restart in these
sessions; use the editor’s Restart when you need fresh state.
In VS Code with the
Dart extension,
F5 runs the app under the debugger, which for a Fleury app has to happen in
the integrated terminal. fleury create projects come pre-wired for this: the
generated .vscode/launch.json points the app at the integrated terminal, and
.vscode/settings.json sets dart.hotReloadOnSave: "allIfDirty" so saving a
dirty file during a debug session reloads without a keypress. For an existing
project, copy those two files’ three fields (console: terminal,
dart.cliConsole: terminal, dart.hotReloadOnSave) and point program at
your entrypoint. No Fleury-specific editor extension exists or is needed.
IntelliJ IDEA and Android Studio run the app in a console that isn’t a
terminal, so runApp stops with “runApp needs an interactive terminal”.
Start fleury shell in a terminal first and leave it running: every run of
the app, including the editor’s restarts, draws there while the IDE keeps its
debugger. Debugging has the
steps.
When the supervisor steps aside
Section titled “When the supervisor steps aside”The supervisor runs only when it can own the session safely: a source app
launched through fleury run or a plain JIT dart run bin/run_app.dart on a
real terminal, on macOS or Linux. A dart run without a file path, or
dart run package:exe, runs a precompiled snapshot instead, so it gets neither
hot reload nor the debug shell. It automatically yields to anything else that owns the run — an
editor debug session (a live VM service), a fleury serve, fleury shell, or
fleury_mcp handle, a compiled app, Windows, a non-TTY, or an app that passes
its own driver: to runApp — and the app runs exactly as it would without
it. (Under fleury run, an app with its own driver still reloads on save, but
the debug shell offers no restart.)
Reloading a browser preview
Section titled “Reloading a browser preview”Because the supervisor steps aside for a serve handle, the usual browser command hot reloads nothing:
fleury serve --spawn dart run bin/run_app.dart # no VM service, no reloadEnable the service in the spawned command itself and the app reloads on save, with the browser preview updating live:
fleury serve --spawn dart --enable-vm-service=0 run bin/run_app.dart=0 lets the VM pick a free port. Reload only — hot restart is intentionally
unavailable here, because a respawned child would re-dial the handle’s
single-accept socket and wedge the session. serve never adds the flag on
your behalf: opening a debug port is your call.
Opting out
Section titled “Opting out”FLEURY_HOT_RELOAD=0 fleury runor runApp(enableHotReload: false) — the right setting for production
launches, where it also skips the service-extension registration.
How it works
Section titled “How it works”A plain dart run has no VM service, so nothing could trigger a reload. The
supervisor closes that gap: it runs your entrypoint as a child process with the
VM service enabled (the child owns the terminal, raw mode, and signals exactly
as a normal run would), watches the package sources listed in
package_config.json, debounces saves, and calls the VM’s reloadSources on
the child. After the VM swaps the code, Fleury walks the element tree calling
State.reassemble() and marking every element dirty, so the next frame redraws
against the new code. Hot restart asks the child to tear down gracefully
(terminal restored), then starts a fresh one in the same session.
fleury run starts the same supervisor from a launcher that never loads your
app, so the app compiles once, even when the launcher itself is a native
executable. Under a plain dart run, your own entrypoint becomes the
supervisor; Keep startup work inside the app
covers what that means for main().
When the child exits for real — quit, Ctrl+C, a crash — the supervisor
mirrors its exit code, so scripts and CI see exactly what they’d see without
it.
Troubleshooting
Section titled “Troubleshooting”Nothing happens when I save — Check you’re on macOS or Linux, on a real
terminal (not a pipe), and that FLEURY_HOT_RELOAD isn’t 0. Your entrypoint
also has to sit in a package with a resolved .dart_tool/package_config.json
(dart pub get) — that file is what says which sources to watch, and with
nothing to watch the supervisor steps aside and the run is an ordinary one. In
an editor debug session, reload-on-save is the editor’s job: run Dart: Hot
Reload from the command palette, or set dart.hotReloadOnSave: "allIfDirty"
(generated projects have it already). To see what the supervisor does, set
FLEURY_DEV_BOOTSTRAP_LOG to a file path: it logs the directories it watches,
each save it sees, and each reload, and under fleury run, why it didn’t
start.
F5 doesn’t restart — See When F5 doesn’t restart.
The compiler rejected my save — Fix the reported source error and save again; the running app keeps its previous code and state. Use hot restart for valid source the VM cannot migrate, or to reset state deliberately.
A reload succeeds but nothing changes — The edit is in code that already
ran: main(), a top-level initializer, or initState. Reload doesn’t run it
again. Override reassemble to recompute, or hot restart.
The app exits right away under dart run, but not with FLEURY_HOT_RELOAD=0
— Startup work before runApp ran twice, and the app process found the port,
lock, or stdin already taken (see
Keep startup work inside the app). An app
that reads its arguments can also fail here when runApp doesn’t receive
args:. Use fleury run, or fix the entrypoint.
A new field throws type 'Null' is not a subtype of type … after reload —
Existing objects never ran the constructor that sets the new field. Give it an
initializer where it’s declared, or hot restart. A non-nullable field with
neither is a compile error, and the reload reports it.
Reload succeeded but the tree looks wrong — Some edits apply but can’t
migrate a running tree cleanly (a StatefulWidget becoming stateless, a
removed field still referenced). Hot restart.