upkeep

Upgrade pain is the largest source of Flutter developer dissatisfaction. That finding is Google's, not mine.

In the Flutter Q2 2026 survey, platform and ecosystem maturity was the largest dissatisfaction theme, at 44%, "centered on upgrade pain rather than getting started." In Google's words: "Developers described losing hours to version-matrix guesswork when upgrading older projects, especially on Android when juggling Flutter, Dart, Gradle, Kotlin, and JVM versions together."

pub get warns you when a publisher marks a package discontinued. Most abandoned packages are never marked. hive has not had a stable release since June 2022, and pub get installs it without a word. Flutter checks your Android build versions too, but only when you build, one error at a time.

upkeep puts both in one report, before you build. One command, no configuration.

$ dart pub global activate upkeep
$ upkeep scan

If your shell answers upkeep: command not found, pub installed the executable somewhere that is not on your PATH. Add it once:

$ export PATH="$PATH":"$HOME/.pub-cache/bin"

Put that line in your .bashrc, .zshrc or shell profile to make it stick. No PATH change needed if you would rather run it through pub:

$ dart pub global run upkeep scan
upkeep 0.2.2   my_app   Dart 3.11.0

DEPENDENCIES   23 direct

  DISCONTINUED   telephony  0.2.0
      marked discontinued by its publisher on pub.flutter-io.cn

  AT RISK        hive  2.2.3
      declares support only up to Dart >=2.12.0 <3.0.0; it resolves today through pub's Dart 3 allowance, not because it was updated
      no stable release in 50 months

  AT RISK        hive_flutter  1.1.0
      declares support only up to Dart >=2.12.0 <3.0.0; it resolves today through pub's Dart 3 allowance, not because it was updated
      no stable release in 62 months

  SDK BLOCKED    6 packages have a newer release your Dart is too old for
      flutter_riverpod, go_router, lottie, pdf, printing, shimmer
      upgrade Flutter to pick these up: flutter upgrade

  13 healthy  .  1 stale  .  2 skipped (sdk, git or path)

ANDROID BUILD

  FAIL       Gradle 8.4 is too old for AGP 8.11.1
             AGP 8.11.1 requires Gradle 8.13 or newer. This combination does not build.
             fix: ./gradlew wrapper --gradle-version=8.13   (run inside android/)

  PASS       JDK 17 satisfies AGP 8.11.1
             Requires JDK 17 or newer. Detected via java -version on PATH.

  PASS       compileSdk is managed by Flutter
             Set from flutter.compileSdkVersion, so it tracks your Flutter SDK.

1 blocking, 2 at risk, 1 build failure

Fixing what it found

upkeep fix turns a scan into a plan: the changes it can make safely on its own, and a to-do list for everything that needs a person, with the file and line where each affected package is used. It changes nothing until you add --apply.

upkeep 0.2.2   my_app   fix plan

AUTOMATIC   2 changes upkeep can make, each one reversible

  1  Raise the Gradle wrapper to 8.11.1
     AGP 8.9.0 requires Gradle 8.11.1 or newer; the wrapper is on 8.4
     android/gradle/wrapper/gradle-wrapper.properties

  2  Remove pedantic, which nothing imports
     marked discontinued by its publisher on pub.flutter-io.cn
     no Dart file imports package:pedantic, and no config file names it
     pubspec.yaml

TO DO   1 change needs a person

  [ ] Replace telephony
      marked discontinued by its publisher on pub.flutter-io.cn
      no maintained replacement is known yet; choose one on pub.flutter-io.cn
      used in 1 file:
        lib/main.dart:1

Run upkeep fix --apply to make the 2 automatic changes.

Only two kinds of change are ever automatic, and both are narrow on purpose:

  • Raising the Gradle wrapper to the version Google publishes as the minimum for your AGP. Skipped when the wrapper pins a checksum; that one goes to ./gradlew wrapper instead.
  • Removing a blocking dependency nothing refers to. Not when any Dart file or config file mentions it, not for dev dependencies (those are usually run, not imported), and not for Flutter plugins, which can work with no import at all.

