Skip to content

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.

You write · editable
Widget example() => const HotReloadNotes(heading: 'Notes');
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

From your project, run:

Terminal window
fleury run

fleury 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.

Terminal window
fleury run --enable-asserts bin/run_app.dart --profile=local

A 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.

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 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.

  • On a Mac laptop, the top-row F5 needs Fn unless the keyboard is set to use F1, F2, and so on as standard function keys.

  • In VS Code’s integrated terminal, VS Code takes F5 before the app sees it: its default terminal.integrated.commandsToSkipShell includes workbench.action.debug.start and workbench.action.debug.continue. F5 then starts a debug session (in a fleury create project, a second copy of the app) instead of restarting this one. To send F5 to 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"
    ]
    }

    F5 still starts debugging from the editor. Otherwise, quit with Ctrl+C and run fleury run again, or run the app in another terminal.

  • Under the VS Code debugger (the F5 launch 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.

  • Every field on your State objects (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 Animation settles at its current target so no stale completion is left pending; a FrameTicker resets its phase and re-anchors its clock.
  • Anything computed in main() before runApp, and top-level globals initialized at startup.
  • Work done in initState for 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.

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
}
}

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.

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.

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.)

Because the supervisor steps aside for a serve handle, the usual browser command hot reloads nothing:

Terminal window
fleury serve --spawn dart run bin/run_app.dart # no VM service, no reload

Enable the service in the spawned command itself and the app reloads on save, with the browser preview updating live:

Terminal window
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.

Terminal window
FLEURY_HOT_RELOAD=0 fleury run

or runApp(enableHotReload: false) — the right setting for production launches, where it also skips the service-extension registration.

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.

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.