Libraries, Packages & pub

By the end of this lesson you will be able to read and write every form of Dart import, resolve a naming conflict between two libraries, explain exactly what "private" means in Dart, lay out a real package (pubspec.yaml, lib/, lib/src/, bin/, test/), read a version constraint like ^1.4.2 correctly (including its 0.x.y trap), understand why pub get sometimes can't find a working set of versions at all, avoid the "my new syntax won't compile" language-version gotcha, and know what pub workspaces and conditional imports are for.

1. Libraries, imports & the file graph

Think of every .dart file as one volume in a library building. A volume can mention things written only for its own use in the margins (private notes, visible only if you're holding that exact volume) and things meant to be read by anyone (its public chapters). import is you walking to the shelf and borrowing another volume so you can use its public chapters in your own writing. export is a librarian's "recommended reading" card taped to the front of your volume, pointing a future borrower straight at a chapter from a totally different volume, as if it were yours.

A library in Dart is, in the ordinary case, just one .dart file — every file you write is automatically its own library, even if it has no library declaration at all. (A library can technically span several files glued together with part — Section 1's very last topic — but that's the exception, not the rule.)

There are three forms of import, distinguished purely by what comes after the word import:

FormExampleResolves to
dart:import 'dart:math';A library bundled inside the Dart SDK itself — always available, no pubspec.yaml entry needed.
package:import 'package:http/http.dart';A library published inside a package — either one you depend on (declared in pubspec.yaml) or your own package's lib/ folder, referenced by its own name.
Relative pathimport 'src/helpers.dart';Another file inside this same package, found by its file-system path relative to the importing file. Only usable within one package — you can't relatively-import across package boundaries.

Every panel on this page marked "real run" uses one helper, runLab: it writes the files you see into a fresh temporary folder, runs a real dart command there (--offline, so no network) and prints exactly what Dart answered. The outputs below are not typed in by hand.

The three import forms in one tiny package (the dart:, package: and relative forms all run together):

When two imports both bring in a name and you use it unprefixed, Dart raises a real compile-time error, ambiguous_import, rather than silently guessing. Three tools fix it:

Here is the clash and its three fixes, run for real. First the plain double import:

And each fix on its own (hide the 2-D one, show only what you need, or give each import an as prefix):

export republishes another library's public names as if they belonged to the exporting file — the classic use is a barrel file (often lib/mypkg.dart) that gathers several internal files into one convenient public import:

// lib/mypkg.dart — a barrel that re-exports two internal files
export 'src/shapes.dart';
export 'src/colors.dart' show Color;  // export can also show/hide

Re-exporting through a barrel, with show hiding one name from importers (the second line proves it is really hidden):

Combining show and hide on the same import directive is actually legal Dart syntax — it compiles and runs — but dart analyze flags it with a real lint: multiple_combinators: Using multiple 'hide' or 'show' combinators is never necessary and often produces surprising results. (Verified while auditing this lesson on Dart 3.11.5: import 'utils_a.dart' show shout hide shout; compiles with only that warning, no error.) Prefer ONE combinator per directive — if you need both kinds of filtering, use two separate import statements (one with a prefix) or restructure into a barrel.

You can check the lint claim yourself — the program compiles and runs, and dart analyze adds the warning:

A name that starts with an underscore, like _cache or _Helper, is private — but Dart's privacy boundary is the library (in the normal case: the one .dart file it's written in), not the class. Anything else in that same file — another class, a top-level function, even code added months later by someone else — can read it freely. Code in a different file that merely imports this one cannot see it at all; from that file's point of view, the name simply doesn't exist.

The same privacy rule as a run: a top-level function in the same file may read a private field of another class, but another file cannot read a private name at all.

Real, verified transcript from authoring this lesson (two separate files, checked with dart analyze and dart run on Dart 3.11.5): a file a.dart declares int _secret = 42; and a public wrapper int publicOne() => _secret;. A second file that imports a.dart and calls publicOne() runs fine and prints 42. A second file that imports a.dart and tries to read _secret directly fails to compile with: Undefined name '_secret'. Try correcting the name to one that is defined, or defining the name. — Dart doesn't even say "private"; from the outside, an underscore name was simply never declared.

