Skip to main content

flutter_app_updater 3.2.0 copy "flutter_app_updater: ^3.2.0" to clipboard
flutter_app_updater: ^3.2.0 copied to clipboard

Verified app updates with default or custom UI for stores, Android markets, packages, and desktop installers.

Flutter App Updater #

English | 简体中文

CI pub package license

Flutter App Updater checks trusted release manifests and delivers updates with a ready-to-use dialog. Use the default UI, replace it with your own, or use the UI-independent core APIs.

Supported platforms and actions:

  • Android: Google Play URL, Chinese Android markets, APK download, APK install, and download then install.
  • iOS: App Store URL.
  • macOS: Mac App Store URL, DMG and ZIP installer download then open.
  • Windows: MSIX, MSI, and EXE installer download then open.

Install #

Version 3.2 adds default/custom update UI and automatic runtime metadata while preserving 3.1 constructors, core APIs, and three-branch exhaustive event switches. See the 3.1-to-3.2 upgrade guide for optional UI adoption and cache behavior. The manifest schema remains v3.

dependencies:
  flutter_app_updater: ^3.2.0

This is the installation constraint for the 3.2.0 release once published.

Quick Start #

Create an updater once, then call checkAndShow(context) from a button inside your MaterialApp. It shows confirmation, download progress, cancellation, retry, and Android installation-permission recovery. No action starts before the user confirms.

import 'package:flutter/material.dart';
import 'package:flutter_app_updater/flutter_app_updater.dart';

final updater = await AppUpdater.fromManifest(
  manifestUrl: Uri.parse('https://example.com/app-updates.json'),
  trustedPublicKeys: trustedManifestPublicKeys,
);

await updater.checkAndShow(context);

Version, build number, application identity, platform, and current process architecture are read automatically. channel defaults to stable; pass channel: 'beta' only for another release channel. trustedManifestPublicKeys maps key IDs to Base64-encoded raw 32-byte Ed25519 public keys. Self-hosted updates require trusted keys; omit trustedPublicKeys for bare official-store or Android-market manifests. Initialization captures runtime information once, without fetching a manifest or executing an update. Native lookup or validation failures fail the initialization Future; handle them separately from structured check/action results. No fabricated version fallback is used. Downloads use an application-private directory by default.

Android private APK installation also requires the host permission described in Self-Hosted Android APK; store delivery does not need that permission. Sandboxed macOS hosts must enable com.apple.security.network.client in both Debug and Release entitlements.

The default dialog follows your theme and uses Chinese for zh locales, English otherwise. Optional updates can be deferred. Required updates block back navigation and dismissal, including after a store or installer handoff: the user must finish updating and reopen the app. The host still owns enforcement outside the dialog. A successful action reports a download or handoff, not a completed installation.

Required flows can check again after a failure or handoff. Refreshing clears stale actions but preserves the dismissal barrier even if the next check fails, finds no update, or returns an optional release. When multiple verified actions are available, the user may select another update method; selecting it never executes it without a new confirmation. Custom UIs can use canCheck, isRequired, selectedAction, and selectAction on UpdateController.

The returned UpdateFlowResult? describes the latest check, after dismissal; it is null if the caller was unmounted or closed the dialog before checking finished. Repeated calls on the same updater share the active dialog. Checks and settings recovery never automatically execute an update.

expectedAppId defaults to the installed package identity and can be overridden. Windows uses the executable InternalName; set expectedAppId when it differs from the publisher's manifest app ID. The remote manifest must still match the resolved identity; another application fails with APP_ID_MISMATCH before selection or execution.

Existing v2 integrations should follow the v2-to-v3 migration guide. The complete transport, signature, artifact, and platform trust boundaries are documented in the security model.

Custom UI #

Configure a reusable UI during initialization with withUI(builder: ...):

final updateUI = (await AppUpdater.fromManifest(
  manifestUrl: Uri.parse('https://example.com/app-updates.json'),
  trustedPublicKeys: trustedManifestPublicKeys,
)).withUI(
  builder: (context, controller) => MyUpdateDialog(controller: controller),
);

// Reuse from a startup prompt, settings button, or another update entry point.
await updateUI.checkAndShow(context);

MyUpdateDialog is your widget; it receives the same UpdateController as the default dialog. withUI returns an immutable AppUpdaterWithUI, stores only the builder, and does not check or show anything. Each show creates fresh state and disposes its controller on dismissal. The original updater is unchanged; updateUI.updater.checkAndShow(context) still shows the default UI and updateUI.updater also exposes the core APIs. Concurrent calls through any UI configuration or the original updater share one active dialog; the first call determines its builder.

You can still supply builder directly for one show, including to override a configured updateUI.checkAndShow(context, builder: ...). That override does not change its saved builder. Either form rebuilds when the shared controller changes, so your UI needs no separate download or retry state machine. This minimal example builds confirmation and dismissal:

