desktop_updater

Flutter desktop updater plugin for macOS, Windows, and Linux.

2.x uses one small update index, one release descriptor, and one verified zip:

app-archive.json -> release.json -> app.zip

No public folder listing is required. Clients fetch exact URLs and verify the zip length and SHA-256 before installation.

flutter_desktop_updater

Quick Start

Add the package:

dependencies:
  desktop_updater: ^2.3.0

Point your app at the hosted archive:

final controller = DesktopUpdaterController(
  appArchiveUrl: Uri.parse("https://updates.example.com/app-archive.json"),
);

Add desktop_updater.yaml at your app repository root, next to pubspec.yaml:

updates:
  baseUrl: https://updates.example.com

Publish one platform:

dart run desktop_updater:release publish --platform macos

Before your first production release, run:

dart run desktop_updater:release doctor --platform macos

With only updates.baseUrl, publish creates an upload-ready package under dist/desktop_updater and prints the manual upload and validate instructions. With an upload provider configured, it uploads versioned files first, validates them, uploads app-archive.json last, then validates hosted update selection.

EL10

Think of your update host as a shelf on the internet:

  1. The app reads app-archive.json.
  2. The archive says which release.json is newest for this platform/channel.
  3. release.json points to one zip and records its size and hash.
  4. The app downloads the zip only after the metadata says it is a valid update.
  5. The app verifies the zip before staging or installing it.

Publish does the reverse: create the zip, create release.json, update app-archive.json, upload the versioned files first, then expose the new archive last.

Ready-Made UI

Use the stock inline card:

DesktopUpdateWidget(
  controller: controller,
  child: const YourHomePage(),
)

Other built-in surfaces:

  • DesktopUpdateDirectCard
  • DesktopUpdateSliver
  • UpdateDialogListener

See Ready-made UI widgets for screenshots, placement guidance, and when to choose each surface.

For custom UI, switch on controller.state.

Release Notes

Use releaseNotesLoader when notes should depend on the selected descriptor, platform, channel, locale, account, or environment:

final controller = DesktopUpdaterController(
  appArchiveUrl: Uri.parse("https://updates.example.com/app-archive.json"),
  releaseNotesLoader: (descriptor) {
    return myNotesApi.fetch(
      version: descriptor.version,
      platform: descriptor.platform,
      channel: descriptor.channel,
    );
  },
);

For a simple hosted file, pass releaseNotesUrl instead:

final controller = DesktopUpdaterController(
  appArchiveUrl: Uri.parse("https://updates.example.com/app-archive.json"),
  releaseNotesUrl: Uri.parse("https://updates.example.com/release-notes.json"),
);

The simple contributor-friendly JSON shape uses a data array:

{
  "data": [
    { "type": "feat",  "message": "Add dark mode support" },
    { "type": "fix",   "message": "Fix crash on startup" },
    { "type": "other", "message": "General stability improvements" }
  ]
}

The richer package-owned shape supports sections, summaries, and item titles:

{
  "schemaVersion": 1,
  "format": "desktop_updater.release_notes.v1",
  "summary": "Quality improvements.",
  "sections": [
    {
      "type": "features",
      "title": "New features",
      "items": [
        { "body": "Add dark mode support" }
      ]
    }
  ]
}

The ready-made card shows a release notes icon when the active update can load notes. Custom UI can call controller.loadReleaseNotes() and render controller.releaseNotesState; the controller keeps caching, retry state, and descriptor context aligned.

Localise the bottom sheet and override section labels via DesktopUpdateLocalization:

localization: const DesktopUpdateLocalization(
  releaseNotesTitleText: "What's new",
  releaseNotesButtonTooltipText: "Release notes",
  releaseNotesTypeLabels: {
    "feat": "New features",
    "fix":  "Bug fixes",
    "other": "Other changes",
  },
  releaseNotesErrorText: "Could not load release notes.",
  releaseNotesRetryText: "Retry",
  releaseNotesEmptyText: "No release notes available for this version.",
),

Error Tooltip

When an update fails the error icon shows a tooltip. Supply an onUpdateFailedTooltip callback to return a custom string, or set updateFailedTooltipText for one static fallback:

localization: DesktopUpdateLocalization(
  updateFailedTooltipText: "Update failed. Please try again.",
  onUpdateFailedTooltip: (error) {
    if (error is SocketException) return "No internet connection.";
    if (error is TimeoutException) return "Connection timed out.";
    return null; // falls back to updateFailedTooltipText
  },
),

Diagnostics And Recovery

2.2.0 adds opt-in diagnostics and recovery for support flows. The default stays quiet: no package-owned files, uploads, telemetry, or storage.

Use in-memory problem reports for normal support, add an app-owned diagnostics sink for durable Dart lifecycle logs, and add diagnosticsLogPath plus an app-owned UpdateRecoveryStore only when support needs post-exit native helper evidence.

Details live in Diagnostics and recovery, Ready-made UI widgets, and Publishing desktop updates.

Production Trust

desktop_updater handles update mechanics. Your app still owns platform trust:

  • macOS production updates should be Developer ID signed, hardened-runtime enabled, notarized, stapled, and Gatekeeper accepted before packaging.
  • Windows production updates should use Authenticode when publisher trust is required.
  • Linux direct zip distribution should add descriptor signing or another publisher-authenticity policy when production trust matters.

Documentation

Advanced Commands

Most apps should start with release publish. Use low-level commands only when your pipeline needs to own each step:

dart run desktop_updater:package --help
dart run desktop_updater:app_archive --help
dart run desktop_updater:verify --help