Dart also supports gluing several physical files into one library with part / part of:

// main_lib.dart
part 'helper_part.dart';
int useHelper() => helperValue() + 1;

// helper_part.dart
part of 'main_lib.dart';
int helperValue() => 41;   // no import needed — it's the SAME library

A part file really is the same library — it reads a private name of the main file without any import:

Verified while authoring this lesson: this really does compile and run (useHelper() prints 42). But part files share ONE library scope — there's no privacy boundary between them at all, imports in the main file apply to every part, and you can't import a part file on its own. Because of this, hand-written part/part of is now discouraged for ordinary code — it mainly exists so code generators (like json_serializable or freezed) can add a generated .g.dart file that behaves as if it were pasted straight into your file, without you hand-maintaining a second library's worth of imports.

Finally, a deferred import delays loading a library's code until you explicitly ask for it, with deferred as plus an await ...loadLibrary() call:

import 'heavy.dart' deferred as heavy;

Future<void> main() async {
  print('before load');
  await heavy.loadLibrary();      // code isn't loaded until THIS line
  print(heavy.heavyGreeting());
}

The deferred example, run on the native VM:

Verified directly while authoring this lesson: on the native Dart VM this compiles and runs immediately (real output: before load, then hello from deferred lib) — loadLibrary() effectively resolves right away there. The feature earns its keep mainly when compiling for the web: a deferred library can be split into its own separate download, so a user who never visits, say, an admin screen never downloads that screen's code at all. It's a code-splitting tool, not a way to make a missing file "optional" — a genuinely missing file is still a build-time error either way.

Input size → what's feasible: imports are resolved at compile time, so even 104 files cost nothing at run time; prefer many small libraries over one giant one, and keep the import graph acyclic (a design rule, not a compiler rule).

A library is (usually) one file. dart: = SDK, package: = a package's lib/, relative path = another file in the same package. show/hide/as resolve name clashes; export republishes names for whoever imports YOU. Privacy (_name) is per-file, not per-class. part/part of merge files into one library — mostly a codegen tool today. deferred as delays loading, mainly to shrink web bundles.

Import graphs: cycles and build order

Once a project has more than a handful of files, the "who imports whom" relationships form a graph — and graphs can have two very practical problems: a cycle (file A eventually imports itself back, directly or through others) and, one level up, the question of which package in a multi-package project has to finish building before another one can even start.

The same depth-first search as code (an explicit stack, so a 200 000-module chain cannot overflow the call stack):

And the build order, as code:

Input size → what's feasible: a project with ≤ 103 files needs no care; a 2·105-module monorepo graph is still one O(V + E) pass (~106 steps), so cycle detection and build ordering stay cheap — only recursion depth needs care.

A cycle is not always a compile error. Dart accepts import cycles between files (an import only makes names visible; it does not paste text, so A importing B importing A still compiles), and — checked on Dart 3.11.5 with two path: packages that depend on each other — dart pub get resolves a package-level cycle too. So why hunt for them? Because a cycle means no valid build order exists (neither side can be built, tested or published first without the other), it tangles initialisation and makes the code hard to reason about and to split apart. Catching an accidental cycle early is exactly the algorithm in the box above: a depth-first search that remembers what is currently on the stack. Treat a found cycle as a design smell to break, not as something the compiler will always stop for you.

2. Package anatomy: pubspec.yaml, lib/, bin/, test/

If a library is one volume, a package is a whole published series with a table of contents (pubspec.yaml) declaring its title, edition number, and which other series it needs you to have read first. lib/ is the public shelf — anyone who buys the series can read those books. lib/src/ is the author's private workshop, visible to nosy readers who go looking, but explicitly "please don't rely on what you find in here, it can change without warning."
name: my_awesome_pkg
description: A short description of what this package does.
version: 1.4.2
environment:
  sdk: '>=3.3.0 <4.0.0'
dependencies:
  http: ^1.2.0
  collection: ^1.18.0
dev_dependencies:
  test: ^1.25.0
  lints: ^4.0.0
dependency_overrides:
  http:
    path: ../local_fork_of_http

A pubspec.yaml is just text that tools read. Here is a tiny reader, run on the example above (it also applies the package-name rule):

