← Back to blog
Flutter Fundamentals · Part 7 of 9
July 21, 202610 min read

Hot Reload vs Hot Restart in Flutter: What Actually Happens Under the Hood

FlutterDart

Hot Reload vs Hot Restart

This is Part 7 of the Flutter Fundamentals series. Hot reload is the feature that makes Flutter feel magical — change code, hit save, see it in under a second with your app's state intact. But that magic causes confusion the moment something doesn't update ("I changed the color and nothing happened!"). The cure is understanding what hot reload actually does — and where its limits are.

Happily, you already have the two prerequisites from earlier parts: the three trees (State lives on the persistent Element) and the Dart dual-compilation model (JIT in dev). Let's connect them.


Three operations, not two

There are actually three ways to apply changes, in increasing heaviness:

| Operation | App State | main() / initState() re-run? | Speed | When you need it | | --- | --- | --- | --- | --- | | Hot reload | ✅ preserved | ❌ no | fastest (~1s) | UI tweaks, build changes, business logic | | Hot restart | ❌ reset | ✅ yes | medium (~seconds) | changed main/initState/static fields, want fresh state | | Full restart | ❌ reset | ✅ yes | slowest (rebuild) | native code (Kotlin/Swift), new assets/deps, plugins |

Knowing which to use — and why a change sometimes silently "doesn't take" — is the practical payoff of this part.


What hot reload actually does

When you hot reload, here's the real sequence (per the official docs):

  1. Scan for changes. The tooling on your machine looks at which Dart source you've edited since the last compile.
  2. Recompile to kernel. The changed libraries (plus main and anything affected) are recompiled into intermediate kernel files.
  3. Inject into the running VM. Those kernel files are sent to the Dart VM already running your app on the device, which reloads the affected libraries — without stopping the app. (This is only possible because dev builds use the JIT VM — Part 1.)
  4. Rebuild the widget tree. Flutter triggers a rebuild/re-layout/repaint of the entire widget tree, so your changed build methods run and the new UI appears.

The console message you've seen:

Performing hot reload...
Reloaded 1 of 448 libraries in 978ms.

Notice step 4: hot reload re-runs build, but does not re-run main() or initState(). The app keeps running; only the code is swapped and the tree rebuilt.


Why your State survives — and why initState doesn't re-run

This is where Part 2 pays off. Hot reload keeps the app's objects alive — including the Element tree and the State objects it holds. It just gives them updated code and rebuilds. So:

  • Your counter value, the route you're on, text typed into a field, a half-scrolled list — all preserved, because that data lives on the persistent Elements/States, which aren't destroyed.
  • build re-runs (new code), so UI/layout changes appear.
  • initState does not re-run, because the State objects already exist — they were never recreated. (initState only runs when a State is first created, Part 3.)
class _DemoState extends State<Demo> {
  int _count = 0;

  @override
  void initState() {
    super.initState();
    _count = 42; // ← change this to 99 and hot reload: STILL shows the old value
  }

  @override
  Widget build(BuildContext context) {
    return Text('$_count'); // ← change this text/style and hot reload: updates instantly
  }
}

Edit the Text styling → hot reload shows it immediately (build re-ran). Change _count = 42 to 99 in initState → hot reload won't show 99, because initState doesn't re-run and the existing State already holds the old value. To see it, hot restart (which recreates everything and re-runs initState).

This is the #1 hot-reload "gotcha": changes to initState, field initializers, or anything that runs only at startup won't appear on hot reload. It's not a bug — it's the direct consequence of State being preserved.


Hot restart: throw it all away and start over

