When a Flutter app's wallet feature becomes a local package, what should its top-level library export from lib/src, and what should stay internal?
answer
- a small surface other teams depend on
- entry points, contracts, types in signatures
- view models and repositories stay in src
- export with show, one file per package
- implementation_imports fails flutter analyze
basics
~20 sExport only what other packages legitimately use: the feature's routes or entry widget, the contract implementations the app shell wires, a setup function and every type in those signatures. View models, repositories, services, DTOs and private widgets stay under lib/src.
solid answer
~40 sA feature package's top-level file, say `lib/wallet.dart`, is its contract with the rest of the app, so it should re-export a deliberately small set of names with `export 'src/...' show ...`. That set is: the entry points (a route list or entry screen), the implementations of shared interfaces the app shell injects (for example a payment launcher), a function that registers the feature's dependencies, and every type that appears in those signatures. Everything else, view models, repositories, services, API models, generated code and internal widgets, stays under `lib/src`, so the wallet team can refactor it without breaking rides or food. The convention is not enforced by the compiler: another package can still import `package:wallet/src/...`, but the `implementation_imports` lint in `package:lints` (which `flutter_lints` includes) flags it, and `flutter analyze` treats infos as fatal by default.
code
dart · 6 lines// packages/wallet/lib/wallet.dart
library;
export 'src/routes.dart' show walletRoutes;
export 'src/payment/wallet_payment_launcher.dart' show WalletPaymentLauncher;
export 'src/setup.dart' show setUpWallet;go deeper
Recall that files directly under lib are public, lib/src is internal by convention, and a top-level file re-exports what others may use.
Explain what a feature package should export, entry points, contract implementations, a setup function and signature types, and why the rest stays in lib/src.
Show how the boundary is enforced in practice: implementation_imports, flutter analyze failing on infos, and review of the top-level library file.
Treat the export list as an inter-team contract: who approves widening it, and how a small surface keeps teams able to ship independently.
## The problem a public API solves When a large Flutter app splits a feature such as **wallet** into its own local package, other packages (the app shell, the rides and food features) import it with a `package:wallet/...` URI. Whatever they can import, they will eventually depend on. A feature package's **public API** is therefore the set of names the owning team promises to keep stable; everything outside it is free to change. Dart's package layout gives you the mechanism: - files directly under `lib/` are the package's **public libraries**; - files under **`lib/src/`** are, by convention, **implementation** that other packages should never import; - a public library **re-exports** chosen names from `lib/src` with `export`, optionally narrowed with `show`. (The Dart rules behind `export`, `show`, `hide` and library privacy belong to the language's library system; this answer is about choosing what a feature package exposes.) ## What to export A useful test: *would another package need to name this to use the feature?* 1. **Entry points.** The route list the app shell merges into its router, or an entry screen widget if routing is centralized. 2. **Contract implementations.** If food's checkout needs to start a payment, a shared contracts package declares the interface and wallet exports the class that implements it, so the shell can inject it. 3. **A setup function.** One function that registers the feature's own dependencies, so the shell does not need to know wallet's internal classes. 4. **Every type in those signatures.** If an exported function returns a `WalletBalance`, export `WalletBalance` too; otherwise callers can receive it but cannot name it without reaching into `src`. ```dart // packages/wallet/lib/wallet.dart library; export 'src/routes.dart' show walletRoutes; export 'src/payment/wallet_payment_launcher.dart' show WalletPaymentLauncher; export 'src/setup.dart' show setUpWallet; ``` ## What stays in lib/src | Stays internal | Why | |---|---| | View models | Paired with one screen; no other feature should drive wallet's UI state | | Repositories and services | Wallet's data access is its own; other features ask through contracts | | API models and generated code | Wire formats change with the backend and codegen | | Internal widgets | Reusable widgets belong in a shared UI package instead | Keeping these private is what lets the wallet team rename a repository, split a view model or swap an HTTP client in a minor change without a cross-team negotiation. ## How the boundary is enforced The `lib/src` rule is a **convention, not a compiler check**. This compiles: ```dart // packages/food/lib/src/checkout/checkout_view_model.dart import 'package:wallet/src/data/wallet_repository.dart'; // reaches into wallet's src ``` What catches it: - the **`implementation_imports`** lint ("Don't import implementation files from another package"), part of `package:lints`' recommended set, which `flutter_lints` includes; - `flutter analyze`, whose `--fatal-infos` flag defaults to true, so a lint hit fails a CI step that runs it; - code review of changes to the top-level `lib/wallet.dart`, which is the only file that widens the surface. Inside the wallet package itself, anything may import `lib/src` files; the rule only concerns other packages. ## A precedent in the SDK Flutter follows the same pattern at scale: `package:flutter/material.dart` is a thin library of `export 'src/material/...'` lines (about 180 of them), and the implementation lives under `packages/flutter/lib/src/material/`. Apps import the one public library, never the files behind it. ## Common mistakes - Exporting whole files without `show`, so every public class in them, including helpers, becomes API by accident. - Re-exporting a repository "just for the shell" instead of exporting a setup function that wires it. - Creating a second public library per screen, which multiplies the surface the team must keep stable. - Letting a sibling feature import `src` "temporarily", which turns an internal refactor into a breaking change for another team. ## Testing against the public surface A useful habit is to write at least one test in the feature package that imports only `package:wallet/wallet.dart`, the same way the app shell does. If that test cannot build the feature's routes or set it up without importing a `src` file, the public API is missing something the shell will also need. Unit tests for internal classes can still import `lib/src` files directly, because the rule only concerns other packages. This keeps two concerns separate: internal tests check behaviour, and the surface test checks that the exported contract is complete and usable on its own.
- An exported function in the wallet package returns a class declared in lib/src that is not exported. What happens to callers?They can call the function and use the returned object, but they cannot name its type, for example in a field or parameter, without importing `package:wallet/src/...`. That pushes them into an implementation import, so every type that appears in an exported signature should be exported too.
- Why is export with show preferred over exporting whole files?`show` lists the exact names that become public. Exporting a whole file makes every public declaration in it part of the API, so a helper someone adds to that file later becomes something other teams can depend on without anyone deciding it should be.
- Does implementation_imports stop the wallet package's own tests from importing lib/src?No. The lint only flags imports of another package's `src` directory. Code in the same package, including its `test/` folder, may import its own `lib/src` files, which is how internal classes get unit tests.
saying these in an interview costs you the question
- Believes the compiler blocks other packages from importing lib/src
- Exports repositories so the app shell can construct them
- Exports whole files without show and calls it encapsulation
- Thinks lib/src also hides files from the package's own tests
- Leaves types used in exported signatures unexported