skip to content

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?

level: middleimportance: must knowfreq 45%

answer

  1. a small surface other teams depend on
  2. entry points, contracts, types in signatures
  3. view models and repositories stay in src
  4. export with show, one file per package
  5. implementation_imports fails flutter analyze

basics

~20 s

Export 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 s

A 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
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;

go deeper

for a junior

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.

for a middle

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.

for a senior

Show how the boundary is enforced in practice: implementation_imports, flutter analyze failing on infos, and review of the top-level library file.

for a principal

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