Home › Mobile › Hot Reload Not Working: When to Restart
Beginner 5 min · September 23, 2026

Hot Reload Not Working: When to Restart

Press R for hot restart instead: hot reload skips main(), static initializers, and enum changes.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 9 min
  • ✓A running Flutter app via flutter run
  • ✓Basic StatefulWidget and main() knowledge
  • ✓An IDE with hot reload on save or terminal access
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Hot Reload Not Working?

Hot reload works by injecting updated Dart source into the running VM, then asking Flutter to reassemble the widget tree. The framework rebuilds every widget with the new build methods while preserving the existing State objects, navigation stack, and field values.

★
Picture redecorating a house while the family still lives in it.

That is why a counter stays at 47 after you restyle its button: the State survived, only the description of the UI changed. The console confirms each cycle with Reloaded N of M libraries plus compile, reload, and reassemble timings.

Hot restart goes one step further: it injects the code and then restarts the app from main(), wiping all state. Counters reset, navigation returns to the initial route, and static initializers run fresh. A full restart (stop plus run) rebuilds everything including native code and asset manifests.

The three levels form a ladder of increasing freshness and increasing cost: reload keeps state in under a second, restart loses state in seconds, rebuild loses the session and takes minutes.

The boundary follows from how Dart initializes state. Static fields and globals initialize lazily on first read and the VM treats them as running state, so reload deliberately leaves them alone — reinitializing them would be a restart by another name.

Type declarations like enums and generics shape the program's structure at load time, so changing them is rejected outright. Native code lives outside the VM entirely and no Dart-side injection can reach it. Once you see reload as state-preserving surgery, every limitation reads as a consequence rather than a quirk.

Plain-English First

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.

io/thecodeforge/flutter/reloadable_counter.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'package:flutter/material.dart';

// Restyling this build method applies on hot reload: state survives.
void main() => runApp(const CounterApp());

class CounterApp extends StatefulWidget {
  const CounterApp({super.key});

  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _count = 0; // preserved across reloads by design

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $_count')),
        floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() => _count++),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
📊 Production Insight
A junior spent a morning convinced reload was broken before a senior pointed at the console: every save had errored on a missing bracket, so nothing ever injected.
🎯 Key Takeaway
Reload injects code and rebuilds widgets while preserving State — read the console line to confirm each cycle actually ran.

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.

io/thecodeforge/flutter/restart_needed.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'package:flutter/material.dart';

// Editing main() or static initializers needs hot restart (R), not reload.
const String launchCopy = 'Welcome back'; // const edits reload fine
final List<String> perks = <String>['fast', 'fresh']; // final initializer: restart to refresh

void main() {
  // Changing this Text needs R: main() never re-executes on reload.
  runApp(const MaterialApp(home: Scaffold(body: Center(child: Text(launchCopy)))));
}

class ThemeBox {
  static final Map<String, int> tones = <String, int>{'brand': 0xFF1A73E8};
  // Changing tones above needs R: static state is preserved on reload.
}
⚠ A Successful Reload Can Still Show Stale UI
Reloaded N libraries with no screen change means your edit is load-time state — main(), statics, or types. Press R for hot restart instead of saving the same file again.
📊 Production Insight
A team re-saved a static config file eleven times during an incident call before someone pressed R and watched the fix appear instantly.
🎯 Key Takeaway
main(), static initializers, enum and generic restructures, and native edits all need restart or rebuild — reload cannot reach them.

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.

io/thecodeforge/flutter/debug_safe_edit.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'package:flutter/material.dart';

// Keep edits inside build methods while debugging: they reload cleanly
// even around breakpoint pauses once you resume the isolate.
class StatusCard extends StatelessWidget {
  const StatusCard({super.key, required this.online});

  final bool online;

  @override
  Widget build(BuildContext context) {
    // Tweaking colors or copy here applies on the next save + reload.
    return Card(
      color: online ? Colors.green.shade100 : Colors.red.shade100,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(online ? 'Online' : 'Offline'),
      ),
    );
  }
}
📊 Production Insight
A developer debugged a breakpoint for twenty minutes while accusing reload of breakage — the isolate had been paused the entire time and resumed perfectly.
🎯 Key Takeaway
Resume paused isolates before testing reload, save deliberately, and align flutter --version across the team.

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.

io/thecodeforge/flutter/restart_ladder.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'package:flutter/material.dart';