await updater.checkAndShow(
  context,
  builder: (context, controller) => AlertDialog(
    title: const Text('App update'),
    content: Text(controller.update?.candidate.releaseNotes ??
        'Checking for updates…'),
    actions: [
      if (controller.canDismiss)
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Close'),
        ),
      if (controller.canPerform)
        FilledButton(
          onPressed: controller.perform,
          child: const Text('Update / Retry'),
        ),
    ],
  ),
);

This minimal example covers confirmation and dismissal. Use phase, checkResult, actionResult, progress, canCancel, and canOpenInstallSettings for complete feedback and recovery. Call cancel() or openInstallSettings() from explicit user gestures. After settings, another perform() confirmation rechecks permission. A missing Android manifest permission requires rebuilding the host; settings cannot fix it. Custom executors own permission preflight unless you provide installPermission. For Android download-and-install and desktop installers, isHandingOff marks native verification and opening; canCancel becomes false and the default dialog removes download cancellation. Back navigation remains guarded; custom close buttons must honor canDismiss. For a custom streaming executor, its first completion result is authoritative. Later events and errors thrown while canceling its stream during cleanup cannot replace that result. Finish failure-prone work before emitting completion, and await stream completion or subscription cancellation to know cleanup has ended.

For a sheet or embedded page, create UpdateController(updater: updater), call check(), render it with ListenableBuilder, and call dispose() when the UI goes away. UpdateDialog(controller: controller) is also reusable directly. The example's Update UI tab demonstrates default and custom dialogs using simulated execution.

For fully headless integration, retain checkAndPrepare() and performRecommended() / performRecommendedStream(). Checking never presents UI or executes an action:

final prepared = await updater.checkAndPrepare();
if (prepared case PreparedUpdateAvailable()) {
  // Call only after your own user confirmation or explicit host policy.
  final actionResult = await updater.performRecommended(prepared);
  debugPrint('${actionResult.isSuccess}: ${actionResult.code}');
}

Advanced configuration and test injection #

Architecture is detected from the current process ABI using arm, arm64, x86, or x64. Unknown ABIs match only universal releases. Override legacy server naming through advanced options:

final updater = await AppUpdater.fromManifest(
  manifestUrl: manifestUrl,
  trustedPublicKeys: trustedManifestPublicKeys,
  options: const AppUpdaterOptions(architecture: 'aarch64'),
);

AppUpdaterOptions also holds headers, artifact storage, distribution/selection policy, retries, executors, and runtimeInfoProvider for tests. An advanced signaturePolicy can configure signature timing and cannot be combined with the ordinary trustedPublicKeys argument.

A supplied executors list replaces all default executors. The first supporting executor handles the action; use StreamingUpdateActionExecutor when the UI needs progress and cooperative cancellation. A later streaming fallback does not make an earlier non-streaming executor cancelable.

Use AppUpdater.manifest or AppUpdater(source: ..., selector: ...) for explicit installed version, build, or platform values. Tests can inject an AppRuntimeInfoProvider returning AppRuntimeInfo without native package lookup.

The REST transport requests exactly the supplied URL; it does not append runtime query parameters. Servers may return a complete manifest for client-side selection. If a server requires runtime query parameters, read them first with PlatformAppRuntimeInfoProvider().load() and construct the endpoint explicitly.

The added dependencies retain Flutter 3.29 compatibility: package_info_plus 8.x and path_provider ^2.1.5. Pub selects an SDK-compatible version.

Reliability and integration boundaries #

UpdateSelectionPolicy.latestRelease remains the default: an unsupported newest compatible release returns noSupportedAction. Opt into selectionPolicy: UpdateSelectionPolicy.latestExecutable on AppUpdater or AppUpdater.manifest to try older compatible newer-than-installed optional releases. Actions retain publisher order after distribution and capability filtering. A required release, or one whose minimum supported version excludes the installed version, blocks fallback. The complete remote manifest still passes trust verification before selection. If all actions are filtered out, UpdateCheckFailed.isRequired and PreparedUpdateCheckFailed.isRequired retain that known required policy. The controller and default/custom dialog guards remain blocking even on the first check failure; hosts using core APIs must honor the same flag.

Default package downloads are available on Android, macOS, and Windows; iOS supports store/URL handoffs, not APK downloads. A per-check selector must target the updater's execution platform. Custom executors remain host-owned extensions.

All network artifact downloads, including direct typed actions and desktop installers, require positive exact size and a 64-hex SHA-256 before any request. The trusted local InstallPackageAction keeps its optional paired size/hash contract. Artifact URLs reject embedded credentials and missing hosts. Low-level HTTP test support retains only localhost, 127.0.0.1, and ::1; remote manifest transport follows its separately configured HTTPS policy.

Cancellation before dispatch (including from UpdateActionStarted) prevents executor side effects. Cancellation cannot undo a store or installer already opened. Only the download owner may remove partial state. Subscription cancellation awaits response/file cleanup and ownership release. Canceling the token is only a request; await the terminal event or subscription cancellation before retrying or deleting the download directory. If filesystem cleanup fails, the download returns PACKAGE_DOWNLOAD_FAILED with a diagnostic and releases ownership; partial files or checkpoints may remain. Restore storage access before retrying or removing those files.

