app_upgrade_checker

Tell your users a new version is out β€” and get them to install it.

app_upgrade_checker checks the version running on the user's device against the latest one you've released. If they're behind, it shows them a full-screen update screen that takes them straight to the download.

✨ Why app_upgrade_checker?

You decide what "the latest version" is β€” read it from the store, or from your own backend or a simple JSON file. And the screen your users see is a designed, animated, fully customizable one.

πŸ”Œ The version comes from wherever you want The store, or a URL you control β€” your API or a JSON file. With your own URL, you set the latest version, the oldest build still allowed, and whether updating is mandatory.
πŸ§ͺ Test mode See the update screen while you build β€” even if your app isn't on the store yet. Try any case: forced, optional, up-to-date, or failed.
🧩 No backend? Still works A static JSON file on GitHub Pages is enough β€” free, no server.
🎨 3 full-screen designs Artwork, animation and a clear call to action β€” ready to ship as-is.
πŸŽ›οΈ 28 theme fields Reorder or hide any block β€” or pass your own widget.
✨ 7 entrance animations Automatically respect reduce motion.
🌍 7 languages built in Arabic, English, Urdu, Spanish, Hindi, French, Indonesian β€” follows the device, with automatic RTL.

The three designs

Cosmic (default) RocketUp SuperHero
AppUpgradeTheme.cosmic() AppUpgradeTheme.rocketUp() AppUpgradeTheme.superHero()

Optional blocks, on any design

Every design ships with a badge pill and three feature cards. Both are off by default β€” one flag each brings in that design's own content.

Default showBadge: true showBadge + showFeatures

Platforms: Android and iOS only. This is a mobile-focused package; it does not support web or desktop.


Table of contents

Getting started

  1. How it works
  2. Install
  3. Quick start β€” the store setup, in one line

Choosing a source

  1. Choosing where the version comes from

Styling

  1. Theming the screen
  2. Motion β€” entrances, button glow, reduce motion

How it works

app_upgrade_checker reads the currently installed version from the device, asks a source what the latest version is, and returns one of three outcomes: an update is available (optionally forced), no update, or the check failed.

The source is what you choose per platform. That's the whole API surface.


Install

dependencies:
  app_upgrade_checker: ^1.0.0
import 'package:app_upgrade_checker/app_upgrade_checker.dart';

Quick start

This section covers the default setup β€” your app is public on Google Play or the App Store. That needs no backend, no JSON, and no configuration.

1. The one line

await AppUpgrade.checkAndPrompt(context);

That's a complete, working integration. With no arguments it reads the store listing for the current platform, compares it to the installed version, and β€” if the user is behind β€” shows the built-in update screen.

2. Where to call it

At app start, from initState. It's safe there by design: it waits for the first frame internally, so the Navigator is ready before anything is shown.

@override
void initState() {
  super.initState();
  AppUpgrade.checkAndPrompt(context); // no post-frame callback needed
}

You can also call it from a button ("Check for updates" in settings), or after login β€” anywhere you have a BuildContext. Calling it twice is safe: the second call reuses the first request and never stacks a second screen. To show a spinner or disable that button meanwhile, read AppUpgrade.isChecking.

3. What you can pass to checkAndPrompt

context is the only required argument. Everything else is optional β€” this table is about when you'd bother:

Parameter Type Default Pass it when…
context BuildContext required always β€” it's how the screen is shown.
config AppConfig AppConfig() β†’ the store you want the version to come from your backend or a JSON file instead of the store β†’ choosing where the version comes from.
theme AppUpgradeTheme? Cosmic design you want a different look β€” another design, your colors, your font β†’ theming.
preview UpdatePreview? null you're still developing and want to see the screen before your app is on the store. UpdatePreview.optional() / .forced() / .upToDate() / .failure() β€” each takes versionName, releaseNotes, storeUrl, delay. Throws in release builds, so it can't ship by accident.
builder Widget Function(UpdateAvailable)? null you want your own screen instead of the built-in one: builder: (update) => MyScreen(update). It receives the full result to render from.
onError void Function(UpdateCheckError)? null you want failed checks reported to Crashlytics / Sentry. The user never sees an error either way.

So a typical real-world call is still short:

await AppUpgrade.checkAndPrompt(
  context,
  theme: AppUpgradeTheme.rocketUp(),  // just a different design
);