// Ladder drill: tweak build output (r), change _seed (R), add native code (rebuild).
void main() => runApp(const LadderDemo());

class LadderDemo extends StatefulWidget {
  const LadderDemo({super.key});

  @override
  State<LadderDemo> createState() => _LadderDemoState();
}

class _LadderDemoState extends State<LadderDemo> {
  static int _seed = 7; // changing this needs R: static state persists
  int _taps = 0; // r keeps this; R resets it to 0

  @override
  Widget build(BuildContext context) {
    // Changing this Text needs only r: build methods always re-run.
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('seed $_seed taps $_taps')),
        floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() => _taps++),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
📊 Production Insight
A team timed their habits: engineers who defaulted to full rebuilds lost forty minutes daily versus teammates who climbed r-then-R and rebuilt only for native edits.
🎯 Key Takeaway
Save and r first, escalate to R on stale screens, and rebuild only for native code or new assets.

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.

io/thecodeforge/flutter/reload_friendly_config.dartDART
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import 'package:flutter/material.dart';

// Reload-friendly: copy lives in the widget tree, so tweaks apply on save.
class LaunchConfig extends InheritedWidget {
  const LaunchConfig({super.key, required this.copy, required super.child});

  final String copy;

  static LaunchConfig of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<LaunchConfig>()!;
  }

  @override
  bool updateShouldNotify(LaunchConfig old) => copy != old.copy;
}

class LaunchScreen extends StatelessWidget {
  const LaunchScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // Editing this fallback copy below reloads instantly.
    final String copy = LaunchConfig.of(context).copy;
    return Scaffold(body: Center(child: Text(copy)));
  }
}
📊 Production Insight
A startup moved its onboarding copy from static tables into widgets and cut design-iteration cycle time from minutes to seconds across a three-week polish sprint.
🎯 Key Takeaway
Keep volatile copy and config in widget build paths so daily tweaks stay in reloadable territory.

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.

📊 Production Insight
A team added the two-minute reload drill to onboarding and watched reload-is-broken support threads drop from weekly to zero within a month.
🎯 Key Takeaway
Drill reload versus restart on purpose, document reload-hostile zones, and upgrade the SDK quarterly.
● Production incidentPOST-MORTEMseverity: high

Demo Day Froze When main() Edits Would Not Reload

Symptom
Ten minutes before a live demo to 200 attendees, the launch screen still showed placeholder text. The engineer had updated the copy inside 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.
Assumption
The team assumed hot reload was broken by the venue's network because the rebuild was unusually slow there. They filed an internal report blaming conference Wi-Fi and moved on. A week later the same symptom appeared on office fiber: an engineer edited a static theme table, reloaded with no effect, and restarted the app in frustration. Only then did someone read the reload documentation and realize both edits — main() copy and a static initializer — belonged to categories reload never applies. The tool had worked exactly as designed both times.
Root cause
Both edits touched load-time state the VM preserves across reloads: 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.
Fix
The immediate fix was process: the team's runbook now lists which edits need R (hot restart) versus r (reload), and demo-day checklists require a full clean rebuild thirty minutes before stage time. The code fix moved launch copy out of 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.
Key lesson
  • 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.