Unix ownership uses handle-scoped flock plus the legacy process lock on a separate inode; keep both .download.owner and .download.lock files, and do not delete them during use. New isolates are mutually exclusive. A legacy downloader in the same process is not covered by the new ownership layer; upgrade all isolate entry points.

Android production downloads use filesDir/flutter_app_updater/foreground/, explicitly allowed by the bundled FileProvider (${applicationId}.flutter_app_updater.fileprovider, @xml/flutter_app_updater_file_paths). Custom directories must match a host-configured provider root. The host's legacy ${applicationId}.fileprovider is tried only if the dedicated provider cannot share that path. The example's debug-only provider probe reads a fixture URI without opening the installer. macOS hosts need com.apple.security.network.client in both Debug and Release sandbox entitlements; the example targets macOS 10.15 or later.

Parser output and checked/prepared actions are immutable snapshots. Public const constructors remain compatible; callers constructing models directly own the mutability of the lists they supply. Successful download, installer handoff, and a newly running version are three separate states; handoff success does not prove installation. Foreground packages use packages/<sha256>/<filename> and desktop installers use installers/<sha256>/<filename> under the configured download directory. The full normalized digest isolates different artifacts, even when their URL filenames match. Always use the returned file path. macOS and Windows recheck exact size and SHA-256 on a worker thread before handoff. Windows denies file writes and deletion through the shell call; macOS rejects symlinks and rechecks file identity immediately before opening. macOS still has a path-based handoff window, and neither platform guarantees file identity after the system accepts the handoff. Custom platform implementations must provide equivalent native validation.

Use the checks in CONTRIBUTING.md before release. The manually triggered Device verification workflow exercises Android native ownership, FileProvider and macOS sandbox HTTPS without installing an update. Configured CI checks do not establish successful Windows or real-device validation.

Progress and cancellation #

Download and installer actions expose started, progress, and one terminal event. Android download-and-install and desktop installers also emit UpdateActionHandoffStarted, a subtype of UpdateActionProgress, before native verification/opening. Existing three-branch switches handle it as completed transfer progress. To show a separate handoff stage, match it before ordinary progress, as below. Download completion does not establish installation. Stop offering cancellation at this boundary; cancellation received while delivering the event still prevents dispatch.

final cancelToken = UpdateActionCancelToken();

await for (final event in updater.performRecommendedStream(
  result,
  cancelToken: cancelToken,
)) {
  switch (event) {
    case UpdateActionStarted():
      showProgress();
    case UpdateActionHandoffStarted():
      showInstallerHandoff(); // Disable download cancellation.
    case UpdateActionProgress(:final fraction):
      updateProgress(fraction);
    case UpdateActionCompleted(:final result):
      handleResult(result);
  }
}

// Call from your UI when the user cancels.
cancelToken.cancel(); // Only while the transfer is still cancelable.

perform() and performRecommended() remain available when a single terminal result is sufficient.

Network and artifact safety #

  • Remote manifests and artifacts require absolute HTTPS URLs. Plain HTTP is an opt-in loopback-only development exception. Redirects are limited to five, every target is revalidated, HTTPS cannot downgrade to HTTP, and sensitive request headers cross only a same-origin redirect. The loopback option allows an initial HTTP development endpoint, including HTTP redirects between loopback endpoints. Once a request uses HTTPS, it may never redirect back to HTTP, even to loopback.
  • Remote self-hosted actions require exact byte size and a required SHA-256 digest, plus an authenticated Ed25519 envelope before payload parsing. The declared size must be positive.
  • Downloads use 30-second request and idle timeouts, retry transient failures, preserve resume state only with a strong ETag, and default to a 1 GiB maximum. Last-Modified alone or a weak ETag causes a clean restart. A URL fingerprint protects checkpoint privacy, and an isolate-local fast guard plus persistent operating-system locks reject competing writers.
  • Declared artifact sizes must be positive and are enforced before and during streaming. After ownership is acquired, cancellation, size violations, and hash mismatches attempt to remove partial files; cleanup failures return a structured failure with a diagnostic. Pre-cancellation preserves existing state.
  • The manifest appId, platform, channel, and architecture are checked before action selection. Unknown runtime architecture fails closed for an architecture-specific release; only a genuinely universal release matches.
  • Actions remain in publisher order. Host UpdateDistributionPolicy and executor capability filtering remove disallowed actions without reordering; the first remaining action is recommended. Use UpdateDistributionPolicy.storeOnly or UpdateDistributionPolicy.selfHostedOnly to make the host boundary explicit.

Getting update information #

AppUpdater.fromManifest and the explicit AppUpdater.manifest entry use an HTTPS GET request for both a static JSON file and a RESTful API. The two delivery styles use the same manifest v3 response schema.

Static JSON file #

