
Mar 9, 2026
The Phoenix Pattern in Flutter: How to Restart Your App Without Restarting Your App
Last updated: March 2026. Tested with Flutter 3.27+ and Dart 3.6+.
This post assumes familiarity with Flutter's widget tree, StatefulWidget, and the concept of Keys. If you're new to keys, start with the Flutter docs on keys.
The Phoenix pattern is a technique for programmatically restarting a Flutter application by rebuilding the entire widget tree from scratch using Flutter's key-based reconciliation. It tears down all widgets, disposes all state, and recreates everything — without killing the OS process. The pattern requires ~30 lines of code and no external dependencies.
TL;DR: Wrap your app in a StatefulWidget that holds a UniqueKey. Change the key, and Flutter destroys and rebuilds every widget below it. All widget state is gone. All providers are recreated. But static variables, singletons, and .env values survive — because the Dart isolate stays alive. Know this boundary before choosing Phoenix over a true process restart.
Why This Exists
You need to restart your Flutter app programmatically. The user logged out and you want a clean slate. An initialization step failed and you want to retry. A configuration changed that affects the entire widget tree.
As of March 2026, Flutter has no built-in restartApp() API. There's an open feature request (issue #127409), but it hasn't been implemented. In the discussion, Flutter contributor @feinstein proposed wrapping runApp() with a GlobalKey swap — essentially baking the Phoenix pattern into the framework — but noted this would only be a "soft restart" that resets widget state, not a "hard restart" that restarts the Dart engine. Static globals would survive either way.
The Alternatives and Their Problems
Method | Platform Support | Restart Level | Drawback |
|---|---|---|---|
| Android + iOS | OS process kill | Apple discourages this — looks like a crash, risks App Store rejection |
| Android only | App close | |
| Android + iOS | Native restart | On iOS, exits the app and sends a local notification to reopen. Requires notification permissions. |
| Android + iOS | Calls | Re-invokes Dart's |
Phoenix pattern | Android + iOS | Widget tree rebuild | In-process only — Dart isolate stays alive |
How the Phoenix Pattern Works
The implementation exploits Flutter's widget reconciliation algorithm. When Flutter encounters a widget with a different Key than before, it treats it as an entirely new widget — disposing the old one and creating a fresh instance.
Here's the complete, runnable implementation:
The step-by-step mechanism:
Phoenixsits at the root of the widget tree, above everything elseIts child (your entire app) is wrapped in a
KeyedSubtreewith aUniqueKeyWhen
Phoenix.rebirth(context)is called,setStateassigns a newUniqueKeyFlutter's reconciliation sees the new key, concludes the old subtree no longer exists
Every widget below gets
dispose()d and recreated from scratch — including allStatefulWidgetstate,InheritedWidgetdata, and navigation stacks
KeyedSubtree is a Flutter framework widget that attaches a key to a child without adding any layout or rendering overhead.
Usage Examples
Basic Setup
Wrap at the runApp level:
With Riverpod
Place ProviderScope inside Phoenix so all providers are torn down and recreated on rebirth:
This ensures every Riverpod provider — including keepAlive providers — gets disposed and rebuilt with fresh state.
Common Trigger Points
Note the context.mounted check — after an await, the widget may have been unmounted.
What Resets vs. What Survives
This is the most misunderstood aspect. The Phoenix pattern is an in-process widget tree rebuild, not an OS-level restart. The Dart VM isolate stays alive.
Resets (destroyed and recreated):
All
StatefulWidgetstate — every widget callsdispose(), theninitState()on the new instanceAll
InheritedWidgetdataAll Riverpod/Provider state (if
ProviderScopeis inside Phoenix)The navigation stack — router is recreated from its initial route
In-memory caches held by widgets or providers
Survives (unchanged after rebirth):
Static variables and singletons — they live on the Dart heap, outside the widget tree
dotenv.envvalues — loaded once into a static map at startup, not reloadedHTTP client instances (Dio, http) created outside the widget tree — their
baseUrland headers persistFirebase initialization —
Firebase.initializeApp()throwsFirebaseExceptionif called a second timeNative plugin state — platform channel connections persist across widget rebuilds
SharedPreferencesdata — stored on disk, completely unaffectedSecure storage (
flutter_secure_storage) — persisted to Keychain/Keystore, unaffected
When NOT to Use the Phoenix Pattern
Switching backend environments at runtime
If your API base URLs come from .env files loaded at startup (e.g., via flutter_dotenv), Phoenix won't reload them. HTTP clients like Dio cache the baseUrl from initialization. The correct approach is Flutter flavors — separate builds per environment, each with their own .env, Firebase config, and bundle ID.
This was debated in issue #97939, where a developer wanted to switch environments and reset all injected providers from root. Flutter contributor @chinmaygarde confirmed that iOS doesn't support relaunching the process without user interaction, and suggested recreating the FlutterEngine instance as an alternative — but noted that plugin state would need careful handling. Another Flutter team member, @dnfield, pushed back on the approach entirely, arguing that widgets should listen for changes via MediaQuery or Localizations rather than rebuilding the whole app.
Switching Firebase projects
Firebase can't be re-initialized in the same process. If dev and prod use different Firebase projects (different google-services.json), Phoenix can't switch between them. Use build flavors with FlutterFire CLI instead.
Clearing persisted data
SharedPreferences, secure storage, and SQLite databases survive a Phoenix restart. Clear them before calling rebirth:
flutter_phoenix Package vs. DIY
The flutter_phoenix package by The Ring provides this exact pattern as a pub dependency. The API is identical: wrap with Phoenix, call Phoenix.rebirth(context).
Approach | Pros | Cons |
|---|---|---|
DIY (~30 lines) | No dependency, full control, easy to extend | You maintain it |
| Maintained by community, tested | Extra dependency for minimal code |
The implementation is small enough that copying it avoids an external dependency. Either approach works.
Frequently Asked Questions
Does the Phoenix pattern work with GoRouter, AutoRoute, or other navigation packages?
Yes. Since Phoenix rebuilds the entire widget tree, the router is recreated from scratch with its initial configuration. All navigation state (current route, stack history) is lost — which is usually the desired behavior for a restart.
Can I use Phoenix with BLoC instead of Riverpod?
Yes. If your BlocProvider widgets are inside the Phoenix-wrapped tree, all BLoCs will be closed and recreated. The same principle applies — anything in the widget tree gets rebuilt.
Does Phoenix.rebirth() trigger a visual flash or loading screen?
It rebuilds the tree synchronously in the same frame. There's no inherent flash, but if your app's initState or build methods trigger async loading (showing a spinner), the user will see that loading state again — which is usually appropriate for a restart.
Is the Phoenix pattern safe for production apps?
Yes. It uses standard Flutter APIs (StatefulWidget, Key, KeyedSubtree) with no hacks or platform-specific code. The flutter_phoenix package has been used in production apps since 2020. One caveat: in issue #48955, developers reported occasional freezes when using UniqueKey-based restarts in integration tests — but this appears specific to the test harness environment, not production usage. Flutter team member @knopp also noted that keyboard and TextInputClient state can get into a weird state after restarts, something to watch for if your app has text fields focused at the time of rebirth.
Key Takeaway
The Phoenix pattern is a clean, dependency-free solution for in-process Flutter app restarts. It handles logout flows, failed initialization retries, and configuration changes that affect the widget tree. It works on both Android and iOS with ~30 lines of code.
But it's not a real OS-level restart. Anything living outside the widget tree — statics, singletons, environment variables, native plugin state — survives the rebirth. If you need those to reset too, you need a true process restart, and Flutter doesn't offer one yet.
Know the boundary. Use it for what it's good at.
Further Reading
Flutter docs:
KeyedSubtreeclass — the widget that makes Phoenix workFlutter docs: When to use keys — understanding widget reconciliation
flutter_phoenixon pub.dev — the package implementationrestarton pub.dev — alternative that re-invokesmain()without process killFlutter issue #127409: Add
restartApp()method — official feature request with "soft vs hard restart" discussionFlutter issue #97939: Restart from OS layer — Flutter team discussion on iOS limitations and engine recreation
Flutter flavors setup — the right approach for environment switching
How and when to force a Flutter widget rebuild (LogRocket) — broader context on rebuilds