A Dart package is any folder with a pubspec.yaml; a Flutter package is one that depends on the Flutter SDK; a plugin is a Flutter package that also carries platform code (Kotlin, Java, Swift, Objective-C, C/C++) that the app links in at build time.
Dart package (collection, http): pure Dart, runs anywhere Dart runs, including servers and CLIs
Flutter package (provider, go_router): imports package:flutter, so it needs the Flutter SDK, but has no native code
Plugin (camera, shared_preferences): declares a flutter: plugin: platforms: block; the tool generates the registrant that wires the native half in, which is why a new plugin needs a full rebuild, not a hot restart
FFI plugin: native code called through dart:ffi instead of platform channels (flutter create --template=plugin_ffi)
In practice: a plugin must support every platform you ship; check its platform list on pub.dev before adding it
FlutterMiddle
Packages & Dependencies
Version constraints, pubspec.lock, version solving, overrides, publishing, melos, upgrades
Questions closed
0 / 19
- 0Review
- 2Asked often
Questions
dependencies are what your code needs to run and are inherited by anyone who depends on you; dev_dependencies are tools for working on the package (tests, codegen, lints) and are never inherited; dependency_overrides force a version on the whole graph, and only in the root package.
Rule of thumb: if lib/ imports it, it goes in dependencies; if only test/, tool/ or the command line uses it, it goes in dev_dependencies
Typical split: json_annotation in dependencies (your models import it), json_serializable and build_runner in dev_dependencies (they only run on your machine)
Why it matters for a package: every entry in dependencies becomes a constraint your consumers' solver must satisfy; build_runner there drags analyzer into their resolution
Not about APK size: AOT compilation drops Dart code that main() never reaches, so a misplaced dev tool barely changes the binary; a plugin listed in dependencies ships its native code
Overrides: ignored when someone depends on your package; a temporary workaround, never a fix
An app always commits pubspec.lock. A published package usually does not, because when someone depends on it their app solves the whole graph and the package's lock file is ignored.
What it holds: the exact version and content hash (sha256) of every package, direct and transitive, from the last solve
**pub get replays it: without it, each machine resolves to whatever is newest inside your constraints that day, so your laptop and CI can build different code
In CI**: flutter pub get --enforce-lockfile fails when the lock does not match the pubspec or a downloaded archive's hash, instead of silently rewriting it
Library CI: test both ends of your ranges: dart pub downgrade then the tests, dart pub upgrade then the tests
Merge conflict in the lock: do not hand-edit it; take either side and run pub get again
pub get reuses the versions in pubspec.lock and solves only what is new; pub upgrade re-solves for the newest versions your constraints allow; --major-versions also rewrites the constraints in pubspec.yaml to cross a major version.
**flutter pub get**: what builds and CI run; an already-locked package never moves, which is why a newer dio does not appear on its own
**flutter pub upgrade dio**: ^5.7.0 reaches the newest 5.x and stops; only pubspec.lock changes
**--major-versions**: edits pubspec.yaml (^5.7.0 becomes ^6.0.0); read the changelog, because pub will not
**flutter pub outdated: look before you move: Current, Upgradable, Resolvable and Latest side by side
--tighten**: raises lower bounds to the versions you actually resolved, so the pubspec stops claiming support for versions nobody tested
A caret allows everything up to the next breaking version, and below 1.0.0 the minor number counts as breaking: ^5.7.0 is >=5.7.0 <6.0.0, but ^0.20.2 is only >=0.20.2 <0.21.0.
**dio: ^5.7.0: any 5.x from 5.7.0 on, never 6.0.0
intl: ^0.20.2**: 0.20.x only; a 0.x version signals an unstable API, so 0.21.0 is treated as a new major. Likewise ^0.0.3 means >=0.0.3 <0.1.0
**uuid: 4.5.1: no operator is an exact pin, not a minimum; tolerable in an app, harmful in a published package because it leaves the solver no room
web_socket_channel**: >=3.0.0 <3.0.3 allows 3.0.0 to 3.0.2; the long form is how you exclude a known-bad 3.0.3, since pub has no !=. Quote it in YAML
**crypto: any: every version ever published; the solver may walk back to an ancient one to satisfy someone else's bound
In practice**: use a caret for everything and let pubspec.lock hold the exact versions
A hosted dependency is a version range the solver can negotiate; path and git are one fixed version, and a package using either cannot go to pub.dev. A branch is acceptable only briefly, pinned to a commit, while an upstream fix is pending.
Hosted (pub.dev or a private server): versioned, cached, and reproducible through the lock file's content hash
**path:: right for packages inside one repo (better still, a pub workspace); wrong once it points outside the repo, because it only works on your machine
git:**: every developer and CI runner needs access to that repo; the lock records the resolved commit, but pub upgrade follows a branch ref to its new tip
**Pin ref: to a commit or tag**: or, since Dart 3.9, use tag_pattern: with a version: constraint so pub solves across the repo's tags
When a branch is acceptable: never for long: point at your fork by commit while the upstream PR is open, with a comment linking the PR and the condition for removing it
Either as git dependencies pinned to tags, which needs no infrastructure, or through a private pub repository, which keeps real version solving. Past a handful of shared packages, the private repository usually wins.
Git with tags: zero setup and works with the SSH keys CI already has; with tag_pattern (Dart 3.9+, and your pubspec needs sdk: ^3.9.0) pub can even solve across tags, but every machine needs access to every repo
Private pub repository: Cloudsmith, JFrog Artifactory or a self-hosted server that implements the pub repository API; each package sets publish_to: to that URL so it can never leak to pub.dev
Auth: dart pub token add <url> stores a token per host; in CI add --env-var so the secret stays in an environment variable
Consumers: hosted: <url> plus version: on each private dependency
One repo instead? If all the apps live in one monorepo, a pub workspace removes the publishing step entirely
Discipline: semver and a changelog exactly as on pub.dev, or consumers stop upgrading
Read from the bottom: the last sentence names your package and the two dependencies that cannot coexist; the first Because names the shared package they disagree about. Then move whichever side is free to move.
Why a wall of text: the solver prints a proof; each Because… line justifies the next; the first names the shared package, and only the last sentence is about your package
Collect facts: flutter pub deps --style=compact shows who pulls the shared package in; flutter pub outdated shows whether any release resolves
Fixes, best first: upgrade the lagging dependency, downgrade the one that ran ahead, loosen your own constraint, or upgrade Flutter when an SDK package holds the pin
SDK packages do not move: flutter_localizations and flutter_test pin exact versions of packages such as intl, so the other side has to give
**requires SDK version in the chain means the conflict is with the Dart SDK itself, not with another package
Last resort**: dependency_overrides, with a comment and a ticket to remove it
It tells the solver to use your chosen version of a package everywhere in the graph and to ignore every constraint other packages put on it, so pub get turns green on a combination nobody has tested.
It fails late: the author capped <0.21.0 because something breaks there, often at run time rather than at compile time
Root only: overrides inside a dependency are ignored, so a published package's overrides protect nobody; in a pub workspace any member's overrides apply to the whole workspace, each package at most once
It outlives the problem: easy to miss in review, and months later it blocks the real upgrade
Honest alternatives: move the package that is free to move, or send the upstream PR that widens the constraint and depend on your fork by commit meanwhile
Local experiments: a gitignored pubspec_overrides.yaml next to the pubspec, for pointing at a local checkout
If you must commit one: a comment with the issue link and the condition for removing it
That package's own environment: sdk: range excludes your Dart version, so pub drops every version of it from the solve. A lower bound below 2.12 means the package predates null safety, and Dart 3 cannot run it at all.
Dart ships inside Flutter: you do not choose it separately; a Flutter upgrade is a Dart upgrade
The Dart 3 exception: a null-safe package with >=2.12.0 <3.0.0 still resolves, because pub reads that upper bound as <4.0.0; only pre-null-safety packages are cut off
The lower bound sets the language version: a package with sdk: ^3.0.0 compiles with Dart 3.0 semantics even on a newer SDK, which is why using a newer feature such as null-aware collection elements needs ^3.8.0
Options, best first: upgrade to a newer release (flutter pub outdated shows whether one exists), move to the replacement or maintained fork named on its pub.dev page, migrate a fork yourself, or drop the package
The mirror trap: publishing your own package with a tight upper bound such as <3.9.0 makes you the blocker; ^3.4.0 breaks only at Dart 4
A major, 2.0.0: changes 1 and 2 can each stop a consumer's code from compiling, which is what breaking means. Change 3 on its own would be a minor.
New required parameter: every existing Toast(message: …) call fails to compile
New enum value: Dart 3 switches over an enum are exhaustive, so a consumer's switch without a default stops compiling
Also breaking though it looks additive: a new member on a class others may implements, or a new subtype of a sealed class
Minor: new optional parameters, new classes, functions and extensions, new members on a final class
Patch: bug fixes with no API change
Below 1.0.0: the caret shifts one digit: after 0.4.2, a breaking release is 0.5.0
To stay on 1.x: make icon optional and hold warning back for 2.0.0
Put the public API in lib/ and the implementation in lib/src/, add README.md, CHANGELOG.md, LICENSE and an example/, run dart pub publish --dry-run, then publish. A published version is permanent: it can be retracted within seven days, never deleted.
Layout: lib/acme_result.dart exports only what is public; lib/src/ is private by convention, and the implementation_imports lint warns consumers who import it
Pubspec: name, description, version, repository and an environment: sdk: constraint; dependencies only from pub.dev or the SDK, so no git: or path:
Dry run: validates the pubspec and layout and lists the files that will be uploaded; it does not run your tests. pana locally shows the pub points you will get
Versioning: semver with one CHANGELOG.md entry per release; stay on 0.x while the API is still moving
What is permanent: the name is yours forever; a retracted version is skipped by new resolutions but still works in existing lock files; a whole package can only be marked discontinued. A secret in the archive is leaked for good
Automate: publish from CI on a git tag with GitHub OIDC rather than from a laptop
A workspace (Dart 3.6+) makes the whole repo one resolution: one pubspec.lock, one pub get at the root, and the same version of each dependency in every package. Melos adds what pub does not do: scripts across packages, versioning and changelogs.
Setup: the root pubspec lists members under workspace:; each member adds resolution: workspace and needs sdk: ^3.6.0 or higher
Members depend on each other by version: acme_core: ^1.0.0 resolves to the local package automatically, no path:; outside the workspace the hosted package is used
Consistent by construction: two features cannot quietly use different dio versions; a mismatch fails pub get instead of surfacing at run time
Overrides: allowed in any member and applied to the whole workspace, each package once; keep them in the root. Old pubspec_overrides.yaml files left from Melos 6 still take effect, so delete them
Melos 7: its config lives in the root pubspec.yaml under melos:; linking packages is now the workspace's job
What neither fixes: one member stuck on an old dependency now blocks the upgrade for the whole repo
Check whether it can reach you at all: with pubspec.lock committed and CI on pub get --enforce-lockfile, a bad upstream release changes nothing until someone upgrades on purpose. If it already landed, bound it from your own pubspec.
Find what moved: git diff pubspec.lock shows which versions changed; flutter pub deps --style=compact shows who pulls the package in
Restore the last good state: check out pubspec.lock from the last good commit and run pub get --enforce-lockfile
Bound it yourself: add the transitive package to your own dependencies: with a range that excludes the bad version, such as '>=1.4.0 <1.4.3'. That is the supported way to constrain a transitive dependency
Not an exact pin: broken_json: 1.4.2 says the same today and corners the solver tomorrow
Report upstream: the author can retract the version within seven days, which stops every new resolution from choosing it
Clean up: remove your bound once a fixed release ships
It is a federated plugin: the Dart API, the platform interface and each platform's implementation are separate packages, so a platform can be added or fixed without the plugin's owner. Your app depends only on the app-facing package.
App-facing (path_provider): the API you import, with almost no logic in it
Platform interface (path_provider_platform_interface): an abstract class plus a static instance that everything calls through
One implementation per platform (path_provider_android, path_provider_foundation, …): each extends that interface
How an implementation gets picked up
Endorsement: the app-facing package depends on the implementation and names it under default_package:, so pub get pulls it in and the generated registrant calls its registerWith(), which sets the instance
Unendorsed implementation: a third-party platform package goes into your own dependencies:
**extends, never implements**: implementations extend the interface, and PlatformInterface.verify enforces it, so the interface can add a method with a default body in a minor release without breaking every implementation
What it means for your pubspec
Do not list endorsed implementations yourself; you would pin versions the app-facing package never agreed to
A solve failure is usually two implementations disagreeing about the platform interface version, not about the plugin itself
A Dart program holds one copy of each library, identified by its package: URI, so pub resolves exactly one version of every package for the whole app. The solve fails, and every real fix ends with one of the two constraints moving.
Why one version
Two copies of analytics would mean two different Event types that cannot be assigned to each other, plus two sets of globals and singletons; pub refuses up front instead of letting that fail at run time
npm-style nesting is impossible: imports name a package, not a path, so there is nowhere to put a second copy
In a pub workspace the failure blocks the whole repo, not just the two packages
Ways out, best first
Bump the laggard: usually one side is simply old, and a newer release already accepts the new major
Migrate the package you own: move its constraint to the new major in a dedicated PR
Widen upstream: a PR that only widens a constraint is easy to merge; depend on your fork by commit while you wait
Isolate: put your own interface in front of the dependency so only one package imports it
Override: last, and only with tests for the package that now runs on a major it never declared
Replace the big bang with a steady stream of small, tested bumps: a bot opens grouped PRs every week, CI proves each one, and only major upgrades get a human's full attention.
**Read flutter pub outdated: Upgradable above Current is free; Resolvable above Upgradable means your own constraint holds a package back; Latest above Resolvable means another package does, and the fix lives upstream
Let a bot open the PRs**: Dependabot (pub ecosystem) or Renovate, weekly and grouped: dev dependencies in one PR, patch bumps in another, or nobody reviews twenty PRs a week
Automerge only behind real CI: analyze, tests and a release build, with pub get --enforce-lockfile so what merges is exactly what was tested
Pin on purpose: the Flutter SDK itself, which you move in a separate PR; generators whose output is committed; native SDKs your CI cannot exercise
Majors by hand: a --major-versions PR is read line by line with the changelog open, never automerged
Keep the debt visible: a short monthly outdated review; a package stuck for more than a quarter becomes a ticket with an owner
Upgrade on a dedicated branch with the version pinned for the whole team, let dart fix --apply handle the mechanical renames, then work through the breaking-change notes, the analyzer, the tests and the native build before shipping through a beta track.
Before
Read the release notes and the breaking-changes page for every release you skip; jumping several at once multiplies the work
Clear deprecation warnings on the current version first: the next releases remove what this one deprecated
Pin the version in one place that every laptop and CI reads, such as FVM's .fvmrc
During
Upgrade the SDK, then flutter pub upgrade: plugins often need releases built for the new SDK
dart fix --dry-run, then --apply: Flutter ships data-driven fixes that rewrite renamed APIs for you
flutter analyze and the full test suite; expect golden tests to change with Material updates
Build release APK and iOS: new Flutter versions raise the minimum Android Gradle Plugin, Gradle, Kotlin and iOS deployment target, and those edits in android/ and ios/ belong in the same PR
After
Ship through internal testing and TestFlight first and watch the crash-free rate
Keep the previous SDK on a branch so a rollback is one revert
Hide the package behind your own interface first, then pick by size: replace it if a maintained alternative exists, vendor it into packages/ if you use a small slice of pure Dart, fork it by git: at a pinned commit if you need much of it and replacing is too big.
Check pub.dev first: a discontinued package's page names a suggested replacement. Then count your call sites and look for android/ or ios/ folders; twelve call sites of pure Dart is a different problem from a plugin with a Kotlin half.
Pin and wait: buys a few weeks, then blocks every other upgrade, so give it an expiry date
**Fork by git: at a pinned commit: fastest, fine for an app, a dead end for anything you publish, since published packages cannot have git dependencies
Vendor it into packages/**: best when you use a small slice; keep the LICENSE, and accept that nobody sends you fixes now
Replace it: the only option that reduces the code you own, and the only one that is a real project
Keep it reviewable: characterization tests first, then one PR that hides the package behind your interface, and a second that swaps the implementation