FieldMeaning
nameThe package's identity — what other packages write after package: in an import. Must be a valid Dart identifier (lowercase, underscores).
environment.sdkThe range of Dart SDK versions this package supports. Its lower bound also sets the package's default language version (see Section 5).
dependenciesPackages your published code actually needs at runtime — these get pulled in for anyone who depends on you too.
dev_dependenciesPackages only needed while working on this package — test runners, linters, build tools. NOT pulled in for people who merely depend on you.
dependency_overridesForce pub to use a specific version/source for a package, overriding normal resolution — handy for testing an unreleased fix (e.g. a local path: fork) but should be removed before you publish, since it can silently mask real constraint problems.

What "ship to consumers" means, computed: a package's dependencies follow it, its dev_dependencies do not.

The standard folder layout, and what each one signals to other developers and to pub.dev's tooling:

FolderConvention
lib/Your package's public API. Anything directly under lib/ (e.g. lib/my_awesome_pkg.dart) is meant to be imported by consumers.
lib/src/Implementation details. By strong convention (not a compiler rule), consumers should never write import 'package:my_awesome_pkg/src/...' directly — only your own lib/ barrel files should reach into src/, so you're free to reorganize it without breaking anyone.
bin/Command-line entry points — files with a main() meant to be run directly, e.g. dart run bin/my_tool.dart, or installed globally via dart pub global activate.
test/Automated tests (covered properly in D18). Never shipped to consumers.
example/A small runnable demo of your package for newcomers — pub.dev shows it prominently, and it also counts toward your pub score, under documentation (Section 6).

The layout as a real package, created in a temp folder and run (bin/ imports the public barrel from lib/; lib/src/ stays internal):

Input size → what's feasible: a pubspec with 102 dependencies is normal; the cost is the solve, not the file. Keep lib/ small and put everything else under lib/src/.

pubspec.yaml declares identity, SDK range and dependencies. lib/ is public API; lib/src/ is "look but don't import directly" internals. dependencies ship to consumers, dev_dependencies don't. dependency_overrides is a temporary escape hatch, not a permanent fixture.

3. Semantic versioning & the caret (^)

A version number MAJOR.MINOR.PATCH is a promise, not just a label. Think of it like a building's floor plan revision: a patch bump means "we fixed a typo on the sign, the layout is identical" — safe to accept blindly. A minor bump means "we added a new wing, but every existing door still opens the same way" — safe, and you might gain something useful. A major bump means "we moved the front door" — anything that depended on the old layout could break.

The version type as code: parse MAJOR.MINOR.PATCH[-pre][+build], compare numerically, and note that comparing versions as text gets 1.10.0 wrong:

The caret (^) is pub's shorthand for "this version, or any later one that promises to still be compatible, per semver." Concretely, ^base means >= base and < the next version that semver allows to break compatibility:

ConstraintAllowsWhy
^1.2.3>=1.2.3 <2.0.0major > 0: only a MAJOR bump is allowed to break things, so everything up to (not including) 2.0.0 is fair game.
^0.4.2>=0.4.2 <0.5.0major is 0 (pre-1.0, "still unstable"), minor > 0: pub treats a MINOR bump as potentially breaking too, so the range is only one minor wide.
^0.0.5>=0.0.5 <0.0.6major AND minor are both 0: the package is treated as so unstable that even a PATCH bump might break something — only the exact patch is trusted.

The three caret rows from the table, computed and probed:

Try it yourself — type a caret constraint (e.g. ^1.4.0, ^0.3.1, or ^0.0.7) and watch which candidate versions get accepted:

The ^0.x.y special case surprises almost everyone the first time: ^0.3.1 does not behave like a "pre-1.0 caret is basically the same as 1.x" shortcut — it's actually the narrowest of the three cases, only one minor version wide. This is pub deliberately treating 0.x packages as not-yet-stable: any minor release before 1.0.0 is allowed to change its public API.

Besides ^, pubspec constraints also accept: an exact pin (2.3.1 — only that version), an explicit range ('>=1.0.0 <2.0.0'), any (accept whatever satisfies everyone else), and combining ranges with a space ('>=1.0.0 <1.9.0').

All the constraint forms in one function (exact pin, explicit range, any, caret):