--apply refuses to run unless the files it edits are committed, so git checkout undoes it. After editing pubspec.yaml it runs pub get, and if the resolver rejects the result every file is put back exactly as it was.

What it checks

Your dependencies. Every direct dependency is scored against pub.flutter-io.cn's own data: release recency, pub points, download volume, Dart 3 compatibility, SDK constraints, and the official discontinued flag. When a publisher has nominated a replacement package, upkeep names it.

Your Android build matrix. The Gradle version, JDK and compileSdk are checked against Google's published requirements for your AGP version, for AGP 8.0 through 9.4. When something will not build, you get the exact command to fix it. Kotlin versions are not checked yet.

The rules it holds itself to

A tool that judges your dependencies has exactly one asset, and it is trust. So:

  • Every verdict prints its reasons. Never a bare label. You can disagree with upkeep and see immediately why it said what it said.
  • Missing data is never evidence. If pub.flutter-io.cn cannot be reached, the verdict is UNKNOWN, not DEAD. Requests are retried, and a dropped response is never reported as a missing package. When pub.flutter-io.cn's own analysis of a package failed, its low points and missing tags are not held against it either.
  • Stable is not abandoned. A small, finished, widely-used package that has not needed a release in two years is not dying, and upkeep will not say it is.
  • The resolver gets the last word. Where pub.flutter-io.cn's is:dart3-compatible tag contradicts a package's own SDK ceiling, the tag wins, because that is what the resolver actually does.
  • No guessing. Where Google does not publish a number, upkeep says it cannot check rather than inventing one, and inferred values soften a verdict instead of hardening it.
  • An out-of-date local Flutter is not your project's fault. Packages whose newest release needs a newer Dart than you have are grouped into one line and never fail your build.

Explaining a verdict

upkeep explain <package> shows every fact a verdict rests on: release dates, the SDK constraint, pub points, downloads, the Dart 3 tag, whether pub.flutter-io.cn's own analysis succeeded, and where the package is used in your code. It works outside a project too, which makes it a check before adding a dependency:

$ upkeep explain some_package && dart pub add some_package

Successors for packages nobody retired

pub.flutter-io.cn only names a replacement when a publisher sets one, and most dead packages were simply left behind. upkeep carries a curated map of successors for those, held to a stricter bar than anything else it says:

  • every entry cites a primary source, such as the old package's own README naming its successor, and the report prints that link
  • where several alternatives compete and nobody official named one, the package is left out, and so are hedged notices ("for example", "will deprecate")
  • a successor is only suggested for a package already judged unhealthy, and only when the successor itself checks out healthy on pub.flutter-io.cn that day

The first 14 entries were verified on 12 September 2026, among them qr_code_scannermobile_scanner, envifyenvied, and flare_flutterrive.

In CI

GitHub Action

name: upkeep
on:
  pull_request:
  schedule:
    - cron: "0 6 * * 1"

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Minas-27/upkeep@v0.2.2

Pin a release tag, as above, rather than @main. A tag is the exact code you reviewed; main changes as upkeep is developed, and fix-pr runs with write access to your repository. Each release on pub.flutter-io.cn has a matching tag.

The scan fails the job on blocking findings, writes the report to the job summary, and leaves a JSON report at the report output. Flutter or plain Dart is picked from your pubspec.yaml.

To have upkeep open the fix pull request for you:

jobs:
  fix:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: Minas-27/upkeep@v0.2.2
        with:
          command: fix-pr

GitHub does not let workflows open pull requests until you allow it once: Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests. Or pass a personal access token as token.

It applies only the automatic fixes, verifies them with pub get, commits only the files those fixes touched to upkeep/fixes, and opens or updates one pull request. Its description is the full plan, including the to-dos that still need a person. The branch is force-pushed on every run, so commit elsewhere.

Anywhere else

