Force Update in Flutter (Part 2): The Upgrader Package
Flutter Development
8 min read
July 15, 2026

Force Update in Flutter (Part 2): The Upgrader Package

Part 2 — use the upgrader package to prompt when a newer store build exists. MaterialApp.builder, GoRouter, testing, and when not to use it.

Muhammad Nabi Rahmani

Muhammad Nabi Rahmani

Flutter Developer passionate about creating beautiful mobile experiences

Force Update in Flutter (Part 2): The Upgrader Package

Force Update series: 1. Why you need it · 2. Upgrader · 3. Remote config intro · 4. force_update_helper · 5. GitHub Gist · 6. Firebase Remote Config · 7. Dart Shelf API

In Part 1 we covered why force update exists. This part is the fastest path to a real prompt: the upgrader package.

It compares the installed app version to what is published on the App Store / Play Store and shows an alert when something newer is live.

When upgrader is the right tool

Use it when:

  • You want users on the latest store build most of the time
  • You do not need a remote "minimum version" kill switch
  • The app is already (or will be) published — store listing is the source of truth

Skip it when you need to retire an API while a newer store build is still rolling out, or when you want to force only on critical releases. That is remote config territory.

Install

dart pub add upgrader package_info_plus url_launcher
flutter pub get

Minimal example

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Upgrader Example',
      home: UpgradeAlert(
        child: Scaffold(
          appBar: AppBar(title: const Text('Upgrader Example')),
          body: const Center(child: Text('Checking…')),
        ),
      ),
    );
  }
}

Fine for demos. Real apps rarely use home alone.

Named routes and GoRouter

If you use onGenerateRoute or GoRouter, put UpgradeAlert in MaterialApp.builder and pass a root navigator key so the dialog can present:

final rootNavigatorKey = GlobalKey<NavigatorState>();

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      navigatorKey: rootNavigatorKey,
      builder: (context, child) {
        return UpgradeAlert(
          navigatorKey: rootNavigatorKey,
          child: child ?? const SizedBox.shrink(),
        );
      },
      onGenerateRoute: onGenerateRoute,
    );
  }
}

GoRouter variant:

final rootNavigatorKey = GlobalKey<NavigatorState>();
final goRouter = GoRouter(
  navigatorKey: rootNavigatorKey,
  routes: [ /* … */ ],
);

MaterialApp.router(
  routerConfig: goRouter,
  builder: (context, child) {
    return UpgradeAlert(
      navigatorKey: goRouter.routerDelegate.navigatorKey,
      child: child ?? const SizedBox.shrink(),
    );
  },
);

Two rules I do not break:

  1. builder — alert is an ancestor of every route
  2. navigatorKey — alert knows which navigator owns the overlay

Material vs Cupertino dialog

builder: (context, child) {
  final dialogStyle =
      defaultTargetPlatform == TargetPlatform.iOS ||
              defaultTargetPlatform == TargetPlatform.macOS
          ? UpgradeDialogStyle.cupertino
          : UpgradeDialogStyle.material;

  return UpgradeAlert(
    navigatorKey: rootNavigatorKey,
    dialogStyle: dialogStyle,
    child: child ?? const SizedBox.shrink(),
  );
},

Soft vs hard with upgrader flags

GoalKnobs
Soft nudgedefault / allow Later + Ignore
Hard forceshowIgnore: false, showLater: false, barrierDismissible: false

Hard-blocking every store release is rude. I keep upgrader soft unless the release is critical — and for critical work I often switch to a remote min version instead.

How to test

upgrader compares installed version (from the running app / pubspec packaging) to the store listing.

  1. App must be published (or use a published package id you control)
  2. Set version: in pubspec.yaml below the store version
  3. Run on a real device for the full "Update now → store" path
  4. Dev-only: clear cached "already prompted" state
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // DEV ONLY — remove before release
  await Upgrader.clearSavedSettings();
  runApp(const MainApp());
}

Limits I hit in production

  • No remote control of when a version dies — only "is there something newer on the store?"
  • You cannot keep supporting 1.4 while 1.5 is live without users getting nagged
  • Backend deprecation schedules need a required_version you own

That is exactly why Part 3 introduces remote config force update.

Summary

  • upgrader = store version comparison + dialog, fast to ship
  • Wire it via builder + navigatorKey in real navigation setups
  • Prefer soft prompts; reserve hard for true emergencies
  • For kill-switch control, move on to remote minimum version

← Previous Part 1: Why you need force update
Next → Part 3: Remote config approach

Share:

Keep Reading

More articles you might enjoy