What happens in each case: update found β†’ the screen is shown. No update β†’ nothing happens. Check failed β†’ the user sees nothing, and onError fires.

Your app isn't published yet? Then there is no store listing to read, so the check finds nothing. Add preview: UpdatePreview.optional() (see the table above) to see the screen anyway.


4. The other entry point: checkUpdate

Use this when you don't want the built-in behaviour: it runs the same check but shows nothing, handing you the answer so you can decide β€” a banner instead of a screen, a postponed prompt, a silent background check. It takes config and preview (no context, since it renders nothing) and returns an UpdateCheckResult, which is always exactly one of three:

Result Meaning What you can read from it
UpdateAvailable the user is behind the version, the store link, the release notes, whether it's forced β€” see the example below
NoUpdate already up to date β€”
UpdateCheckError the check failed state β€” socket / timeout / server / unAuthorized / format, so you can retry a dropped connection but report a bad response β€” and message
final result = await AppUpgrade.checkUpdate();

switch (result) {
  case UpdateAvailable():
    // Every field is optional to use β€” read only what you need:
    result.isForceUpdate;            // bool   β€” is it mandatory?
    result.versionName;              // String?β€” "3.5.0"
    result.storeUrl;                 // String?β€” the download link
    result.releaseNotes;             // String?β€” what's new
    result.versionCode;              // int?   β€” CustomSource only, else null
    result.minSupportedVersionCode;  // int?   β€” CustomSource only, else null
    result.data;                     // the raw response

    if (context.mounted) await AppUpgrade.showUpdateDialog(context, result);
    // ...or send them straight there: AppUpgrade.openStore(result.storeUrl!);
  case NoUpdate():
    break;
  case UpdateCheckError():
    debugPrint('check failed: ${result.message}');
}

checkAndPrompt is just these two combined β€” checkUpdate to get the result, then showUpdateDialog to show it.


That's the quick start. Everything below goes deeper: using your own backend instead of the store, the JSON contract, theming the screen, animations, and the full reference for every class.


Choosing where the version comes from

First: what is AppConfig?

AppConfig answers one question β€” "where do I find out the latest version?" β€” and it answers it separately for each platform, because Android and iOS are usually distributed differently.

AppConfig(
  android: /* a source */,   // null β†’ check Google Play
  ios:     /* a source */,   // null β†’ check the App Store
)
  • Leave a platform null and it checks that platform's store with defaults. That's why AppConfig() β€” the default in Quick start β€” needs no arguments.
  • Set a platform and you replace its source. Each platform gets exactly one.
  • You can mix them freely: Android from your backend, iOS from the App Store, or any other combination.

There are two kinds of source to choose from:

Source Where the version comes from You need
CustomSource a URL you control β€” your API or a JSON file a URL returning the JSON contract
PlayStoreSource / AppStoreSource the public store listing nothing β€” your app just has to be published

1. Store sources β€” read the listing directly

The simplest path, and the default: the device reads your public store listing and takes the version from there. Nothing to host, nothing to maintain. This is what runs when you call checkAndPrompt(context) with no config.

Platform Class Reads from
Android PlayStoreSource the Google Play listing (an HTML read β€” unofficial)
iOS AppStoreSource Apple's iTunes Lookup API (official, public, no auth)

Use it when your app is published publicly and you're happy to treat "the version on the store" as the truth.

One thing to know: a store listing only exposes the version name ("3.2.0") β€” never a numeric build number. So there is nothing like minSupportedVersionCode here; "is this update mandatory?" is decided by forcePolicy instead.

// Both platforms on their store, with defaults β€” same as AppConfig()
const AppConfig();

// Or configure them:
AppConfig(
  android: const PlayStoreSource(forcePolicy: ForcePolicy.always),
  ios: const AppStoreSource(appleId: '123456789'),
)

PlayStoreSource β€” what you can pass:

Parameter Type Default Pass it when…
forcePolicy ForcePolicy auto you want to change when an update becomes mandatory β†’ ForcePolicy.
language String? null you want the release notes in a specific language (hl=), e.g. 'ar'.
country String? null your listing differs per storefront (gl=), e.g. 'SA'.

AppStoreSource β€” what you can pass:

Parameter Type Default Pass it when…
forcePolicy ForcePolicy auto same as above β†’ ForcePolicy.
appleId String? null the bundle-id lookup returns nothing. Pass the numeric Apple ID (trackId) instead.
country String? null your app is published in a specific storefront, e.g. 'sa'. Defaults to the US store.

ForcePolicy

Controls when a store-method update is mandatory (ignored for CustomSource, which carries its own flags):

Value Behaviour
ForcePolicy.auto (default) Forced only on a major bump (3.x.x β†’ 4.x.x).
ForcePolicy.always Every available update is forced.
ForcePolicy.never Every update is optional.

Not published yet, or on a closed track? A store lookup has no listing to read β€” use a CustomSource, or the preview parameter during development.


2. CustomSource β€” your backend or a JSON file

Point it at any URL you control and you decide the latest version, the oldest build still allowed, and whether the update is mandatory. Use it for private or non-store distribution (TestFlight, enterprise, Huawei, a direct APK), staged rollouts, or whenever you need a real numeric build number β€” the stores only expose a version name to a device.

The library sends a plain GET and expects the JSON below. A static file and a real backend are identical here β€” only the URL differs, so a file on GitHub Pages, Cloudflare, S3 or Netlify works with no server at all.

AppConfig(
  android: const CustomSource(url: 'https://api.you.com/app-version/android'),
  ios: const CustomSource(url: 'https://api.you.com/app-version/ios'),
)
Parameter Type Default Pass it when…
url String required always. Use a separate URL per platform so each carries its own storeUrl.
headers Map<String,String>? null your endpoint needs auth or any extra header β€” it's the only thing sent, so also handy for a segment header. Note the check often runs before sign-in.
fallbackStoreUrl String? null you'd rather keep the download link in the app than in the JSON. Used only if the response has no storeUrl.

Want the real store version? Have your backend call the official store APIs (Google Play Developer, App Store Connect) and return the JSON below. Both need a secret key, so they can only be called from a server β€” and it's the only way to get a numeric build number or a closed-track version.

What your URL must return

All fields are optional (and may be nested under a data key). The minimum that works: { "latestVersionCode": 55, "storeUrl": "..." }.

{
  "latestVersionCode": 55,
  "latestVersionName": "3.5.0",
  "minSupportedVersionCode": 50,
  "storeUrl": "https://play.google.com/store/apps/details?id=com.you.app",
  "releaseNotes": "Performance improvements and bug fixes",
  "hasUpdate": true,
  "isForceUpdate": false
}
Field Type Meaning
latestVersionCode int Build number of the latest release. Primary basis for comparison.
latestVersionName string Display version, e.g. "3.5.0" (also accepted as versionName).
minSupportedVersionCode int Builds below this are force-updated.
storeUrl string Where "Update now" sends the user.
releaseNotes string Shown in the update screen.
hasUpdate bool Optional override β€” if present, the library trusts it.
isForceUpdate bool Optional override β€” if present, the library trusts it.

Your endpoint receives nothing β€” no body, no parameters, and the installed version is never sent. The response is the same for everyone, so cache it hard.

A live example: alaakhaledahmed.github.io/app_upgrade_checker/version/android.json β€” served from docs/version/ in this repo and used by example/lib/main.dart.


Theming the screen

The screen carries no styling of its own β€” a AppUpgradeTheme supplies every colour, text, asset and animation it draws, and which blocks it draws at all. So restyling is never a subclass: you build a theme and pass it as theme:.

Three designs ship as named constructors; each fills all 28 fields with its own values, and you override only what you need.

Every AppUpgradeTheme field

Every field is optional; anything you omit keeps the design's own value.