Production debug guideFive steps from a stale screen to the right refresh for your edit.5 entries
Symptom · 01
You save, the screen does not change, and you are unsure reload even ran
→
Fix
Look at the debug console for the Reloaded N of M libraries line with compile, reload, and reassemble timings. If it is missing, reload never triggered: check that hot reload on save is enabled in your IDE, or press r in the flutter run terminal to reload manually. If the line shows a compile error instead, fix the error first — broken code never injects.
Symptom · 02
Reload succeeds in the console but the screen stays stale
→
Fix
Classify your edit: 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.
Symptom · 03
Reload works for teammates but not for you on the same branch
→
Fix
Check for a paused debugger: a breakpoint hold or a paused isolate silently blocks injection until you resume. Open DevTools, confirm the isolate is running rather than paused, and resume or detach. Also compare flutter --version output — mismatched SDKs across machines produce different reload behavior on identical code.
Symptom · 04
You edited Kotlin, Swift, Gradle, or added new assets and reload does nothing
→
Fix
Stop the session entirely and rebuild: native code and asset manifests live outside the Dart VM, so neither reload nor hot restart can reach them. Run the app fresh with flutter run and confirm the change. Reserve full rebuilds for this category only — reaching for them on Dart edits wastes the state reload exists to preserve.
Symptom · 05
You want a workflow where this never surprises you again
→
Fix
Adopt the ladder habit: save for reload first, glance at the console line, press R the moment a successful reload shows no change, and rebuild only for native or asset edits. Keep launch copy and theme values in widget code rather than main() or statics so everyday tweaks stay reloadable by construction.
Hot Reload Failures at a Glance
Root CauseHow to ConfirmFixPrevention
Edit inside main() or static initializersConsole shows successful reload but screen unchangedPress R for hot restart to rerun load-time stateKeep volatile copy and config in widget build paths
Enum, generic, or type restructureReload rejected with a VM diagnostic messageRestart and continue; structure cannot injectBatch structural edits, then restart once deliberately
Paused debugger or breakpoint holdIsolate shows paused in DevToolsResume or detach, then reload againResume before judging reload; save deliberately
Native code or new assets changedEdits outside lib/ with no Dart errorFull stop and flutter run rebuildRebuild only for this tier, never for Dart-only tweaks
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
iothecodeforgeflutterreloadable_counter.dartvoid main() => runApp(const CounterApp());What Hot Reload Preserves and Why
iothecodeforgeflutterrestart_needed.dartconst String launchCopy = 'Welcome back'; // const edits reload fineEdits That Silently Need a Restart
iothecodeforgeflutterdebug_safe_edit.dartclass StatusCard extends StatelessWidget {When the Debugger Holds Reload Hostage
iothecodeforgeflutterrestart_ladder.dartvoid main() => runApp(const LadderDemo());The r Versus R Versus Rebuild Ladder
iothecodeforgeflutterreload_friendly_config.dartclass LaunchConfig extends InheritedWidget {Structuring Code So Everyday Edits Reload

Key takeaways

1
Reload preserves State and rebuilds widgets; it never reruns main(), initState, or statics.
2
Successful reload plus stale screen is the restart signal
press R once, don't re-save.
3
Enum, generic, and type restructures are rejected; batch them and restart deliberately.
4
Resume paused debuggers before judging reload, and read every console reload line.
5
Rebuild fully only for native code, manifests, or new assets outside the VM.
6
Keep volatile copy in widget build paths so daily tweaks stay reloadable.

Common mistakes to avoid

5 patterns
×

Re-saving the same file instead of pressing R

Symptom
Five successful reloads with a stubbornly stale screen, growing frustration, and eventually a full rebuild that destroys useful state.
Fix
Treat successful-reload-plus-stale-screen as the restart signal. Press R once immediately instead of saving harder.
×

Keeping daily-tweaked copy in main() or statics

Symptom
Every copy tweak costs a restart and lost navigation, slowing design iteration from seconds to minutes all sprint long.
Fix
Move volatile strings, colors, and flags into widget build methods or inherited configuration.
×

Editing code while paused at a breakpoint

Symptom
Saves do nothing, stepping shows mixed old and new behavior, and reload gets blamed for what the paused isolate caused.
Fix
Resume or finish the debug pause first, then save and reload. Never evaluate reload health while paused.
×

Full-rebuilding for Dart-only changes

Symptom
Minutes lost per cycle plus destroyed session state, multiplied across every engineer every day.
Fix
Climb the ladder: r first, R on stale screens, rebuild only for native code or new assets.
×

Ignoring the console reload line

Symptom
Compile errors and rejected reloads go unread while the team debates whether the tool or the code is at fault.
Fix
Read every Reloaded line: missing means no trigger, error means broken code, success-plus-stale means restart-tier edit.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does hot reload preserve and what does it rerun?
Q02JUNIOR
When do you press R instead of r?
Q03SENIOR
Which edits require a full rebuild instead of restart?
Q04SENIOR
Why do enum-to-class changes fail reload?
Q05SENIOR
How would you structure an app so copy tweaks always reload?
Q01 of 05JUNIOR

What does hot reload preserve and what does it rerun?

ANSWER
It injects new code and rebuilds widgets while preserving State objects, navigation, and field values. It never reruns main(), initState(), or static initializers — those are load-time state.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is hot reload broken if nothing changes on save?
02
Does hot reload run initState again?
03
Why do static variables keep old values after reload?
04
Can I reload while paused at a breakpoint?
05
Do I need rebuilds for pubspec dependency changes?
06
How fast should a reload cycle be?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Flutter. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Gradle Build Failed: Fix the Minimum Version
4 / 7 · Flutter
Next
LateInitializationError: Assign Before You Read
→