Dart (3.13) Module 14 – Libraries, packages and pub
Libraries, imports, exports and privacy
How Dart libraries, import prefixes, show and hide, a curated export file over lib/src, and conditional and deferred imports fit together.
What you will learn
- Import and export libraries with a prefix, show and hide
- Organise a package behind one export file with helpers private to lib/src
- Choose between a curated export file and part files when splitting a library
- Use conditional imports and deferred loading where they help
Before you start
What you’ll learn · Before you start
What you will learn
- Import and export libraries with a prefix, show and hide
- Organise a package behind one export file with helpers private to lib/src
- Choose between a curated export file and part files when splitting a library
- Use conditional imports and deferred loading where they help
Before you start
On this page
A 400-line file that holds a whole feature is quick to write and hard to live with: every helper in it is reachable from everywhere, and the day you rename one you cannot tell who outside depended on it. Dart answers with a rule that costs nothing to adopt, that every file is a library and a name beginning with an underscore is private to its own library, and with a convention that turns a pile of files into one public surface.
A package builds that surface from a few parts: imports shaped with a prefix, show and hide; one export file over a
lib/src folder of private helpers; the implementation_imports lint that keeps other packages out of that folder;
and, for code that runs on more than one platform, conditional imports and deferred loading. Every output below was
recorded on the Dart SDK 3.13.5. Adding dependencies comes next; here the subject is the files you already have.
Every file is a library, and privacy is per library
Dart has no public or private keyword. Instead, every .dart file is a library: In Dart, a single .dart file: the unit of code organisation and of privacy. Every file is a library, and a name beginning with an underscore is private to it., and an
identifier that starts with an underscore is private to that library: visible everywhere inside the one file, invisible
to every other. This is library privacy: Dart's way of hiding names: an identifier that starts with an underscore is visible only inside its library, and every Dart file is a library. Other classes of the same file can still use it. More in “Classes, objects, getters and privacy”, and the word “library” is the surprise in
it, because privacy is drawn around the file, not around the class.
The difference shows the moment a top-level function reaches for a field that a class made private. In the first file,
balanceOf sits beside the Account class in the same library, so it reads a._balance without complaint. A second
file imports the first and tries the same read:
from session import files, run
# Privacy is per library (per file). A top-level function in the same file
# reads the private field; a different library cannot even name it.
files("privacy")
run(
"""
cd privacy
dart run bank.dart
!dart run report.dart
""",
) Output
$ cd privacy
$ dart run bank.dart
Asha: 150
$ dart run report.dart
report.dart:7:11: Error: The getter '_balance' isn't defined for the type 'Account'.
- 'Account' is from 'bank.dart'.
Try correcting the name to the name of an existing getter, or defining a getter or field named '_balance'.
print(a._balance);
^^^^^^^^
[exit status 254]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 privacy_session.py
dart run bank.dart printed Asha: 150: balanceOf is a plain top-level function, but because it lives in the same
library as Account, the private _balance is in scope. dart run report.dart, a different library, failed to
compile with The getter '_balance' isn't defined for the type 'Account' and the program ended with exit status 254.
The underscore did not hide the field from another class; it hid it from another file.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- bank.dart is one library. bank.dart holds the Account class with a private field _balance; the underscore makes _balance private to this whole file.
- The same file reads it. balanceOf is a top-level function in the same file, so it is in the same library and reads a._balance; the program printed Asha: 150.
- Another library cannot. report.dart imports bank.dart, but it is a different library, so a._balance there does not compile and the run exits 254.
So privacy in Dart is a property of the file, which is exactly why a package can keep helpers private file by file and still let its own public library use them. There is no finer-grained visibility and no coarser one: the library is the unit.
Key idea
Every Dart file is a library, and a leading underscore makes a name private to that whole file, not to a class, so a top-level function in the same file sees it while any other library cannot.
Checkpoint
Check: Privacy is per library
Shaping what an import brings in: a prefix, show and hide
A plain import brings every public name of the target library into scope. Three modifiers narrow that. A prefix,
written as, puts the names behind a namespace so they never clash; show brings in only the names you list; and
hide brings in everything except the names you list. This program imports dart:math two ways and dart:convert
with one name hidden:
// Three ways to control what an import brings into scope.
import 'dart:math' as math; // reach names through the math. prefix
import 'dart:math' show pi; // bring in only pi, with no prefix
import 'dart:convert' hide base64; // everything from dart:convert except base64
void main() {
print(math.max(3, 7)); // 7, reached through the prefix
print(pi.toStringAsFixed(4)); // 3.1416, brought in by show
print(jsonEncode({'ok': true})); // {"ok":true}, still here after the hide
} Output
7
3.1416
{"ok":true}
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run imports_demo.dart
It printed 7, 3.1416 and {"ok":true}. math.max(3, 7) reached max through the import prefix: A name given to an import with as, such as import 'dart:math' as math. It puts that library's names behind the prefix, so math.max is reached without clashing with other libraries. math; pi.toStringAsFixed(4) used pi because show pi brought in that name and no other; and
jsonEncode still worked after hide base64, because hide removes only the names it lists. A prefix is the usual
cure for two libraries that both define a name such as Element or Level.
Where the library lives decides how you write its path. Another package’s library is always reached by a package:
URI, which names the package and a file under its lib. Inside your own package, a file in lib reaches a sibling by
a relative path, import 'src/slugify.dart', while a script in bin or a test reaches a file in lib by
package:. The diagram follows one such URI to its file:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Name the package and file. package:slug/slug.dart names the slug package and the file slug.dart under its lib folder.
- Resolve the package. dart pub get wrote .dart_tool/package_config.json; the analyzer and compilers read it to map the name slug to its folder.
- Reach the public library. The path after the package name points at lib/slug.dart, the one public library the caller imports.
- Follow the export. lib/slug.dart re-exports selected names, so the caller sees them without importing the src file.
- The implementation stays hidden. The real code in lib/src/slugify.dart is reached only through the export; callers never import it directly.
The name-to-folder map lives in .dart_tool/package_config.json, which dart pub get writes; the analyzer and the
compilers read it whenever they resolve a package: import. Effective Dart’s style guide asks you to “place dart:
imports before other imports” and “package: imports before relative imports”, each section sorted. The Dart 3.13
formatter now reinforces that by adding a blank line between the sections, though it does not sort them for you. A pubspec is what makes a folder a package and gives it
the name the package: URI uses; the next lesson, pub dependencies and versioning, is about filling it in.
Checkpoint
Check: Prefix, show and hide
A curated public API: one export file over a src folder
The package layout convention gives each package two public directories, lib and bin, and one place for code that
is not public: a src folder inside lib. The docs are blunt about it: you “should never import from another
package’s lib/src directory”, because those files may change in ways that break you. A package then presents a
public API: The names a package promises to callers: the symbols its public library files in lib expose, directly or through an export. Code under lib/src is not part of it. by putting one or a few library files directly in lib, usually led by
lib/<package>.dart, each re-exporting only the names callers should see with an
export directive: An export line that republishes names from another library as if they were declared locally, often narrowed with show or hide, so a package can present a curated public API from one file..
The slug package below does exactly that. Its public file, lib/slug.dart, carries a bare library; directive
(allowed since Dart 2.19) so it can hold library-level documentation, and then one line: export 'src/slugify.dart' show slugify;. The implementation, including a private _collapseDashes helper and a transliterate function that
is public-in-package but never exported, lives under lib/src:
lib/slug.dart · slug/lib/slug.dart
/// Turns human titles into URL-safe slugs.
///
/// The whole public surface of this package is one function, [slugify].
/// Everything else lives under lib/src and is not exported, so a change to it
/// is never a breaking change for callers.
library;
export 'src/slugify.dart' show slugify; lib/src/slugify.dart · slug/lib/src/slugify.dart
import 'transliterate.dart';
/// Converts [title] into a lowercase, dash-separated slug.
///
/// Accented letters are folded to ASCII and every run of other characters
/// becomes one dash: `slugify('Café del Mar!')` returns `cafe-del-mar`.
String slugify(String title) {
final folded = transliterate(title.toLowerCase());
return _collapseDashes(folded);
}
/// Private to this library: callers reach it only through [slugify].
String _collapseDashes(String s) {
final dashed = s.replaceAll(RegExp(r'[^a-z0-9]+'), '-');
return dashed.replaceAll(RegExp(r'^-+|-+$'), '');
} lib/src/transliterate.dart · slug/lib/src/transliterate.dart
/// Folds a small set of accented letters to ASCII.
///
/// The name has no underscore, so slugify.dart (a different library in the
/// same package) can call it. It is not exported from lib/slug.dart, so it
/// stays out of the package's public API all the same.
String transliterate(String s) {
final buffer = StringBuffer();
for (final rune in s.runes) {
buffer.write(_table[rune] ?? String.fromCharCode(rune));
}
return buffer.toString();
}
const _table = <int, String>{
0xe9: 'e', // é
0xe8: 'e', // è
0xe7: 'c', // ç
0xf1: 'n', // ñ
0xfc: 'u', // ü
}; bin/demo.dart · slug/bin/demo.dart
import 'package:slug/slug.dart';
void main() {
print(slugify('Café del Mar!'));
print(slugify(' Hello, World '));
} Resolving, analysing, running and testing the package shows the surface holding together:
from session import files, run
# The slug package: one public function behind a curated export file, with the
# implementation under lib/src. Resolve offline, analyse, run and test it.
files("slug")
run(
"""
cd slug
cat lib/slug.dart
dart pub get --offline
dart analyze
dart run bin/demo.dart
dart test
""",
pub=True,
) Output
$ cd slug $ cat lib/slug.dart /// Turns human titles into URL-safe slugs. /// /// The whole public surface of this package is one function, [slugify]. /// Everything else lives under lib/src and is not exported, so a change to it /// is never a breaking change for callers. library; export 'src/slugify.dart' show slugify; $ dart pub get --offline Resolving dependencies... Downloading packages... + _fe_analyzer_shared 109.0.0 + analyzer 14.5.0 + args 2.7.0 + async 2.13.1 + boolean_selector 2.1.2 + cli_config 0.2.0 + cli_util 0.6.0 + collection 1.19.1 + convert 3.1.2 + coverage 1.16.0 + crypto 3.0.7 + ffi 2.2.0 + file 7.0.1 + frontend_server_client 4.1.0 + glob 2.2.0 + http_multi_server 3.2.2 + http_parser 4.1.2 + io 1.1.0 + lints 6.1.0 + logging 1.3.0 + matcher 0.12.20 + meta 1.19.0 + mime 2.1.0 + node_preamble 2.0.2 + package_config 3.0.0 + path 1.9.1 + pool 1.5.3 + pub_semver 2.2.1 + shelf 1.4.3 + shelf_packages_handler 3.0.2 + shelf_static 1.2.0 + shelf_web_socket 3.0.0 + source_map_stack_trace 2.1.2 + source_maps 0.10.14 + source_span 1.10.2 + stack_trace 1.12.2 + stream_channel 2.1.4 + string_scanner 1.4.1 + term_glyph 1.2.2 + test 1.33.0 + test_api 0.7.15 + test_core 0.6.21 + typed_data 1.4.0 + vm_service 15.4.0 + watcher 1.2.1 + web 1.1.1 + web_socket 1.0.1 + web_socket_channel 3.0.3 + webkit_inspection_protocol 1.2.1 + yaml 3.1.4 Changed 50 dependencies! $ dart analyze Analyzing slug... No issues found! $ dart run bin/demo.dart cafe-del-mar hello-world $ dart test --:-- +0: loading test/slug_test.dart --:-- +0: test/slug_test.dart: folds accents and collapses punctuation --:-- +1: test/slug_test.dart: trims leading and trailing dashes --:-- +2: All tests passed!
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 package_session.py
dart analyze reported No issues found!, dart run bin/demo.dart printed cafe-del-mar and hello-world, and
dart test passed both cases. bin/demo.dart imported only package:slug/slug.dart and saw slugify alone; it could
not name transliterate or _collapseDashes, so the package can rename either without breaking a caller. That
is the whole point of the one-file surface. Splitting a single large library into this shape is a mechanical move:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- One file, four declarations. money.dart holds the Money class, parseMoney and two private helpers, all in one library.
- Move code under lib/src. Split the implementation into two lib/src files: Money with _format in model.dart, parseMoney with _stripSymbol in parse.dart, which imports model.dart for the Money type.
- Helpers stay private. Each lib/src file is its own library, so _format and _stripSymbol stay private to their file with no change.
- One curated export file. lib/money.dart re-exports only Money and parseMoney, with a bare library directive for its docs.
- Callers import one library. A caller imports package:money/money.dart and sees Money and parseMoney, never the helpers or the src files.
The bare library; directive has one more use. The doc comment above it may hold a doc import: A comment-only directive, written /// @docImport 'package:foo/foo.dart';, that lets a doc comment reference a symbol without a real import pulling it into scope. Available since Dart 3.8.
(/// @docImport 'package:foo/foo.dart';), a comment-only directive the analyzer has supported since Dart 3.8, so
[SomeType] links in the package’s doc comments resolve without a real import that would pull the type into scope.
Key idea
Put implementation under lib/src and expose one library file in lib that re-exports only the public names with
export, so helpers stay private and a caller’s import never reaches an internal file.
Checkpoint
Check: An export file over lib/src
Staying out of another package’s src folder
The convention only helps if callers honour it, so the analyzer carries a lint rule: A named check of the analyzer for style or likely bugs, such as unused_import; lint rules are turned on in analysis_options.yaml, often through package:lints. More in “The dart command: analyze, format, test, compile” named
implementation_imports that flags an implementation import: An import of a file under another package's lib/src, which reaches into that package's internals. The implementation_imports lint flags it, because such files are not public API.: an import of a
file under another package’s lib/src. Its message restates the rule. Inside your own package, lib, bin and test
may all import your lib/src files, as the layout docs allow; the lint is about another package’s src. It also
checks only libraries under lib, whose own address is a package: URI, so a script in bin or a test that reaches
into another package’s src goes unreported. The app package below reaches into greeter’s internals from a
library file:
lib/consumer.dart · impl/app/lib/consumer.dart
// This library reaches into another package's lib/src instead of its public
// library 'package:greeter/greeter.dart'. The implementation_imports lint
// flags it, because files under lib/src are not part of a package's public API.
import 'package:greeter/src/greet.dart';
String welcome() => greet('Dart'); impl/app/analysis_options.yaml
include: package:lints/recommended.yaml
linter:
rules:
- implementation_imports from session import files, run, write
# A library under lib/ that imports another package's lib/src triggers the
# implementation_imports lint. As a lint it is an info, so dart analyze exits
# 0; raising its severity to error makes the same code exit 3; importing the
# public library instead is clean.
files("impl")
run(
"""
cd impl/app
dart pub get --offline
dart analyze
dart run bin/app.dart
""",
pub=True,
)
write(
"impl/app/analysis_options.yaml",
"""include: package:lints/recommended.yaml
analyzer:
errors:
implementation_imports: error
linter:
rules:
- implementation_imports
""",
)
run(
"""
cd impl/app
!dart analyze
""",
)
write(
"impl/app/lib/consumer.dart",
"""import 'package:greeter/greeter.dart';
String welcome() => greet('Dart');
""",
)
run(
"""
cd impl/app
dart analyze
""",
) Output
$ cd impl/app $ dart pub get --offline Resolving dependencies... Downloading packages... + greeter 1.0.0 from path ../greeter + lints 6.1.0 Changed 2 dependencies! $ dart analyze Analyzing app... info - lib/consumer.dart:4:8 - Import of a library in the 'lib/src' directory of another package. Try importing a public library that exports this library, or removing the import. - implementation_imports 1 issue found. $ dart run bin/app.dart Hello, Dart! $ cd impl/app $ dart analyze Analyzing app... error - lib/consumer.dart:4:8 - Import of a library in the 'lib/src' directory of another package. Try importing a public library that exports this library, or removing the import. - implementation_imports 1 issue found. [exit status 3] $ cd impl/app $ dart analyze Analyzing app... No issues found!
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 impl_imports_session.py
The first dart analyze reported the lint as an info, so it exited 0: a lint is advice, and dart analyze returns a
non-zero status only when it finds a warning or an error. Meanwhile dart run bin/app.dart, which imports the public
package:greeter/greeter.dart, printed Hello, Dart! without complaint. Raising the rule to error under
analyzer: errors: made the same import fail analysis and exit with status 3, the status that would stop a CI job.
Changing consumer.dart to import the public library cleared it: No issues found!.
Make the lint bite in CI
A lint is only an info, so dart analyze passes it by default. Run dart analyze --fatal-infos in CI, or raise
the one rule you care about to warning or error under analyzer: errors:, to turn the advice into a build gate.
Checkpoint
Check: Keeping out of another src folder
part and part of, and when an export file is better
There is a second way to spread code across files, and it is the opposite of a library per file. A part
directive in a main file pulls another file into the same library; that other file begins with part of and names
its parent. Because the files then share one library, they share one set of imports and there is no privacy boundary
between them: a _name in one part is visible to every other part. Dart’s guide to creating packages does not
recommend part directive: A directive that pulls another file into the same library; that file begins with part of. The files then share one scope, one set of imports and no privacy boundary, which suits generated code. for code you write; it suggests small libraries, each its own
file, behind an export file. Generated code is the exception: a builder such as json_serializable, run with
dart run build_runner build, writes a .g.dart file that is part of your library, driven by an
annotation: Metadata written as @ and a compile-time constant before a declaration, such as @override or @Deprecated('message'). It does nothing by itself; tools such as the analyzer read it. More in “Operator overloading, callable objects, metadata” you put on a class, which is why generated code so often appears as a part.
The trade-off decides which shape to use:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Approach | Privacy between files | Hand-written code | Generated code | Imports per file |
|---|---|---|---|---|
| Export file over lib/src How the files relate: a library per file | Good each file is its own library, so underscore names stay private | Good the usual choice: add a file and export the names you want public | Mixed generators still emit part files that you include separately | Good every file writes its own import directives |
| part and part of How the files relate: one shared library | Poor all parts share one library, so there is no privacy between them | Poor one library spread across files, with shared scope you rarely want | Good a generated .g.dart file is a part of your library by design | Poor only the main file has directives; the parts share them |
How the answers decide
- “Must the files hide names from each other?” Yes, keep names private per file: rules out part and part of. Parts share one library, so a private name in one is visible to all; an export file keeps each file private.
- “Who writes the code in the extra files?” A code generator emits it: adds a caveat to part and part of. Generated code is emitted as a part of your library, which is the one case where part of fits well.
- “Who writes the code in the extra files?” I write it by hand and “Must the files hide names from each other?” No, shared scope is fine: rules out part and part of. For code you write by hand, an export file keeps libraries small and independent, even when privacy is not the concern.
In short: an export file over lib/src keeps each file its own library with its own privacy, which is what you want
for code you write; part and part of are for the one library a generator extends, where sharing scope is the
feature, not a cost.
Checkpoint
Check: part and part of
Conditional imports and deferred loading for many platforms
A package that runs on the Dart VM and on the web often needs a different file on each, because dart:io exists only
off the web and dart:js_interop, the library web apps use to reach JavaScript, only on it. A conditional import: An import that chooses a file at compile time from an environment declaration, as in import 'stub.dart' if (dart.library.io) 'io.dart';, so one library can serve several platforms. picks the file at compile time from an
A compile-time key the compiler defines for each platform library a target has, such as dart.library.io when dart:io exists. Conditional imports test these keys.: import 'src/stub.dart' if (dart.library.io) 'src/io_impl.dart'; imports the stub unless dart.library.io is defined, in which case it imports the dart:io
version. Both files must declare the same public names, so the rest of the program does not change. The same
main.dart on two targets:
conditional/main.dart
// Import the stub by default, but swap in the dart:io version when this
// program is compiled for a platform that has dart:io (the Dart VM, native).
import 'src/stub.dart' if (dart.library.io) 'src/io_impl.dart';
void main() {
print(platformLabel());
} src/stub.dart · conditional/src/stub.dart
// The fallback, used when dart:io is not available (for example on the web).
String platformLabel() => 'portable build (no dart:io)'; src/io_impl.dart · conditional/src/io_impl.dart
import 'dart:io';
// Used when dart:io is available. It must define the same public names as the
// stub, so main.dart calls platformLabel() either way.
String platformLabel() => 'native: ${Platform.operatingSystem}'; from session import files, run
# The same program on two targets. The VM has dart:io, so the conditional
# import picks io_impl.dart; compiled to JavaScript it has no dart:io, so it
# falls back to stub.dart. Node runs the compiled JavaScript.
files("conditional")
run(
"""
cd conditional
dart run main.dart
dart compile js main.dart -o main.js
node main.js
""",
) Output
$ cd conditional $ dart run main.dart native: macos $ dart compile js main.dart -o main.js Compiled 10,737,903 input bytes (5,318,473 characters source) to 12,113 characters JavaScript in … seconds $ node main.js portable build (no dart:io)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conditional_session.py
On the VM dart run main.dart printed native: macos, because dart.library.io is defined there and the dart:io
file won. Compiled with dart compile js and run with node, the same program printed portable build (no dart:io):
the web target has no dart:io, so the stub was chosen. One caution from the SDK changelog: since Dart 3.11
dart2wasm: The Dart compiler behind dart compile wasm, which turns a program into a WebAssembly module that uses WasmGC, plus a small JavaScript file that loads it. More in “Numbers in Dart: int, double, BigInt and the web” no longer supports dart:js_util, so dart.library.js_util is no longer a key
in a WebAssembly build; test dart.library.js_interop instead when you branch for the web.
deferred loading: Importing a library with deferred as so its code is left out of the initial download and loaded only when its loadLibrary() is awaited. Supported when compiling for the web. is the other tool. Importing a library deferred as leaves its
code out of the initial download; the code loads only when you await its loadLibrary(). The language docs note that
“the dart tool doesn’t support deferred loading for targets other than web”, so it pays off when a web app splits a
rarely used screen out of its first load. The syntax and what a web compile does with it:
// 'deferred as' marks a library to load lazily. On the web its code is split
// into a separate file that is fetched only when loadLibrary() is awaited.
import 'report.dart' deferred as report;
Future<void> main() async {
print('before load');
await report.loadLibrary();
print(report.heavyReport());
} from session import files, run
# Deferred loading runs eagerly on the VM, but dart compile js splits the
# deferred library into its own .part.js file that the browser fetches lazily.
# report.dart prints one line, so the VM run shows both lines in order.
files("deferred")
run(
"""
cd deferred
dart run main.dart
dart compile js main.dart -o out.js
ls -1 out.js out.js_1.part.js
""",
) Output
$ cd deferred $ dart run main.dart before load report from the deferred library $ dart compile js main.dart -o out.js Compiled 10,737,881 input bytes (5,318,451 characters source) to 204,353 characters JavaScript in … seconds $ ls -1 out.js out.js_1.part.js out.js out.js_1.part.js
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deferred_session.py
On the VM both lines printed and nothing was split off: the dart tool supports deferred loading only for the web, so
there loadLibrary() simply completes. But dart compile js main.dart -o out.js, which runs the
dart2js: The Dart compiler behind dart compile js, which turns a program into JavaScript. There int and double are both JavaScript numbers, so integers are exact only up to 2^53. More in “Numbers in Dart: int, double, BigInt and the web” compiler, wrote two files, out.js and out.js_1.part.js: the deferred library
became a JavaScript file of its own that a browser fetches only when loadLibrary() runs. dart2wasm gained the same
ability as a preview in Dart 3.13, behind dart compile wasm -O2 --enable-deferred-loading.
Key idea
Use a conditional import to pick a file per platform from a dart.library.* declaration, and deferred as with
loadLibrary() to split rarely used web code out of the first download.
Checkpoint
Check: Conditional and deferred imports
Practise: split a one-file package
A single file, money.dart, holds a whole money package: a Money class, a parseMoney function, and two helpers,
_stripSymbol and _format, that should not be part of the public surface. You will reshape it into lib/money.dart
over a lib/src folder, keeping the helpers private. Start from these two files:
practice/pubspec.yaml
name: money
description: A tiny money package to split into a curated public API.
version: 0.1.0
publish_to: none
environment:
sdk: ^3.13.0
dev_dependencies:
lints: ^6.0.0
test: ^1.25.0 practice/money.dart
// A whole package in one file. The task is to move the implementation under
// lib/src and leave lib/money.dart as a curated export of only the public API
// (the Money class and parseMoney), keeping the helpers below private.
/// An amount of money in whole paise, so arithmetic stays exact.
class Money {
const Money(this.paise);
final int paise;
Money operator +(Money other) => Money(paise + other.paise);
@override
String toString() => _format(paise);
}
/// Parses '12.50' into Money(1250).
Money parseMoney(String text) {
final cleaned = _stripSymbol(text);
final parts = cleaned.split('.');
final rupees = int.parse(parts[0]);
final paise = parts.length > 1 ? int.parse(parts[1].padRight(2, '0')) : 0;
return Money(rupees * 100 + paise);
}
// Helpers that should become private to the implementation library.
String _stripSymbol(String text) =>
text.replaceAll(RegExp(r'[^0-9.]'), '').trim();
String _format(int paise) {
final rupees = paise ~/ 100;
final rest = (paise % 100).toString().padLeft(2, '0');
return '$rupees.$rest';
} Make a console project with dart create -t console money and replace its pubspec.yaml with the one above. Delete
the template’s bin/money.dart and test/money_test.dart first: both call a calculate() function that the split
removes, so dart analyze would report two errors. Then:
- Move
Moneyand_formatintolib/src/model.dart, andparseMoneyand_stripSymbolintolib/src/parse.dart, which importsmodel.dartfor theMoneytype. Each helper lands in the file that uses it, so it keeps its underscore and stays private to that file. - Replace
lib/money.dartwith a one-line documentation comment, a barelibrary;directive, and two exports:export 'src/model.dart' show Money;andexport 'src/parse.dart' show parseMoney;. - Write
bin/demo.dartthat importspackage:money/money.dartand printsparseMoney('₹12.50') + parseMoney('7.00'), which should print19.50. Confirm it cannot name_formator_stripSymbol.
Check your work with dart analyze (it should report no issues) and a test under test/ that imports only
package:money/money.dart. When the public import compiles and the helpers are unreachable from outside, the split is
correct. The practice set below starts with whichever idea of this lesson your answers show is weakest.
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): a top-level function reads a class’s _balance while another file cannot, even after importing
it. Why?
Because privacy in Dart is per library, and a library is a single file. The underscore on _balance hides the field
from every other library, not from other code in the same file. The balanceOf function lives in the same file as
Account, so it is inside the same library and the field is in scope; it printed Asha: 150. The second file is a
different library: importing the first does not grant access to its private names, so a._balance there does not even
compile, and the run ended with exit status 254. There is no per-class privacy to reach for and no protected level;
when you want a helper hidden from the outside world but usable nearby, put it in the same file, or expose it only
within the package and never export it.
Warm-up (2-5 years): what does export 'src/slugify.dart' show slugify; do, and why keep the implementation under
lib/src?
The export directive re-exports names from another library as if they were declared in the exporting one, and show slugify narrows that to a single name, so a caller importing package:slug/slug.dart sees slugify and nothing else.
Keeping the implementation under lib/src means those files are not part of the package’s public surface: callers are
told not to import package:slug/src/..., and the implementation_imports lint flags them when they do. Together they
let the package change, rename or reorganise everything behind the one export file without breaking anyone, because the
only promise it ever made was the handful of names the export line lists.
The two important questions below take this same package further. Try your own answer to each first:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
You inherit a utilities package whose lib folder holds eleven files that other teams import directly, so every rename is a breaking change. Design a curated public API with one or a few export files over lib/src, and explain how you move the eleven files without breaking the teams that depend on them today.
-
Mid to senior
A logging package must run on the Dart VM, in the browser through JavaScript, and in a WebAssembly build, writing to a file where it can and to the console otherwise. Design its imports so one public API serves all three targets, and say what you check and how you keep a web build from pulling in dart:io.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why can a top-level function read a class's _balance while another file cannot, even after importing it?
Every Dart file is a library, and a leading underscore makes a name private to that whole file, not to a class. The function is in the same file, so the field is in scope; in another file the read does not compile.
-
How can an import bring in fewer names than the library offers?
A plain import brings in every public name; a prefix with as, a show list or a hide list narrows that. A package: URI reaches any package's lib folder, and a relative path reaches a sibling file.
-
How does a package keep its helpers private while offering a public API?
It puts the implementation under lib/src and exposes one file in lib that re-exports only the public names, such as export 'src/slugify.dart' show slugify;. The package can then reorganise everything behind that line.
-
What does the implementation_imports lint catch, and how do you make it fail a build?
Code under lib that imports another package's lib/src. A lint is only an info, so run dart analyze --fatal-infos in CI or raise the rule to error, which made dart analyze exit 3.
-
When does part and part of fit better than an export file?
For generated code. part puts several files in one shared library with no privacy between them; for code you write, an export file is better.
-
What do a conditional import and a deferred import do?
A conditional import picks a file per platform from a dart.library.* declaration. deferred as with loadLibrary() splits rarely used web code out of the first download.
Key takeaways
- Every Dart file is a library, and a leading underscore makes a name private to that whole file, so privacy is per library and never per class.
- A plain
importbrings in every public name; a prefix withas, ashowlist or ahidelist narrows that, and apackage:URI reaches any package’slibwhile a relative path reaches a sibling file. - A package presents one public surface by putting implementation under
lib/srcand exposing alibfile that re-exports only the public names withexport. - The
implementation_importslint flags code underlibthat reaches into another package’slib/src; as a lint it is an info, so make it bite with--fatal-infosor by raising it toerror, which madedart analyzeexit 3. partandpart ofput several files in one shared library with no privacy between them, which suits generated code but not code you write, where an export file is better.- A conditional import picks a file per platform from a
dart.library.*declaration, anddeferred aswithloadLibrary()splits rarely used web code out of the first download.
Terms in this lesson
- conditional import
- An import that chooses a file at compile time from an environment declaration, as in import 'stub.dart' if (dart.library.io) 'io.dart';, so one library can serve several platforms.
- deferred loading
- Importing a library with deferred as so its code is left out of the initial download and loaded only when its loadLibrary() is awaited. Supported when compiling for the web.
- doc import
- A comment-only directive, written /// @docImport 'package:foo/foo.dart';, that lets a doc comment reference a symbol without a real import pulling it into scope. Available since Dart 3.8.
- export directive
- An export line that republishes names from another library as if they were declared locally, often narrowed with show or hide, so a package can present a curated public API from one file.
- implementation import
- An import of a file under another package's lib/src, which reaches into that package's internals. The implementation_imports lint flags it, because such files are not public API.
- import prefix
- A name given to an import with as, such as import 'dart:math' as math. It puts that library's names behind the prefix, so math.max is reached without clashing with other libraries.
- library
- In Dart, a single .dart file: the unit of code organisation and of privacy. Every file is a library, and a name beginning with an underscore is private to it.
- part directive
- A directive that pulls another file into the same library; that file begins with part of. The files then share one scope, one set of imports and no privacy boundary, which suits generated code.
- public API
- The names a package promises to callers: the symbols its public library files in lib expose, directly or through an export. Code under lib/src is not part of it.
References
- Language: libraries and imports (The Dart project (Google))
- Package layout conventions (The Dart project (Google))
- Effective Dart: Style (The Dart project (Google))
- implementation_imports lint rule (The Dart project (Google))
- Environment declarations (The Dart project (Google))
- Compiling to WebAssembly (The Dart project (Google))
- Announcing Dart 3.13 (The Dart project (Google))
- Dart SDK CHANGELOG (The Dart project (Google))
- Creating packages (The Dart project (Google))
- Documentation comment references (The Dart project (Google))
- dart analyze (The Dart project (Google))
- implementation_imports rule source (The Dart project (Google))
- build_runner (The Dart project (Google))
- json_serializable package (google.dev on pub.dev)
Report a problem with this lesson
Kept only in this browser. Your Learn progress