A vibrant digital illustration depicting the "Phoenix Pattern" in Flutter for app restarts. At the center is a fiery, mythical phoenix with wings spread in shades of orange and teal, symbolizing rebirth.  The image shows a visual loop: on the left, an existing mobile app interface (labeled "LOGOUT" and "STATE") is breaking down into pixels and particles. A curved orange arrow leads upwards from this old state through the phoenix, representing transformation. A second arrow leads from the phoenix down to a fresh, identical app interface on the right (labeled "NEW" and "STATE"), symbolizing the clean, restarted state.  Various Flutter-specific technical labels are integrated into the design:  * The Flutter logo (a blue 'F') is present in the background. * Text snippets like "StatefulWidget," "UniqueKey," "KeyedSubtree," and a code structure snippet "widget_tree" emphasize the framework's mechanics. * The overall composition illustrates the conceptual flow of destroying the widget tree and rebuilding it immediately with a new key.
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

exit(0)

Android + iOS

OS process kill

Apple discourages this — looks like a crash, risks App Store rejection

SystemNavigator.pop()

Android only

App close

Does nothing on iOS

restart_app package

Android + iOS

Native restart

On iOS, exits the app and sends a local notification to reopen. Requires notification permissions.

restart package

Android + iOS

Calls main() again

Re-invokes Dart's main() without exit(0) or notifications. Mentioned in #127409.

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:

import 'package:flutter/material.dart';

/// Wraps the app widget tree and provides a static [rebirth] method
/// to tear down and rebuild the entire tree from scratch.
class Phoenix extends StatefulWidget {
  final Widget child;

  const Phoenix({super.key, required this.child});

  /// Call from anywhere with a valid [BuildContext] to restart the app.
  static void rebirth(BuildContext context) {
    context.findAncestorStateOfType<_PhoenixState>()!.restartApp();
  }

  @override
  State<Phoenix> createState() => _PhoenixState();
}

class _PhoenixState extends State<Phoenix> {
  Key _key = UniqueKey();

  void restartApp() {
    setState(() {
      _key = UniqueKey(); // New key = Flutter treats the subtree as new
    });
  }

  @override
  Widget build(BuildContext context) {
    return KeyedSubtree(
      key: _key,
      child: widget.child,
    );
  }
}
import 'package:flutter/material.dart';

/// Wraps the app widget tree and provides a static [rebirth] method
/// to tear down and rebuild the entire tree from scratch.
class Phoenix extends StatefulWidget {
  final Widget child;

  const Phoenix({super.key, required this.child});

  /// Call from anywhere with a valid [BuildContext] to restart the app.
  static void rebirth(BuildContext context) {
    context.findAncestorStateOfType<_PhoenixState>()!.restartApp();
  }

  @override
  State<Phoenix> createState() => _PhoenixState();
}

class _PhoenixState extends State<Phoenix> {
  Key _key = UniqueKey();

  void restartApp() {
    setState(() {
      _key = UniqueKey(); // New key = Flutter treats the subtree as new
    });
  }

  @override
  Widget build(BuildContext context) {
    return KeyedSubtree(
      key: _key,
      child: widget.child,
    );
  }
}
import 'package:flutter/material.dart';

/// Wraps the app widget tree and provides a static [rebirth] method
/// to tear down and rebuild the entire tree from scratch.
class Phoenix extends StatefulWidget {
  final Widget child;

  const Phoenix({super.key, required this.child});

  /// Call from anywhere with a valid [BuildContext] to restart the app.
  static void rebirth(BuildContext context) {
    context.findAncestorStateOfType<_PhoenixState>()!.restartApp();
  }

  @override
  State<Phoenix> createState() => _PhoenixState();
}

class _PhoenixState extends State<Phoenix> {
  Key _key = UniqueKey();

  void restartApp() {
    setState(() {
      _key = UniqueKey(); // New key = Flutter treats the subtree as new
    });
  }

  @override
  Widget build(BuildContext context) {
    return KeyedSubtree(
      key: _key,
      child: widget.child,
    );
  }
}

The step-by-step mechanism:

  1. Phoenix sits at the root of the widget tree, above everything else

  2. Its child (your entire app) is wrapped in a KeyedSubtree with a UniqueKey

  3. When Phoenix.rebirth(context) is called, setState assigns a new UniqueKey

  4. Flutter's reconciliation sees the new key, concludes the old subtree no longer exists

  5. Every widget below gets dispose()d and recreated from scratch — including all StatefulWidget state, InheritedWidget data, 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:

void main() {
  runApp(
    Phoenix(
      child: MyApp(),
    ),
  );
}
void main() {
  runApp(
    Phoenix(
      child: MyApp(),
    ),
  );
}
void main() {
  runApp(
    Phoenix(
      child: MyApp(),
    ),
  );
}

With Riverpod

Place ProviderScope inside Phoenix so all providers are torn down and recreated on rebirth:

void main() {
  runApp(
    Phoenix(
      child: ProviderScope(
        child: MyApp(),
      ),
    ),
  );
}
void main() {
  runApp(
    Phoenix(
      child: ProviderScope(
        child: MyApp(),
      ),
    ),
  );
}
void main() {
  runApp(
    Phoenix(
      child: ProviderScope(
        child: MyApp(),
      ),
    ),
  );
}

This ensures every Riverpod provider — including keepAlive providers — gets disposed and rebuilt with fresh state.

Common Trigger Points

// After user logout — clear session, then restart
Future<void> handleLogout(BuildContext context) async {
  await authService.logout();
  await secureStorage.deleteAll();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}

// After failed app initialization
if (initFailed) {
  Phoenix.rebirth(context);
}

// After a configuration change that affects the whole app
Future<void> onConfigChanged(BuildContext context) async {
  await persistNewConfig();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}
// After user logout — clear session, then restart
Future<void> handleLogout(BuildContext context) async {
  await authService.logout();
  await secureStorage.deleteAll();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}

// After failed app initialization
if (initFailed) {
  Phoenix.rebirth(context);
}

// After a configuration change that affects the whole app
Future<void> onConfigChanged(BuildContext context) async {
  await persistNewConfig();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}
// After user logout — clear session, then restart
Future<void> handleLogout(BuildContext context) async {
  await authService.logout();
  await secureStorage.deleteAll();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}

// After failed app initialization
if (initFailed) {
  Phoenix.rebirth(context);
}

// After a configuration change that affects the whole app
Future<void> onConfigChanged(BuildContext context) async {
  await persistNewConfig();
  if (context.mounted) {
    Phoenix.rebirth(context);
  }
}

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 StatefulWidget state — every widget calls dispose(), then initState() on the new instance

  • All InheritedWidget data

  • All Riverpod/Provider state (if ProviderScope is 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.env values — loaded once into a static map at startup, not reloaded

  • HTTP client instances (Dio, http) created outside the widget tree — their baseUrl and headers persist

  • Firebase initializationFirebase.initializeApp() throws FirebaseException if called a second time

  • Native plugin state — platform channel connections persist across widget rebuilds

  • SharedPreferences data — stored on disk, completely unaffected

  • Secure 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:

await SharedPreferences.getInstance().then((prefs) => prefs.clear());
await secureStorage.deleteAll();
Phoenix.rebirth(context);
await SharedPreferences.getInstance().then((prefs) => prefs.clear());
await secureStorage.deleteAll();
Phoenix.rebirth(context);
await SharedPreferences.getInstance().then((prefs) => prefs.clear());
await secureStorage.deleteAll();
Phoenix.rebirth(context);

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

flutter_phoenix package

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