Hot restart destroys the running app's state and Dart objects, then restarts the app from main() — but it's still faster than a full rebuild because it reuses the already-compiled engine and reloads only Dart. After a hot restart:

  • All in-memory state is gone (counters reset, you're back on the first screen, fields cleared).
  • main() runs again, every State is recreated, so initState runs again.
  • Static/global field initializers run again.

Use it when you want a clean slate, or when your change is one hot reload can't apply (below).


What requires a hot restart (and why)

Several categories of change can't be hot reloaded, all for the same underlying reason: the changed code only runs at startup or changes the shape of types, so swapping the code without re-running startup wouldn't reflect it.

1. Changes to main() or initState()

These run once at launch. Hot reload doesn't re-run them, so edits won't show until a restart.

2. Global and static field initializers

In Dart, static fields are lazily initialized once on first access. Hot reload won't re-initialize them. The docs' example:

// Editing one of these after first access won't show on hot reload,
// because the list was already initialized once.
final sampleTable = [
  Table(children: const [TableRow(children: [Text('T1')])]),
  // ...
];

Workarounds: make it const (so it's a compile-time value the reload can re-evaluate) or convert it to a getter (List<Table> get sampleTable => [...]) so it's recomputed on each access.

3. Changing an enum to/from a regular class

// Before
enum Color { red, green, blue }
// After → hot restart required
class Color { Color(this.i); final int i; }

This changes the type's shape, which the running VM can't reconcile via reload.

4. Modifying generic type parameters

// Before
class A<T> { T? i; }
// After → hot restart required
class A<T, V> { T? i; V? v; }

Again, the type's structure changed.

5. Native code changes → full restart

Editing Kotlin/Java/Swift/Objective-C, adding a plugin, or changing native dependencies requires recompiling the native layer — neither hot reload nor hot restart touches native code, so you need a full flutter run (stop and relaunch).

6. New assets / pubspec.yaml changes

Adding fonts, images, or dependencies generally needs at least a restart (and sometimes a full rebuild) so the bundle/registration is regenerated.

The one-line heuristic: if your change only affects what build produces, hot reload shows it. If it affects startup code, static initializers, or the shape of a type, you need hot restart. If it touches native code/assets/deps, you need a full restart.


The "stale UI upstream of rebuild" subtlety

Even a successful hot reload only shows changes in code that actually re-executes during the rebuild. The docs put it well:

If the modified code won't be re-executed as a result of rebuilding the widget tree, then you won't see its effects after hot reload.

For example, changing imports used only in main, or logic gated behind a one-time initState branch, won't reflect. When in doubt and a change should be visible but isn't — hot restart to rule out the State-preservation effect.


Practical workflow tips

  • Reload on save: enable it in your IDE (VS Code: "dart.flutterHotReloadOnSave": "all"; Android Studio: Flutter → Perform hot reload on save) so iteration is automatic.
  • Reach for hot reload first (it's the default and fastest), restart when a change doesn't appear.
  • When debugging weird preserved state (a controller initialized with old config, a one-time fetch), hot restart to get a clean initState run.
  • Remember the async-series lesson: state preserved across reloads can hide initState-time bugs — a restart re-runs your setup honestly.

Practice Challenges

Challenge 1 — Reload or restart? You change the text inside a build method's Text(...). Which do you need?

Show solution

Hot reload. It re-runs build, so the new text appears instantly with state preserved.

Challenge 2 — Why didn't it change? You change a value assigned in initState and hot reload, but the UI shows the old value. Explain and fix.

Show solution

Hot reload preserves State and does not re-run initState (the State object already exists), so the new initialization never runs. Hot restart to recreate the State and re-run initState, showing the new value.

Challenge 3 — Categorize. For each, pick hot reload / hot restart / full restart: (a) tweak a button color; (b) change main(); (c) edit a Kotlin platform-channel method; (d) convert an enum to a class.

Show solution

(a) hot reload (build change); (b) hot restart (main runs only at startup); (c) full restart (native code recompile); (d) hot restart (the type's shape changed).

Challenge 4 — Static field gotcha. A final myList = [...] at top level doesn't reflect your edits on hot reload. Give two fixes.

Show solution

Static/global fields are lazily initialized once, so hot reload won't re-init them. Fix by making it const (a compile-time constant the reload can re-evaluate) or converting it to a getter (List get myList => [...]) so it's recomputed on each access. (Or hot restart.)


Questions to test yourself

Q1 (basic). In one line, what's the difference between hot reload and hot restart?

Show answer

Hot reload injects updated code into the running app and rebuilds the widget tree while preserving state (doesn't re-run main/initState). Hot restart resets all state and restarts the app from main() (re-running initState), but still reuses the engine so it's faster than a full rebuild.

Q2 (basic). Does hot reload re-run main() and initState()?

Show answer

No. Hot reload re-runs build (so UI changes appear) but does not re-run main() or initState(), because the app keeps running and existing State objects are preserved rather than recreated.

Q3 (intermediate). Why is your app's state preserved across a hot reload? (Tie it to the three trees.)

Show answer

Because hot reload keeps the running app's objects alive — including the persistent Element tree and the State objects it holds (Part 2). It only swaps in new code and rebuilds the widget tree. Since State lives on the durable Element (not the disposable widget), it isn't destroyed, so values survive.

Q4 (intermediate). A change to a value set in initState doesn't appear after hot reload. Why, and what's the fix?

Show answer

initState runs only when a State is first created. Hot reload preserves existing State objects, so it never re-runs initState, and the new initialization is skipped. Hot restart recreates the State and re-runs initState, showing the change.

Q5 (intermediate). Name two categories of change that require a hot restart (not reload), and the one that requires a full restart.

Show answer

Hot restart: changes to main()/initState(), global/static field initializers, converting between enum and class, or modifying generic type parameters — all things that run at startup or change a type's shape. Full restart: native code changes (Kotlin/Java/Swift/Obj-C), added plugins, or new assets/dependencies, since those need recompilation of the native layer or bundle.

Q6 (advanced). Hot reload technically relies on a property of Flutter's development build. What is it, and why does production not have hot reload?

Show answer

Development builds run on the JIT Dart VM, which can load new code into the already-running program — that's what lets hot reload inject recompiled libraries without stopping the app. Production builds are compiled AOT to native machine code (for fast startup and native speed), and AOT code can't have new source injected at runtime, so there's no hot reload in release builds. (This is the JIT-dev / AOT-release split from Part 1.)


Wrapping up

Hot reload stops being mysterious once you see the machinery:

  • It recompiles changed libraries to kernel, injects them into the running JIT VM, and rebuilds the widget tree — re-running build but not main/initState.
  • State survives because it lives on the persistent Elements (Part 2), which hot reload keeps alive.
  • Hot restart wipes state and re-runs main/initState (for startup/static/type-shape changes); full restart is for native code/assets/deps.
  • Heuristic: build-only change → reload; startup/static/type change → restart; native/assets → full restart.
  • Production has no hot reload because release builds are AOT-compiled.

You can now build screens and iterate on them fast. The last skill of a real app is moving between screens. In Part 8 we cover Navigation basics — Navigator 1.0, push, pop, and named routes.