Publish the manifest payload, or its signed envelope, as an HTTPS document on a CDN, object store, or web server. The repository includes a complete payload at doc/examples/update-manifest-v3.json. Here, static JSON means a document hosted over HTTPS. Neither entry reads local file:// URLs or Flutter asset paths. A trusted adapter can instead construct an UpdateManifest and use UpdateSource.staticManifest.

final updater = await AppUpdater.fromManifest(
  manifestUrl: Uri.parse(
    'https://cdn.example.com/releases/app-updates.json',
  ),
  trustedPublicKeys: trustedManifestPublicKeys,
);

final result = await updater.checkAndPrepare();

RESTful API #

A REST endpoint can select releases dynamically from request parameters. The default fetcher sends a GET, so put selection inputs in the URL and optional credentials in headers:

GET /v1/updates HTTP/1.1
Host: updates.example.com
Accept: application/json
Authorization: Bearer <short-lived-token>
final endpoint = Uri.https(
  'updates.example.com',
  '/v1/updates',
);

final updater = await AppUpdater.fromManifest(
  manifestUrl: endpoint,
  trustedPublicKeys: trustedManifestPublicKeys,
  options: AppUpdaterOptions(
    headers: {
      'Accept': 'application/json',
      'Authorization': 'Bearer $accessToken',
    },
  ),
);

final result = await updater.checkAndPrepare();

The default transport requires HTTP 200, accepts at most 1 MiB, and parses the response body directly as either a manifest v3 object or a signed envelope. Do not wrap it in { "data": ... }, { "result": ... }, or another business response object. If an existing API cannot return that shape, provide a custom ManifestFetcher. Caller headers are removed after a cross-origin redirect.

For either delivery style, a bare response is accepted only for official-store or Android-market actions when ManifestSignaturePolicy.optional is used. Self-hosted package or installer actions require a valid Ed25519 envelope. Signing every production response is the recommended policy.

The CLI manifest sign validates the payload schema and remote action policy before signing; invalid input leaves an existing output untouched. manifest verify validates a plain payload, not a signed envelope. Runtime ManifestSignatureVerifier verifies envelopes against host-owned trusted keys and timestamp policy before parsing the payload.

Publish and renew a signed manifest #

Create a payload template, then calculate metadata from the final artifact:

dart run flutter_app_updater manifest generate > manifest-payload.json
dart run flutter_app_updater hash app.apk
wc -c < app.apk

The byte-count command above is for macOS/Linux. In PowerShell, use (Get-Item app.apk).Length.

Edit the template's app identity, version/build, platform/channel, artifact URL, exact byte size, and SHA-256. generate contains example values, and sign does not calculate artifact metadata. Validate and sign the edited payload:

dart run flutter_app_updater manifest verify manifest-payload.json
# The release environment supplies FLUTTER_APP_UPDATER_ED25519_PRIVATE_KEY_BASE64.
dart run flutter_app_updater manifest sign manifest-payload.json \
  --key-id release-2026-01 --expires-in 24h --output manifest-signed.json

The private-key environment variable contains a Base64-encoded raw 32-byte Ed25519 seed, not PEM. Keep it in the publishing environment. In the application, trustedManifestPublicKeys contains the matching raw 32-byte public key:

const trustedManifestPublicKeys = <String, String>{
  'release-2026-01': '<Base64-encoded raw public key>',
};

Serve manifest-signed.json at the configured manifest URL. A signed envelope expires even when the release payload has not changed: the default maximum validity is seven days, and the command above issues a 24-hour envelope. Renew and republish before expiresAt, including periods without a new app release. The manifest URL should remain stable while its signed response is replaced; keep CDN cache lifetimes within the remaining validity window. Artifact URLs must continue to serve the same immutable bytes and digest. Without renewal, clients reject the expired response; they do not silently skip verification.

Manifest v3 response schema #