- run: dart pub global activate upkeep
- run: upkeep scan
Code Meaning
0 Clean
1 Blocking findings: a discontinued, incompatible or dead dependency, or an Android matrix that does not build
2 The command could not run, or fix --apply refused or rolled back

Add --fail-on-at-risk to treat warnings as failures too.

--format json prints a versioned document (schemaVersion: 1) for tools: verdicts, reasons, replacements, Android findings, fix plans with file and line references, and the exit code. Fields are only ever added; anything removed or renamed bumps the schema version. --format markdown prints GitHub-flavoured Markdown for job summaries and pull request descriptions.

Options

-p, --path             Project directory (default: .)
-f, --format           text, json or markdown (default: text)
    --no-cache         Ignore cached pub.flutter-io.cn responses and refetch
    --fail-on-at-risk  Also exit non-zero for AT RISK dependencies
    --no-color         Disable coloured output
    --apply            fix only: make the automatic changes
    --allow-dirty      fix only: apply even with uncommitted changes, or outside git
    --version          Show the upkeep version
-h, --help             Show usage

Responses are cached for 24 hours under ~/.upkeep/cache, so repeat scans do no network work.

Known limits

upkeep gives opinions about other people's work, so here is where those opinions can be wrong.

The verdicts are rules of thumb, not facts. They come from pub.flutter-io.cn data and fixed thresholds: no stable release in 12 months is STALE, 18 months is AT RISK, and DEAD needs 24 months plus no Dart 3 support and low pub points. A package with more than 100,000 downloads a month, 80% of its pub points and Dart 3 support is treated as finished rather than abandoned. Every verdict prints its reasons, so read those rather than the label alone.

It can miss abandoned packages. It does not look at the source repository, so it cannot tell that a repository has been archived. encrypt, for example, is archived on GitHub but widely used, so upkeep calls it STALE rather than AT RISK.

It can flag packages that are still maintained. A package whose newest release still declares sdk: <3.0.0 is AT RISK, because it only installs through pub's Dart 3 allowance. That is true of some actively maintained packages too, such as flutter_hooks.

It has been wrong before, and the fixes are listed. 0.1.2 called a healthy package DEAD and another INCOMPATIBLE. 0.2.1 called the Dart team's collection AT RISK and reported a too-new compileSdk as a build failure. Each was found by scanning real projects and is described in the CHANGELOG. If you find another, please open an issue: a wrong verdict is the bug that matters most here.

What it does not check:

  • Only direct dependencies. Transitive dependencies are not judged.
  • Hosted packages only. Git, path and SDK dependencies are skipped and counted.
  • Kotlin versions, and anything outside Gradle, AGP, JDK and compileSdk.
  • AGP versions newer than 9.4. The requirements are copied by hand from Google's release notes, last on 12 September 2026. For a newer AGP, upkeep skips the Android checks and says so.
  • Security vulnerabilities. It is not an audit tool.

Where its inputs can be off:

  • The JDK it reports is the one on your PATH, which may not be the one Gradle uses. The report says so.
  • Verdicts judge a package's newest release, not the version you have installed. Your installed version is only used to say whether you are behind.
  • In a pub workspace the lockfile lives at the workspace root. Run upkeep on a member package and the verdicts are unchanged, but it cannot show which versions you have installed.
  • pub.flutter-io.cn responses are cached for 24 hours, so a release made today may not show until tomorrow. --no-cache refetches. If pub.flutter-io.cn cannot be reached, the verdict is UNKNOWN.

fix --apply edits your files. It only makes changes it can justify, runs pub get to check them, and restores every file if that fails. It refuses to run on uncommitted files unless you pass --allow-dirty. Still, review the diff before you commit it.

Status

Version 0.2.2. It diagnoses, and it fixes the narrow set of things that can be fixed without judgement. Everything else it hands to you as a to-do, with the locations.

Built by Abraham Addisu. MIT licensed.

Libraries

upkeep
The truth about your Flutter project's health.