Field Type Default Purpose
lang ThemeLang? the device's language, else en Language of the default texts: en, ar, ur, es, hi, fr, id. ar/ur also switch the screen to RTL. Any text you set yourself wins over the translation.
order List<UpdateSection> visual β†’ badge β†’ title β†’ version β†’ description β†’ features β†’ buttons Which blocks appear, and in what order.
background UpdateBackground per design Solid colour, gradient, or an image.
visual UpdateVisual? per design The top artwork β€” Lottie, image, icon, or your widget.
badge UpdateBadgeStyle per design The pill above the headline.
title UpdateTitle per design The two-line headline.
version UpdateVersionStyle default The version pill.
description UpdateTextStyle per design The body paragraph.
fallbackDescription String 'A new version is ready to install.' Used when neither the theme nor the response has text.
features List<UpdateFeature> per design The cards.
updateButton UpdateButtonStyle per design Primary button.
laterButton LaterButtonStyle default Dismiss link.
showVisual, showTitle, showDescription, showUpdateButton, showLaterButton bool true On by default.
showBadge, showFeatures, showVersion bool false Off by default β€” one flag brings in the design's own content.
contentPadding EdgeInsetsGeometry horizontal: 15 Padding around the column.
sectionSpacing double 20 Gap between blocks.
featureSpacing double 10 Gap between feature cards.
scrollable bool true Keep true β€” prevents overflow on short screens.
alignment CrossAxisAlignment center Horizontal alignment.
fontFamily String? null Your font; null uses the bundled one.
textDirection TextDirection? null null follows the host app; set rtl to force it.
entrance UpdateEntrance per design How the screen arrives.
pulse UpdatePulse? per design Button glow. copyWith(noPulse: true) switches it off.

Theme building blocks

Class Key fields
UpdateTitle firstLine, secondLine, highlight, sparkle, color, highlightColor, sparkleColor, shadowColor, fontSize, fontWeight, textAlign, textDirection, firstLineHeight β€” the gap between the two lines, as a multiple of fontSize (default 1.0). Lower it to pull them together, but note it also tightens a line that wraps, so a long headline can overlap itself.
UpdateBadgeStyle text ('NEW UPDATE AVAILABLE'), prefix ('Λ™βœ¦ '), textColor, backgroundColor, borderColor, borderWidth (2), radius (50), padding, margin.
UpdateButtonStyle text ('UPDATE NOW'), icon (rocket_launch_rounded), iconWidget, iconColor, gradient, backgroundColor, textColor, fontSize, fontWeight, borderColor, radius, padding.
LaterButtonStyle text, color, fontSize, fontWeight, underline styling.
UpdateFeature icon or iconWidget, title, subtitle, iconColor, iconSize, iconGradient, card border and background.
UpdateTextStyle text, color, fontSize, fontWeight, textAlign.
UpdateVersionStyle text, colours, border, radius, padding.
UpdateBackground .solid(color), .gradient(colors), AssetBackground(path, package:, color:), .none().
UpdateVisual .lottie(path, package:), .asset(path, package:), .network(url), .icon(icon), .custom(builder) β€” plus heightFactor / height.
UpdateEntrance .fade(), .slideUp(), .rocketPull(), .descend(), .warpIn(), .liftoff(), .none() β€” each takes a duration.
UpdatePulse The breathing glow behind the primary button; null on the theme switches it off.
UpdateSection The enum used by order: visual, badge, title, version, description, features, updateButton, laterButton.

One rule for every style above. Whatever you set is yours; everything you leave out keeps the design's value. So changing a label never costs you its colours, gradient or border β€” and to change one of those, you just name it:

updateButton: const UpdateButtonStyle(text: 'Get it now'),        // Cosmic's gradient kept
updateButton: const UpdateButtonStyle(backgroundColor: Colors.green), // flat green instead

A features list works the same way, pairing your cards with the design's by position; any extra card you add is kept as-is.

Picking and building a theme

AppUpgradeTheme.cosmic()      // astronaut over a blue starfield
AppUpgradeTheme.rocketUp()    // rocket over a pink starfield
AppUpgradeTheme.superHero()   // cartoon hero over a red starfield
final myTheme = AppUpgradeTheme.cosmic(
  title: const UpdateTitle(
    firstLine: 'Ready for', secondLine: 'something', highlight: 'better!'),
  showFeatures: true,
  updateButton: const UpdateButtonStyle(text: 'Get it now'), // keeps the design's gradient + icon
  lang: ThemeLang.ar,               // default texts in Arabic + RTL
  fontFamily: 'Cairo',              // the family: from YOUR pubspec.yaml
);

await AppUpgrade.checkAndPrompt(context, theme: myTheme);

// inline, or a variant of one you already have:
await AppUpgrade.checkAndPrompt(context, theme: AppUpgradeTheme.rocketUp());
await AppUpgrade.checkAndPrompt(context, theme: myTheme.copyWith(showBadge: false));

Blocks: show, hide, reorder