Two extra rules pub actually applies, beyond plain semver textual math: (1) A prerelease of the version that would otherwise be the caret's exclusive ceiling is still excluded — ^1.2.3 does NOT let in 2.0.0-dev, even though 2.0.0-dev sorts numerically below the plain 2.0.0 boundary under semver's own ordering rules. Pub's real version-constraint implementation (the pub_semver package) deliberately carves this case out, because a prerelease of the NEXT breaking version is conceptually "the next major, still being built" — not something a caret on the current major should ever reach into. (2) When solving your whole dependency graph, pub treats "any stable version" as strictly higher priority than "any prerelease version" — it will not hand you an unstable version just because it happens to be newest; you only get one if nothing stable satisfies the constraints, or if you wrote a constraint that explicitly names a prerelease. This lesson's own satisfiesCaret/SemVer code in verify/d17.dart is a teaching simplification of the CORE caret math (verified correct for plain X.Y.Z versions) — it does not implement pub's exact prerelease-boundary carve-out described here.

Input size → what's feasible: a caret check is O(1) after parsing, so even 106 checks (≈ 6·107 steps) fit in a second; sorting 105 versions is n log n ≈ 1.7·106 comparisons.

^1.2.3 → next MAJOR is the ceiling. ^0.x.y (x>0) → next MINOR is the ceiling. ^0.0.y → next PATCH is the ceiling. The lower the leading non-zero part, the narrower the caret's promise.

4. Version solving, conflicts & pubspec.lock

Imagine three friends planning a group trip, each with their own hard constraints ("I can only travel in a week that has no rain," "I need at least 4 nights," "I'm free only in weeks 2 through 6"). Picking a trip date that satisfies EVERYONE simultaneously — and backing out and trying a different week if your first guess turns out to conflict with someone you hadn't checked yet — is exactly what dart pub get does with version numbers instead of weeks.

When you run dart pub get, pub doesn't just satisfy your own pubspec.yaml — every dependency can itself depend on other packages with their own constraints, so pub has to find one single version per package that satisfies every constraint reachable from your whole dependency graph, generally preferring the newest version it can. This is called version solving, and it's a real constraint-satisfaction search, not a simple lookup.

The search from the animation, run on the page's graphs (solvable, genuinely conflicting, and one that needs backtracking):

When no combination works — e.g. package A needs C ^2.0.0 (allows [2.0.0, 3.0.0)) while package B needs C ^1.5.0 (allows [1.5.0, 2.0.0)), and those two ranges share nothing — pub reports a version solving failed error. The fix is never "just pick one" (there IS no version satisfying both); you have to either upgrade whichever package has the outdated constraint, or use dependency_overrides temporarily while you sort it out.

pubspec.lock is the RESULT of a successful solve — the exact version chosen for every package, direct and transitive, frozen in one file.

Checked directly against dart.dev's current pub glossary while auditing this lesson: the guidance below is still exactly what it says today, word for word in substance — "Application packages should check their lockfiles into source control... For regular (library) packages, you usually won't." There is no newer dart.dev guidance recommending that published packages commit their lockfile too.

Commit pubspec.lock?Why
Applications (apps you deploy/ship)✅ YesEveryone building this exact app — CI, teammates, production — gets the exact same dependency versions. Reproducible builds.
Reusable packages (published for others to depend on)🚫 Usually notYour lockfile only reflects versions available when YOU last ran pub get; forcing it on consumers defeats the whole point of publishing a flexible caret range for them to resolve within.

The everyday pub commands, once dependencies exist:

CommandDoes
dart pub getResolve + download dependencies per the current constraints; writes/updates pubspec.lock.
dart pub upgradeRe-resolve, picking the NEWEST version each constraint still allows (updates the lockfile, not the constraints). --major-versions also bumps caret constraints themselves.
dart pub outdatedShows, per dependency, your current/resolvable/latest versions — what's available that your constraints already allow, and what would need a constraint change.
dart pub add foo / remove fooEdit pubspec.yaml's dependency list for you, then run get automatically.
dart pub depsPrints the whole resolved dependency tree.

The lock file for real: dart pub get --offline with a path dependency creates pubspec.lock and .dart_tool/package_config.json:

What actually happens to pubspec.lock across repeated runs — does every pub get re-solve from scratch, or does it reuse what's already locked?

The same lock/upgrade/outdated story as code, over a fake registry (the real dart pub outdated needs the network, so it is simulated here; get, lock reuse and upgrade follow the rules in the animation):

Input size → what's feasible: version solving is NP-complete in general, so a hand-written newest-first backtracker is fine for ≤ ~20 packages with few conflicts; real projects with hundreds of packages rely on pub's PubGrub solver, which is why you let pub get do it.

Version solving must satisfy every constraint in the WHOLE dependency graph at once, not just your own pubspec.yaml — a genuine conflict between two ranges has no fix except changing one of the constraints. Commit pubspec.lock for apps, generally not for packages.

5. The language-version gotcha

Imagine a construction crew whose blueprint says "built to the 2015 building code" pinned to the office wall — even if it's 2026 and every worker owns the newest power tools, they're still required to build ONLY what the 2015 code allows, because that's the promise stamped on the blueprint. Bumping the wall poster to "2024 code" isn't enough on its own, either — the crew's official permit (a separate cached document) has to be reissued to match before anyone's allowed to actually use the new techniques.

Every Dart file has an effective language version that decides which syntax features are even legal in it — records, patterns, class modifiers, and every other feature introduced after Dart 2.12 all require a high-enough language version. By default, that language version is taken from the lower bound of your package's environment: sdk: constraint, no matter which SDK you actually have installed.

Real, verified transcript from authoring this lesson: a package with sdk: '>=2.12.0 <4.0.0' tried to use record syntax final pair = (1, 2); under Dart SDK 3.11.5 — and it still failed with: This requires the 'records' language feature to be enabled. Try updating your pubspec.yaml to set the minimum SDK constraint to 3.0.0 or higher, and running 'pub get'. (That is the analyzer's wording; dart run itself prints The 'records' language feature is disabled for this library. Try removing the package language version or setting the language version to 3.0 or higher., as the panel below shows.) Editing the lower bound up to 3.0.0 in pubspec.yaml alone was still not enough — the fix only took effect after re-running dart pub get, because the language version is cached in .dart_tool/package_config.json, generated at pub get time, not read fresh from pubspec.yaml on every build.

The records example as a run, then the stale cache shown directly (edit the pubspec only: the cached language version stays 2.12 until pub get runs again):

This is a real trap on any project stitched together from older packages, or one that copy-pasted an old pubspec.yaml years ago and never revisited its SDK floor: your installed SDK can be brand new while your actual code is still frozen at an old language version, and the error messages ("this feature requires...") don't always make the real cause ("...and you forgot to re-run pub get after bumping the constraint") obvious.

Input size → what's feasible: a language-version error appears on the first build whatever the project size; the cost of the fix is one dart pub get.

The SDK constraint's LOWER bound sets a file's language version by default — not the SDK you have installed, and not the upper bound. Bumping the lower bound requires re-running dart pub get to actually take effect, because the resolved language version is cached in .dart_tool/package_config.json.

6. Publishing basics: pub points & dart doc

Publishing to pub.dev is like putting a product on a store shelf that also comes with an automatic inspection report stapled to the front — shoppers can see, at a glance, whether the box has an ingredients label (documentation), passed a safety check (static analysis), and is still being restocked (up-to-date dependencies) before they even pick it up.

pub.dev scores every published package out of a fixed number of pub points — 160 (verified directly on a real, currently-perfect-scoring package: http shows 30/30 conventions + 20/20 documentation + 20/20 platform support + 50/50 static analysis + 40/40 up-to-date dependencies = 160/160). Those five weighted categories are: following Dart file conventions (a valid pubspec.yaml, a LICENSE, a README.md, a CHANGELOG.md), providing documentation (an illustrative code example, normally in example/, plus API doc-comment coverage — at least 20% of public members documented), declaring supported platforms, passing static analysis cleanly (no errors, ideally no warnings/lints either), and keeping dependencies reasonably up to date. None of this is optional ceremony — each category maps to something a real consumer of your package actually needs to trust it.

The five categories and their maxima as code, so the 160 is a computed number rather than a claim:

pub.dev's own scoring help page (checked while auditing this lesson) now also describes a sixth consideration, "support modern toolchains" (SwiftPM for iOS/macOS packages, Wasm compilation support) — and says outright that the scoring model "is likely to be extended with additional checks in the future." As of this lesson's last check, real package score breakdowns still total exactly 160 across the five weighted categories above, so treat "modern toolchain" support as a good practice pub.dev is watching, not (yet) a seventh bucket of points — and don't be surprised if the total or the split changes again; check a live package's own score page for the current truth rather than trusting any fixed number forever.

dart doc (bundled with the SDK) reads your /// doc comments and generates a full browsable HTML API reference — by default into doc/api/ — the same kind of output pub.dev shows on a package's "API reference" tab.

/// Converts [celsius] to Fahrenheit.
///
/// Example:
/// ```dart
/// toFahrenheit(0);   // 32.0
/// ```
double toFahrenheit(double celsius) => celsius * 9 / 5 + 32;

What dart doc reads, and a real run (offline) that produces doc/api/index.html:

A verified publisher badge next to a package's name on pub.dev means the publishing account has proven ownership of a domain (e.g. a company's own domain) — it's a signal about WHO published something, independent of the package's own pub-points quality score.