{
  "schemaVersion": 3,
  "appId": "com.example.app",
  "channel": "stable",
  "releases": [
    {
      "version": "2.0.0",
      "buildNumber": "42",
      "platform": "android",
      "architecture": "arm64",
      "releaseNotes": "Bug fixes",
      "releasedAt": "2026-07-03T10:00:00Z",
      "policy": {
        "level": "recommended",
        "minSupportedVersion": "1.5.0"
      },
      "actions": [
        {
          "type": "downloadAndInstallPackage",
          "packageUrl": "https://example.com/app.apk",
          "packageType": "apk",
          "packageSizeBytes": 25600000,
          "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        }
      ]
    }
  ]
}

Manifest fields: required and optional #

Root object:

Field Required Type and constraints Meaning
schemaVersion Yes Integer, exactly 3 Selects the manifest contract.
appId Yes Non-empty string Must equal the client's expectedAppId.
channel Yes Non-empty string Default channel inherited by releases that omit channel.
releases Yes Array Ordered release candidates; it may be empty when no release exists.

Each item in releases:

Field Required Type and constraints Meaning
version Yes Semantic-version string Used as the primary update ordering key.
platform Yes android, ios, macos, or windows for the stable package Must exactly match the client platform.
releaseNotes Yes Non-empty string Publisher text shown by the host application.
actions Yes Non-empty array Delivery choices in publisher order; the first allowed supported action is recommended.
buildNumber No Non-negative ASCII decimal integer string; iOS/macOS also allow one to three dot-separated integer components Breaks ties when semantic versions are equal; components are compared numerically.
channel No Non-empty string Overrides the root channel for this release.
architecture No Non-empty string Exact runtime architecture; omit it for a universal release.
releasedAt No ISO-8601 string Publisher release timestamp.
policy No Object Update recommendation and minimum supported version.

Optional policy fields:

Field Required Type and constraints Meaning
level No optional, recommended, or required; defaults to optional How strongly the host should present the update.
minSupportedVersion No Semantic-version string no greater than this release's version Makes the update required when the installed version is older.

Action-specific fields:

Every action object requires a non-empty type discriminator. The value selects one row below; the remaining required and optional fields are exact for that action type.

type Required fields Optional fields Notes
openStore store, storeUrl None store is googlePlay, appStore, or macAppStore; the remote URL must use the matching official host.
openAndroidMarket market, targetPackageName fallbackUrl targetPackageName must equal the manifest appId.
downloadPackage packageUrl, packageType, packageSizeBytes, sha256 None Exact positive size and a 64-hex-character SHA-256 are mandatory.
downloadAndInstallPackage packageUrl, packageType, packageSizeBytes, sha256 None Runtime installation supports Android APKs only.
openInstaller installerUrl, installerType, installerSizeBytes, sha256 None Stable types are Windows msix/msi/exe and macOS dmg/zip.
installPackage packagePath packageType (defaults to apk) Trusted local typed code only; remote manifests reject this action.

buildNumber is optional. Android and Windows require a non-negative ASCII decimal integer string (0, 42, and 00042 are valid). Leading zeroes are allowed and the value is parsed as an integer. iOS and macOS additionally accept one to three integer components, such as 12.3.4. Missing trailing components compare as zero: 12.3 equals 12.3.0, and 12.10 is newer than 12.9. The same rule applies to installedBuildNumber; prerelease suffixes are not supported. Any other value rejects the entire response. Older clients still reject dotted manifest builds; keep publishing integer builds until those clients are upgraded, or omit build numbers when same-version comparison is unnecessary. minSupportedVersion, when present, must be less than or equal to that release's version.

Manifest v3 is an exact allowlist at every object boundary: the root, each release, policy, and action object reject unknown fields. There is no extensions escape hatch. The signed envelope also rejects extra fields. Adding a field requires a new schema version. A v3 response with any unknown, mis-typed, or invalid field rejects the entire response rather than ignoring the field.

All remote URLs must be absolute. Remote policy further requires HTTPS, exact positive artifact sizes, and a 64-character hexadecimal sha256 for every self-hosted artifact. Removed earlier-schema field names are rejected anywhere in the document; use only the v3 fields listed above.

The parser also recognizes linux/fuchsia platform values and appImage/deb/rpm installer values for typed model compatibility. They are not stable default executor or plugin-registration support and can end in NO_SUPPORTED_ACTION; the platform matrix below is the executable contract.

This JSON is the signed payload, not the network response. For self-hosted actions, encode the exact payload bytes in a versioned Ed25519 envelope with keyId, issuedAt, expiresAt, payload, and signature. Rotate keys by shipping an overlap period in which the host trusts both key IDs. Bare remote manifests are accepted only for official-store or Android-market actions when the host uses the optional signature policy.

Signed network response fields are all required. The envelope is also an exact allowlist; any extra field rejects the response:

{
  "format": "flutter_app_updater.ed25519.v1",
  "keyId": "release-2026-01",
  "issuedAt": "2026-07-14T12:00:00Z",
  "expiresAt": "2026-07-15T12:00:00Z",
  "payload": "<Base64 of the exact manifest JSON bytes>",
  "signature": "<Base64 Ed25519 signature>"
}
Field Required Type and constraints
format Yes Exactly flutter_app_updater.ed25519.v1.
keyId Yes Non-empty key identifier present in trustedPublicKeys.
issuedAt Yes ISO-8601 validity-window start.
expiresAt Yes ISO-8601 validity-window end; later than issuedAt and within the configured maximum validity.
payload Yes Base64 of the exact manifest v3 JSON bytes.
signature Yes Base64 Ed25519 signature over the package's domain-separated input.

Recipes #

Official Store #

{
  "type": "openStore",
  "store": "googlePlay",
  "storeUrl": "https://play.google.com/store/apps/details?id=com.example.app"
}

Use appStore for iOS and macAppStore for macOS.

Chinese Android Markets #

{
  "type": "openAndroidMarket",
  "market": "xiaomi",
  "targetPackageName": "com.example.app",
  "fallbackUrl": "https://app.mi.com/details?id=com.example.app"
}

Supported market names: huawei, honor, xiaomi, oppo, vivo, meizu, tencentMyApp, and generic.

Self-Hosted Android APK #

Self-hosted APK installation is intended for policy-compliant enterprise, private, or non-Play distribution. Google Play builds should use store actions and must not request REQUEST_INSTALL_PACKAGES solely to update themselves. The plugin does not add this sensitive permission automatically. Apps that are eligible to install APKs must opt in from their application manifest:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

AndroidInstallPermission().status() distinguishes granted, denied, and notDeclared. After an explicit user gesture, openSettings() opens this host's installation settings. Recheck status() on return and ask the user to confirm the action again. Before API 26 there is no per-app authorization; granted does not bypass the system's legacy unknown-sources setting.

Use one action when you want the package to download and then start Android installation:

{
  "type": "downloadAndInstallPackage",
  "packageUrl": "https://example.com/app.apk",
  "packageType": "apk",
  "packageSizeBytes": 25600000,
  "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

Use separate actions when your app wants to download now and install later:

{
  "type": "downloadPackage",
  "packageUrl": "https://example.com/app.apk",
  "packageType": "apk",
  "packageSizeBytes": 25600000,
  "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

Remote manifests cannot request installation of an arbitrary local path. A host may construct an InstallPackageAction only at a trusted typed boundary, for example after its own verified download. For ordinary local APKs, Android rechecks exact size and SHA-256 only when both are supplied; a path-only action checks package identity and signing lineage without an expected size or digest. Pass the original trusted download metadata when creating the install action:

// downloaded is the successful result of performing downloadAction.
final installAction = InstallPackageAction(
  packagePath: downloaded.file!.path,
  packageSizeBytes: downloadAction.packageSizeBytes,
  sha256: downloadAction.sha256,
);
// Call updater.perform(installAction) after explicit installation confirmation.

Foreground download-and-install supplies this metadata automatically. Managed background APKs are rechecked against their persisted size and SHA-256 at installation, including actions returned by createInstallAction().

Advanced Android-only background downloads #

AndroidBackgroundDownloadManager is an opt-in API for one durable, user-visible APK transfer at a time. It is separate from the default AppUpdater action flow and is available only on Android.

Foreground downloads and Android durable downloads have different URL persistence contracts. The foreground action flow may use an HTTPS artifact URL with short-lived query credentials. The foreground checkpoint stores only a SHA-256 URL fingerprint, never the raw URL or query token. Android durable start() accepts only a stable, credential-free entry URL with no userinfo, query, or fragment. The durable task record persists only that stable entry URL.

For expiring download credentials, configure the stable entry endpoint to return an HTTPS redirect to a short-lived signed URL. The signed redirect target exists only as the current process's in-memory transport target and is never persisted. Each redirect target is revalidated, and every HTTPS-to-HTTP downgrade is rejected.

A start request must come from a visible, user-initiated host flow and must include that stable HTTPS URL, the exact content length, and a lowercase or uppercase SHA-256 digest:

final downloads = AndroidBackgroundDownloadManager();
final task = await downloads.start(
  DownloadPackageAction(
    packageUrl: Uri.parse('https://downloads.example.com/app.apk'),
    packageType: PackageType.apk,
    packageSizeBytes: 25600000,
    sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  ),
);

await for (final snapshot in downloads.watch(task.id)) {
  updateBackgroundDownloadUi(snapshot);
}

Use list(), listUnfinished(), and get() after startup to reconcile your UI with durable native state. Call resume() only in response to a user action when a task is waiting or paused. cancel() is terminal. Call remove() only after cancel or another terminal result; it then deletes the durable task and its private artifact. Canceling an already canceled task is idempotent; completed/failed tasks reject cancellation. Neither path cancels a newer task's shared Android job. Stale terminal-task notification actions are ignored rather than restoring an older notification. Scheduling and cancellation recheck task ownership under the same admission lock, so a delayed notification retry cannot replace a newer task's shared Android job or notification. Notification controls run independently of installer verification and do not wait for startup APK hashing. Their actual scheduling path still checks the required host setup; notification broadcast handling does not wait for unrelated long-running I/O.

If cancellation commits before a worker opens its partial file, the worker closes that writer and retries artifact cleanup before finishing. A cleanup failure keeps the task canceled; repeated cancel(), remove(), or startup reconciliation can retry cleanup after storage access returns.

Storage failures become waitingForStorage, including failures entering running or persisting completion. If storage also rejects that state write, the durable snapshot can retain its previous status. Once storage access is restored, explicit resume() retries this recorded recovery in the same process; it does not resume an unrelated active execution. Valid APKs and checkpoints are reverified and reused. This recovery also covers startup directory-listing, state-read, and state-write failures after automatic retries are exhausted: restore storage access and explicitly resume the original startup task in the same process. Directory-listing failures are reported as storage failures rather than an empty task list. Startup recovery does not take over newly admitted tasks or registered active executions, and startup retries do not pause newer revisions. Newly admitted tasks have separate execution-bound recovery: a temporary state-read failure or system stop before the first readable snapshot still leaves explicit resume available after storage access returns. The recovery is bound to the admitted execution and checked against the durable revision; it cannot take over an ordinary active task or overwrite a newer revision or terminal result. If a running-state, checkpoint, or completion write fails and the next read also fails, recovery retains that execution's last trusted snapshot. A verified APK already on disk can be reused without another download.

Corrupt-state recovery advances beyond the last observed task revision so current and reattached observers receive the failure instead of stale progress. Malformed raw maximum revisions are ignored as recovery floors; an already observed maximum counter saturates as a terminal failure and remains removable. Native observers and Dart watch() accept that saturated corrupt-state failure once even at the same revision; watch() then closes on the terminal snapshot.

On API 21–33, a canceled or completed task's foreground worker can still be finishing when another download is accepted. The new task then moves to pausedBySystem instead of remaining queued without a worker. Observe watch() or refresh get(), and offer a user-triggered resume() after the old worker has stopped. Duplicate starts for the same execution reuse its worker. An explicit resume for a newer revision of that same task is retained during old-worker cleanup and starts after cleanup; a successful resume need not be repeated. On API 34+, network changes stop only the execution bound to the old network lease. A change before the first lease uses the new network without pausing it. A system stop before engine execution also leaves a nonterminal task explicitly resumable. Job completion coordinates release of the local execution slot with the system completion callback, so the next accepted job is not lost during handoff. Delayed callbacks from the old job cannot stop or finalize that new job. UIDT callbacks check an internal generation token as well as the task and job IDs; this does not change public task IDs or notification actions. Deferred recovery is bound to the task revision captured when the service start was scheduled. Once a retry advances that revision, an older callback cannot pause it, including during its intermediate queued state before running. On other platforms, manager operations throw BackgroundDownloadException with UpdateErrorCode.backgroundDownloadUnavailable before calling native code. Invalid task IDs or action metadata throw ArgumentError on Android. IDs are trimmed, then must match [A-Za-z0-9_-]{1,80}, consistently with native storage.

Durable task state is stored under Android noBackupFilesDir; APK and partial artifacts are stored under the app-private filesDir exposed by the package's FileProvider. On first use after upgrading, tasks and artifacts from the pre-release single-root layout are reset instead of migrated. Hosts must treat those old tasks as no longer resumable and start a fresh user-visible transfer.

Required host manifest #

The plugin manifest deliberately does not opt applications into background execution. Merge all of the following declarations into the host app's android/app/src/main/AndroidManifest.xml, keeping the app's existing activities, providers, metadata, and other components:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />

    <application>
        <!-- Keep the host's existing activity and application declarations. -->
        <service
            android:name="com.indiegeeker.flutter_app_updater.background.UserInitiatedDownloadJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false" />
        <service
            android:name="com.indiegeeker.flutter_app_updater.background.BackgroundDownloadForegroundService"
            android:foregroundServiceType="dataSync"
            android:exported="false" />
        <receiver
            android:name="com.indiegeeker.flutter_app_updater.background.BackgroundDownloadActionReceiver"
            android:exported="false" />
    </application>
</manifest>

The host application owns notification permission: it decides when and how to request POST_NOTIFICATIONS at runtime on Android 13+, explains the visible transfer to the user, and handles denial. The package declares neither this runtime request nor a battery-exemption prompt. A manifest declaration alone does not grant notification permission.

On API 21-25 the package enters the visible foreground-service lifecycle after startService; on API 26-33 it does so after startForegroundService. The dataSync foreground-service type has platform meaning on API 29+. A process stop or network loss can leave durable state that the host shows after reopen; recovery is explicit through resume() or the user-visible notification action. There is no unattended API 21-33 network recovery guarantee. On API 34+ the package submits a user-initiated data transfer job with an internet requirement. Android still controls admission, stop reasons, and execution time, so the host must handle scheduling rejection and expose retry.

Google Play distribution does not make self-hosted APK updates or foreground services policy-compliant. Play apps should normally use store delivery, and the publisher remains responsible for current foreground-service and user-initiated-transfer declarations, eligibility, and review requirements. Do not add REQUEST_INSTALL_PACKAGES solely to self-update a Play build.

Server and installation contracts #

Production artifacts must use HTTPS and immutable bytes. Supply the exact content length and SHA-256 to start(). Safe and efficient resume requires the server to support Range: bytes=N-, return a precise 206 Content-Range, and keep a strong ETag stable for those bytes so If-Range cannot append a changed artifact. A server that ignores Range may cause a clean restart; weak or changing validators are not resume evidence.

Download completion and installation are deliberately separate:

final installAction = await downloads.createInstallAction(task.id);
// createInstallAction revalidates the private APK but does not install the APK.
final installResult = await updater.perform(installAction);

createInstallAction() rechecks the private file, expected size, SHA-256, package name, and signing lineage, then returns an InstallPackageAction. Only the later explicit AppUpdater.perform() call can open Android's package installer, which remains the final installation authority. The plugin does not provide silent installation.

Recovery limits #

Native records and partial bytes survive Flutter engine detach and ordinary process recreation. They do not make Android continue work after force-stop, and jobs are not persisted across reboot. Reopen the app, call listUnfinished(), and let the user choose resume() where the state permits it. Recents swipe, Task Manager Stop, battery restriction, background-start limits, storage pressure, and OEM process management can stop or reject execution.

The implementation does not use or promise WorkManager or DownloadManager, does not request battery exemptions, and does not promise uninterrupted background execution on any OEM family.

iOS App Store #

{
  "type": "openStore",
  "store": "appStore",
  "storeUrl": "https://apps.apple.com/app/id123456789"
}

macOS and Windows Installers #

{
  "type": "openInstaller",
  "installerUrl": "https://example.com/app.msi",
  "installerType": "msi",
  "installerSizeBytes": 82000000,
  "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}

Supported stable installer types:

  • Windows: msix, msi, exe
  • macOS: dmg, zip

Platform Matrix #

Platform Official store Chinese markets Package download Package install Desktop installer
Android Stable Stable Stable Stable Not applicable
iOS Stable Not applicable Unsupported Unsupported Not applicable
macOS Stable Not applicable Stable Unsupported Stable
Windows Unsupported Not applicable Stable Unsupported Stable

Unsupported actions return structured failures instead of throwing platform exceptions through the public API.

Error Handling #

checkAndPrepare() returns:

  • PreparedUpdateAvailable
  • PreparedUpdateNotAvailable
  • PreparedUpdateCheckFailed

perform() and performRecommended() return UpdateActionResult.

Useful error codes include:

  • MANIFEST_FETCH_FAILED
  • MANIFEST_INVALID
  • APP_ID_MISMATCH
  • NO_SUPPORTED_ACTION
  • STORE_NOT_AVAILABLE
  • MARKET_NOT_AVAILABLE
  • PACKAGE_DOWNLOAD_FAILED
  • PACKAGE_TOO_LARGE
  • PACKAGE_HASH_MISMATCH
  • PACKAGE_INSTALL_PERMISSION_REQUIRED
  • PACKAGE_FILE_NOT_FOUND
  • PACKAGE_INSTALL_FAILED
  • INSTALLER_OPEN_FAILED
  • PLATFORM_NOT_SUPPORTED
  • DOWNLOAD_IN_PROGRESS
  • ACTION_FAILED
  • ACTION_CANCELED

Example #

The bundled example opens on a configurable update simulator with no external side effects. It also includes a separately labeled production integration tab that is disabled by default. When explicitly configured, that tab exercises the real signed-manifest fetch, verification, identity binding, selection, preparation, and user-confirmed action path. See example/README.md for the safety boundary, configuration, and runnable commands.

Maintainer Verification #

The supported floor is Flutter 3.29.0. The reusable full gate tests that exact version and current stable Flutter, validates root/example analysis and tests, enforces total and critical coverage at 80%, generates API docs, builds every registered platform example, runs Android and Windows native tests, and checks a clean publish archive. Run this local subset with the current stable SDK; repeat package/example analysis and tests with Flutter 3.29.0.

flutter pub get
(cd example && flutter pub get)
dart format --output=none --set-exit-if-changed lib test example/lib example/test example/integration_test tool
flutter analyze --no-pub
flutter test --coverage --no-pub
dart pub global activate dartdoc 9.0.8
dart pub global run dartdoc --no-generate-docs
(cd example && flutter analyze --no-pub && flutter test --no-pub)
(cd example && flutter build apk --debug --no-pub)
bash tool/ci/publish_dry_run.sh

The docs check uses dartdoc 9.0.8, matching CI, because the current SDK's bundled 9.0.6 can crash on Flutter @docImport comments. This is a documentation tool compatibility requirement on the current SDK, not a change to the package's Flutter 3.29 support. The minimum-SDK Android gate separately builds a fresh host with Flutter 3.29.0's default Kotlin 1.8.22 toolchain.

The Android-native reliability gate runs from example/android:

../../android/gradlew :flutter_app_updater:testDebugUnitTest :flutter_app_updater:lintDebug :app:processDebugMainManifest

For a release, update pubspec.yaml and CHANGELOG.md, merge the release commit to main, and tag that exact commit as v<version>. The publish workflow runs the same full gate and requires tag, version, CHANGELOG, and origin/main ancestry to agree. Validate locally with:

version="$(awk '/^version:/ {print $2; exit}' pubspec.yaml)"
dart run tool/ci/verify_release_metadata.dart --tag "v$version"

See CONTRIBUTING.md for development checks and SECURITY.md for private vulnerability reporting.

License #

Apache-2.0

3
likes
140
points
1.46k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Verified app updates with default or custom UI for stores, Android markets, packages, and desktop installers.

Repository (GitHub)
View/report issues
Contributing

Topics

#app-update #app-distribution #flutter-plugin #release-management #self-hosted

License

Apache-2.0 (license)

Dependencies

crypto, cryptography, ffi, flutter, package_info_plus, path_provider, plugin_platform_interface, pub_semver

More

Packages that depend on flutter_app_updater

Packages that implement flutter_app_updater