visual β†’ badge β†’ title β†’ version β†’ description β†’ features β†’ updateButton β†’ laterButton

showBadge, showFeatures and showVersion are off by default; the rest are on. Turning one on brings in that design's own content (pictured at the top):

AppUpgradeTheme.cosmic(
  showBadge: true,          // the design's own badge text
  showFeatures: true,       // the design's own three cards
  showDescription: false,   // drop the paragraph

  order: const [            // reorder or drop blocks
    UpdateSection.visual,
    UpdateSection.features, // cards above the headline
    UpdateSection.title,
    UpdateSection.updateButton,
  ],
);

Title, background and artwork

All three are theme fields, so you set them where you build the theme:

AppUpgradeTheme.cosmic(
  // Headline: line one, line two, and the highlighted last word.
  title: const UpdateTitle(
    firstLine: 'Ready for', secondLine: 'something', highlight: 'better!'),

  // Behind everything β€” pick exactly one:
  background: const UpdateBackground.solid(Color(0xff01114f)),

  // The artwork on top β€” pick exactly one:
  visual: const UpdateVisual.lottie('assets/rocket.json', package: null),
);

Every option for each field:

// title β€” the same three-part shape in any language
const UpdateTitle(
  firstLine: 'Ready for', secondLine: 'something', highlight: 'better!');
const UpdateTitle(
  firstLine: 'Ω‡Ω„ Ψ£Ω†Ψͺ Ω…Ψ³ΨͺΨΉΨ―', secondLine: 'Ω„Ψ΄ΩŠΨ‘', highlight: 'أفآل!');

// background
const UpdateBackground.solid(Color(0xff01114f));
const UpdateBackground.gradient(LinearGradient(colors: [...]));
const UpdateBackground.asset('assets/bg.png', package: null, color: Color(0xff01114f));
const UpdateBackground.network('https://…', color: Color(0xff01114f));
const UpdateBackground.none();

// visual
const UpdateVisual.lottie('assets/anim.json', package: null);
const UpdateVisual.asset('assets/hero.png', package: null);
const UpdateVisual.network('https://…');
const UpdateVisual.icon(Icons.system_update_rounded, circleGradient: [...]);
UpdateVisual.custom((context) => MyWidget());
  • package: null β€” the asset is in your app, not the package's.
  • On an image background, color fills the rest of the screen: match the image's own edge so there's no visible seam.
  • textDirection: TextDirection.rtl forces RTL in an app with no RTL locale.
  • No fontFamily β†’ the bundled font (IBM Plex Sans Arabic, Regular + Bold).
  • Pull the headline's two lines closer with UpdateTitle(firstLineHeight: 0.7) β€” keep it near 1.0 for long text.
  • Borrow a palette from another design: AppUpgradeTheme.cosmic(background: RocketUpDesign.backgroundStyle).

Motion

Entrances

How the screen arrives, picked per design like any other value:

AppUpgradeTheme.cosmic(entrance: const UpdateEntrance.rocketPull());
Entrance Motion
rocketPull() panel rises while the artwork leads it, so the artwork appears to pull the page
warpIn() eases down from a slight over-scale, like settling after a jump
liftoff() the backdrop sinks while content holds still β€” the camera seems to climb
descend() arrives from above
slideUp() plain slide from the bottom
fade() cross-fade
none() no entrance, shown instantly

Defaults: Cosmic warpIn, RocketUp rocketPull, SuperHero slideUp. All are tunable (duration, parallax, stagger, …).

Every entrance runs for 900ms by default and takes a duration of its own: UpdateEntrance.warpIn(duration: Duration(milliseconds: 500)).

Button glow

A slow breathing glow keeps the eye on the action:

AppUpgradeTheme.cosmic(
  pulse: const UpdatePulse(period: Duration(milliseconds: 1800)),
);

AppUpgradeTheme.cosmic().copyWith(noPulse: true);   // off

Reduce motion

Every entrance degrades to a short fade and the glow is suppressed when the platform's "reduce motion" accessibility setting is on. That setting exists for motion sensitivity, so it is honoured automatically and cannot be overridden.

Libraries

app_upgrade_checker
AppUpgrade β€” check whether a newer version of the app is available, either directly from the store (public apps) or via your backend (private/internal distribution), then prompt the user to update.