Hot Reload Not Working: When to Restart
Press R for hot restart instead: hot reload skips main(), static initializers, and enum changes.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓A running Flutter app via flutter run
- ✓Basic StatefulWidget and
main()knowledge - ✓An IDE with hot reload on save or terminal access
- Hot reload injects code into the running VM and rebuilds widgets while keeping state, but it never reruns main() or initState()
- Edits to global and static initializers, main(), enum-to-class swaps, and generic type changes all require a hot restart or full rebuild
- A paused debugger, breakpoint hold, or compile error silently blocks reload — check the console for Reloaded N libraries first
- Native Kotlin, Swift, or new-asset changes need a full stop and restart; match every edit to r, R, or rebuild before assuming breakage
Picture redecorating a house while the family still lives in it. Hot reload is like swapping the curtains and repainting walls — everyone stays put and life goes on. But some changes need the family to step outside: you cannot replace the foundation or rewire the mains with people inside. Edits to main(), global settings, and type definitions are foundation work. They need a hot restart, where the family steps out and walks back into a fresh house that still looks familiar.
You change a color, hit save, and nothing happens. You change it again, harder, and still nothing. Hot reload — the feature that sold you on Flutter — suddenly feels broken, and you restart the whole app losing five minutes of navigation state just to see a padding tweak. Every Flutter developer hits this wall, usually right after editing exactly the kind of code hot reload cannot touch.
Hot reload is stateful surgery on a running Dart VM: new code is injected, the widget tree rebuilds, and your navigation stack, counters, and form input survive. That miracle has boundaries. Anything the VM treats as load-time state — main(), static initializers, type declarations — is stitched in at startup and cannot be re-stitched mid-flight. The tool does not always shout about it; sometimes the change just silently does nothing.
This guide maps the boundaries exactly. You will learn what reload preserves and why, which five edit categories demand a restart, how debuggers and compile errors fake a broken reload, and the r-versus-R-versus-rebuild ladder for every situation. By the end you will stop fighting the tool and start matching each edit to its correct refresh — saving the state you want and rebuilding only when the runtime truly requires it.
What Hot Reload Preserves and Why
Hot reload is two operations fused into one keystroke. First the Dart VM injects updated libraries, swapping method bodies and adding new classes while keeping every existing object alive. Then Flutter reassembles the widget tree: each Element rebuilds with the new widget configuration, but State objects, controllers, and the navigation stack carry over untouched. Your form input survives, your scroll position holds, and the counter stays at 47 — only the pixels change.
This preservation is the entire point and the source of every limitation. The VM cannot both keep your runtime state and re-run the code that created it, so it keeps the state and skips the creators. Anything initialized once at load — statics, globals, the main() invocation — stays exactly as it was when the session started. Expecting reload to refresh those is expecting it to be a restart wearing a reload costume.
Watch the console line to build intuition. Reloaded 1 of 697 libraries in 1,006ms breaks the cycle into compile, reload, and reassemble phases with timings. Fast cycles under a second mean healthy injection; missing lines mean the save never triggered; error lines mean broken code blocked everything. The line is your reload vital sign — check it before blaming the framework.
Edits That Silently Need a Restart
Five edit categories never visibly apply on reload, and the quiet ones hurt most. Code inside main() never re-executes, so new root widgets, changed initial routes, and updated launch copy sit dormant until restart. Global and static initializers never re-run because the VM treats them as live state — change a static theme table and the old values keep painting. These two cover the vast majority of my-reload-does-nothing reports.
Structural type edits are rejected outright rather than ignored. Converting an enum to a class (or the reverse) and changing a class's generic type parameters both fail the VM's reload safety checks, producing a diagnostic while the app keeps running old code. Native changes — Kotlin, Swift, Gradle files, Info.plist — live outside the VM entirely and need a full stop-and-run cycle, as do newly added assets whose manifests are built at compile time.
Memorize the response, not just the list. Successful reload plus stale screen means your edit is load-time state: press R for hot restart immediately instead of saving repeatedly. Rejected reload with a diagnostic means structural change: restart and keep going. Full rebuild is reserved for native and asset edits. Matching the symptom to the level in one step is the whole skill.
main(), statics, or types. Press R for hot restart instead of saving the same file again.When the Debugger Holds Reload Hostage
A paused isolate cannot accept injected code, so breakpoints silently veto reload. You hit a breakpoint, edit a file while studying variables, save out of habit, and nothing happens — the VM queues nothing while paused. Worse, stepping through frames while a reload is pending can produce confusing mixed states where some frames run old code. Resume or terminate the pause before judging reload health.
Hot reload on save adds its own failure mode. If your IDE saves all files on every keystroke pause, half-typed code triggers reload attempts that fail compilation and print errors you never asked for. Developers then conclude reload is flaky when it is actually dutifully rejecting broken intermediate states. Tune save behavior so reload fires on deliberate saves, and read the error it prints instead of dismissing it as noise.
SDK drift completes the trio. Two engineers on different Flutter channels can see different reload behavior on identical code, especially around newly supported reload cases. When reload works for everyone but you, compare flutter --version first and breakpoint state second — the answer is environmental far more often than it is a framework bug. Log the SDK version in team runbooks so drift is visible before it bites.
The r Versus R Versus Rebuild Ladder
The flutter run terminal offers two keys and the distinction matters. Lowercase r performs hot reload: inject code, rebuild widgets, keep every scrap of state. Uppercase R performs hot restart: inject code, rerun main(), wipe navigation and fields. IDE toolbar buttons mirror them — the lightning bolt reloads, the circular arrow restarts. Reaching for the right key first is worth minutes per cycle across a working day.
Default to r and escalate on evidence. Save, watch for the Reloaded line, and check the screen: change visible means done in under a second with state intact. Successful reload with a stale screen means load-time state changed — press R once and verify. R is cheap enough to use freely; it only costs your current navigation stack, which you can rebuild in seconds during development.
Reserve full stop-and-run for the native and asset tier. Kotlin, Swift, Gradle, manifests, and brand-new assets require a fresh build because they live outside the reloadable universe. Rebuilding for Dart-only edits is pure waste — minutes lost plus session state destroyed for freshness you already had. Teams that internalize the ladder stop losing demo state to unnecessary rebuilds. Tape the ladder to your monitor until it becomes reflex.
Structuring Code So Everyday Edits Reload
The deepest fix is architectural: keep volatile values in reloadable territory. Launch copy, feature flags, theme colors, and copy decks belong in widget build methods or instance configuration — not in main() or static tables. When the values designers tweak hourly live in build paths, every tweak is a sub-second reload; when they live in load-time state, every tweak is a restart plus lost navigation.
Prefer instance state over static state for screen-scoped data. A static cache shared across screens feels convenient until its initializer needs changing mid-session — suddenly every tweak costs a restart. Instance fields created in initState reset naturally on restart anyway and read clearly, while statics secretly persist across both reload and restart-to-same-session confusion. Save statics for genuinely global, rarely changed constants.
Review code placement with reload in mind. If a pull request adds a static table of user-facing strings, ask whether those strings will be tweaked during development — they will — and suggest a widget-owned source instead. Reload-friendly placement is a code-review criterion that pays back every day, unlike most style nits that pay back never. Reload-aware reviews compound daily across the whole team.
Proving Your Reload Workflow Is Healthy
Validate the pipeline deliberately instead of trusting vibes. Make a trivial build-method edit — change a Text string — save, and confirm both the Reloaded line and the visible change within seconds. Then change a static initializer, save, confirm the screen correctly stays stale, and press R to watch it apply. This two-minute drill proves you can distinguish healthy boundaries from real breakage on your machine, SDK, and device.
Fold the drill into onboarding and pairing. New hires who perform it once stop filing reload-is-broken tickets forever, and pairing sessions avoid the ritual of both engineers re-saving the same file in disbelief. Document your project's reload-hostile zones — generated files, static registries, native bridges — in the readme so nobody rediscovers them under deadline.
Finally, keep the toolchain current on a schedule. Reload support improves with SDK releases, and teams pinned to old channels miss fixes for entire edit categories. A quarterly flutter upgrade plus the ladder habit keeps the fastest feedback loop in mobile development running at full speed. The fastest feedback loop in mobile development deserves five minutes of deliberate practice and a permanent runbook entry.
Demo Day Froze When main() Edits Would Not Reload
main(), hit save five times, and watched the console print successful reloads with zero visible change. With four minutes left they killed the session and ran a full rebuild, which took three minutes on the venue's throttled network while the host stalled. The demo started 90 seconds late with a visibly rattled presenter, and the team spent the next sprint answering why Flutter's flagship feature had failed on stage.main() copy and a static initializer — belonged to categories reload never applies. The tool had worked exactly as designed both times.main() is never re-executed, and static field initializers never re-run. Each save produced a genuinely successful reload that rebuilt widgets around unchanged state, so the console looked healthy while the screen stayed stale. Nobody on the team knew the five no-reload categories, so success messages read as lies instead of hints.main() into a widget build method and converted the static theme table into instance configuration, so future copy tweaks reload normally. A lunch-and-learn walked all twelve mobile engineers through the reload boundary with live examples.- Move frequently edited values out of
main()and static initializers into widget build paths. Copy, colors, and flags that live in reloadable code stay demo-friendly; the same values in load-time state will betray you on stage. - Read the console line, not just the screen. A successful Reloaded N libraries message next to a stale screen means your edit is in a no-reload category — press R instead of saving harder.
- Rehearse the failure mode before it matters. A thirty-minute pre-demo clean rebuild plus a runbook entry for r versus R costs almost nothing and would have saved this team's launch slot.
main() body, global or static initializers, enum-to-class swaps, generic type changes, and native code all need more than reload. Press R in the terminal for a hot restart, which reruns main() and freshens static state while keeping the debug session. If the change appears after R, your edit was in a no-reload category — move on, nothing is broken.main() or statics so everyday tweaks stay reloadable by construction.| File | Command / Code | Purpose |
|---|---|---|
| io | void main() => runApp(const CounterApp()); | What Hot Reload Preserves and Why |
| io | const String launchCopy = 'Welcome back'; // const edits reload fine | Edits That Silently Need a Restart |
| io | class StatusCard extends StatelessWidget { | When the Debugger Holds Reload Hostage |
| io | void main() => runApp(const LadderDemo()); | The r Versus R Versus Rebuild Ladder |
| io | class LaunchConfig extends InheritedWidget { | Structuring Code So Everyday Edits Reload |
Key takeaways
main(), initState, or statics.Common mistakes to avoid
5 patternsRe-saving the same file instead of pressing R
Keeping daily-tweaked copy in main() or statics
Editing code while paused at a breakpoint
Full-rebuilding for Dart-only changes
Ignoring the console reload line
Interview Questions on This Topic
What does hot reload preserve and what does it rerun?
main(), initState(), or static initializers — those are load-time state.Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Flutter. Mark it forged?
5 min read · try the examples if you haven't