Input size → what's feasible: scoring and docs are per package, not per file: a 104-line package gets the same five categories as a 10-line one; dart doc takes seconds.

pub points reward real, checkable signals: conventions, documentation, declared platforms, clean analysis, fresh dependencies. dart doc turns /// comments into a browsable API site. A verified-publisher badge is about domain-verified identity, not code quality.

7. Workspaces & monorepos

Without a workspace, every package in a monorepo is like a separate household doing its own weekly grocery run — even if two houses on the same street both need onions, each one drives to the store and keeps its own separate receipt. A pub workspace is more like the whole street agreeing to do ONE shared grocery run: one shopping list, one receipt (one pubspec.lock), and every house's pantry (each package's dependency) resolved together so nobody accidentally ends up with two incompatible versions of "onions" in the same street.

A pub workspace lets several packages in one repository share a single dependency resolution and a single pubspec.lock, instead of each package resolving on its own. The root pubspec.yaml lists its member packages; each member opts in with resolution: workspace:

### root pubspec.yaml
name: my_repo_root
environment:
  sdk: ^3.11.0
workspace:
  - packages/pkg_a
  - packages/pkg_b

### packages/pkg_a/pubspec.yaml
name: pkg_a
environment:
  sdk: ^3.11.0
resolution: workspace
dependencies:
  pkg_b: any   # a sibling workspace package — no `path:` needed at all
Verified directly while authoring this lesson, on Dart SDK 3.11.5: creating exactly this two-package layout and running dart pub get from the root resolved successfully with one shared pubspec.lock, and pkg_a could call into pkg_b's public API through an ordinary package:pkg_b/pkg_b.dart import — with only pkg_b: any in pkg_a's own pubspec.yaml, no explicit path: dependency required, because the workspace itself links sibling members together. Pub workspaces were introduced in Dart 3.6.0 (per dart.dev's own workspace documentation) — every member's environment.sdk must be ^3.6.0 or higher for resolution: workspace to work at all. They behave as described here on this site's SDK (3.11.5). A couple of real caveats worth knowing: if a leftover pubspec.lock or .dart_tool/package_config.json already exists inside a member package, pub get deletes it in favor of the one shared root lock; and a stray, non-member pubspec.yaml sitting between the root and a member directory makes pub get fail outright.

The two-package workspace from above, run for real: one pub get at the root, exactly one pubspec.lock, and pkg_a calling pkg_b with no path: entry:

Workspaces solve a real, common pain: several packages in one repo that depend on each other AND share dev tooling (the same lint rules, the same test runner version) no longer fight over slightly different resolved versions of a shared third-party dependency, because they resolve together, once.

Input size → what's feasible: 2–20 packages in one repo is the sweet spot: one shared resolution instead of 2–20 separate ones; a single package needs no workspace.

Pub workspaces (Dart 3.x) give a monorepo one shared pubspec.lock across member packages, opted into via workspace: (root) and resolution: workspace (members) — sibling packages depend on each other without needing an explicit path: entry.

8. Conditional imports (platform-specific code)

A conditional import is like a hotel room's power socket adapter built into the wall itself: the SAME wall plate works whether the hotel is wired for a US-style plug or an EU-style one — the building picks the right internal wiring automatically based on which country it's actually built in, and every appliance just plugs into the one visible socket without needing to know or care which wiring is behind it.

A conditional import lets one import statement resolve to a different actual file depending on which platform the code is being compiled for — the trick behind packages that work on both native (VM/AOT) and the web despite using completely different underlying APIs:

// run_cond.dart
import 'stub_impl.dart'
  if (dart.library.io) 'io_impl.dart';

void main() {
  print(platformName());
}
Verified directly while authoring this lesson: with stub_impl.dart defining String platformName() => 'unknown'; and io_impl.dart defining String platformName() => 'io (native)';, running this on the native Dart VM printed io (native) — because dart.library.io is true on native platforms, so the compiler swapped in io_impl.dart instead of the stub. (This was verified on the native VM inside this offline environment; verifying the equivalent web-side swap would need an actual web compile step, which isn't available here — treat that half as documented behavior, not independently re-checked for this lesson.)

The three-way import from the next paragraph, run on the native VM (the dart.library.io branch wins):

Every if (dart.library.XYZ) clause is checked in order, and the FIRST one whose library exists for the current compile target wins; the plain import at the very front is the fallback used if none of the conditions match. A typical three-way split looks like:

import 'platform_stub.dart'
  if (dart.library.io) 'platform_io.dart'
  if (dart.library.js_interop) 'platform_web.dart';
All the candidate files listed in a conditional import must declare the SAME public API (same top-level names, same signatures) — the compiler doesn't check this for you at the point of the conditional import itself; a mismatch only surfaces as confusing errors wherever the mismatched name is actually used. Treat the stub file as the contract every implementation must honor.

Input size → what's feasible: the choice is made at compile time, so it costs nothing at run time for any app size; keep the stub, io and web files small and identical in API.

Conditional imports pick between several real files at compile time based on which dart.library.* is available for the target platform — the standard way one package supports native and web with different underlying implementations behind one shared API.

Quiz

Interview questions

Cheat sheet

ConceptSyntax / rule
Import formsdart:x (SDK), package:name/file.dart (a package's lib/), 'relative/path.dart' (same package only)
Resolve a name clashshow keep-only, hide keep-except, as prefix namespace everything — never guessed automatically (ambiguous_import error otherwise)
exportrepublish another library's names as if written here — the barrel-file pattern
Privacy (_name)per LIBRARY (file), not per class — same file sees it, importers never do
part / part ofmerges files into ONE library, no privacy boundary between them — mostly a codegen mechanism now
deferred asdelay loading a library until await lib.loadLibrary() — mainly for web bundle splitting
Conditional importimport 'stub.dart' if (dart.library.io) 'io_impl.dart'; — resolves per compile target
pubspec essentialsname, environment.sdk, dependencies (ship), dev_dependencies (don't ship), dependency_overrides (temporary)
Folderslib/ public API, lib/src/ "don't import directly", bin/ CLI entry points, test/, example/
^1.2.3>=1.2.3 <2.0.0 — next MAJOR is the ceiling
^0.4.2>=0.4.2 <0.5.0 — next MINOR is the ceiling (0.x is "unstable")
^0.0.5>=0.0.5 <0.0.6 — next PATCH is the ceiling (most fragile)
Version solvingpub finds ONE version per package satisfying every constraint in the whole graph — no solution = "version solving failed"
pubspec.lockcommit it for apps (reproducible builds); usually don't for packages
pub get/upgrade/outdated/add/remove/depsresolve+download / re-resolve newest / show available updates / edit+get / edit+get / print the tree
Language-version gotchathe SDK constraint's LOWER bound sets a file's language version; bumping it needs a fresh pub get to actually take effect
Pub pointsup to 160, across conventions, documentation, platform support, static analysis, and up-to-date dependencies
dart docgenerates a browsable API reference from /// doc comments, default output doc/api/
Pub workspacesroot workspace: [..] + member resolution: workspace → one shared pubspec.lock, siblings need no path: