Package Development, Publishing & Open Source Maintenance
Build, Publish & Maintain Packages Like a Pro
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Flutter/Dart package development is the process of creating reusable libraries that can be shared on pub.dev or used privately within organizations. It encompasses everything from structuring code for maximum reusability, to writing comprehensive documentation and tests, to publishing and maintaining packages over time. For senior engineers, package development skills signal architectural thinking — you're not just consuming the ecosystem, you're contributing to it.
Real-world relevance
Every major Flutter app depends on dozens of community packages. Companies like Very Good Ventures, Invertase, and the Flutter team themselves maintain hundreds of packages. Creating internal packages is standard practice in enterprises — shared design systems, API clients, analytics wrappers, and utility libraries. Open source packages build your professional reputation: Riverpod's creator (Remi Rousselet) became one of Flutter's most recognized community members through package development.
Key points
- Package Structure — lib/, example/, test/ — A Dart package has a strict conventional structure: lib/ contains your library source code (with lib/src/ for private implementation), example/ contains a runnable demo app, test/ contains unit and widget tests, and the root has pubspec.yaml, README.md, CHANGELOG.md, and LICENSE. The lib/.dart file is the public barrel export — it controls exactly which APIs are visible to consumers. Everything in lib/src/ is private unless explicitly exported.
- pubspec.yaml — Packages vs Apps — Package pubspec.yaml differs from app pubspec.yaml: packages use 'version' (required for publishing), 'homepage' or 'repository' (links on pub.dev), 'environment' with SDK constraints, and 'dependencies' with version ranges (^2.0.0) not exact pins. Never use 'dependency_overrides' in published packages. Set 'publish_to: none' for private packages. Use 'platforms' to declare supported platforms. The 'topics' and 'screenshots' fields improve discoverability on pub.dev.
- Barrel Exports & API Surface Control — Your main lib/.dart file should explicitly export only public APIs: export 'src/models/user.dart'; export 'src/widgets/custom_button.dart'; Never export everything with 'export src/' — this leaks internal implementation. Use 'show' and 'hide' to fine-tune: export 'src/utils.dart' show formatDate, parseDate; This is critical for maintaining backward compatibility — unexported code can change freely without breaking consumers.
- Publishing to pub.dev — Run 'dart pub publish --dry-run' first to check for issues. Then 'dart pub publish' to upload. You must authenticate with a Google account. Packages are immutable once published — you cannot delete or overwrite a version. If you publish broken code, you must publish a new version. Retraction: you can 'retract' a version (marks it as broken, warns users) but cannot remove it. This immutability means you should ALWAYS dry-run and test thoroughly before publishing.
- Semantic Versioning & Breaking Changes — Follow strict semver: MAJOR.MINOR.PATCH. PATCH (1.0.1): bug fixes, no API changes. MINOR (1.1.0): new features, backward compatible. MAJOR (2.0.0): breaking API changes. Pre-1.0.0 (0.x.y): breaking changes allowed in minor versions. Pub's version solver uses semver — if you break API in a minor version, you'll break every consumer's build. In interviews, explain that semver is a CONTRACT with your users, not a suggestion.
- Writing Effective README & API Docs — A great package README includes: 1-line description, installation, basic usage with code, features list, advanced usage, migration guides for major versions, and badges (pub version, build status, coverage). API docs are generated from dartdoc comments (///). Write examples in doc comments — they appear on pub.dev API reference. Run 'dart doc' locally to preview. The pub.dev scoring algorithm (pana) checks documentation quality and penalizes missing docs.
- Federated Plugins — Platform-Specific Code — Federated plugins split platform code into separate packages: my_plugin (app-facing API), my_plugin_platform_interface (shared interface), my_plugin_ios (iOS implementation), my_plugin_android (Android implementation), my_plugin_web (web implementation). This allows third parties to add platform support without modifying the core package. Use MethodChannel for platform communication. Register implementations via MyPluginPlatform.instance = MyPluginIOS(). This is the standard architecture for Flutter team plugins.
- Testing Packages Across Platforms — Packages need comprehensive testing: unit tests for logic, widget tests for UI components, integration tests for platform plugins, and golden tests for visual regression. Run tests on CI across platforms: flutter test (all), flutter test --platform chrome (web). For platform plugins, use the integration_test package with real devices/emulators. Test edge cases: null parameters, empty lists, large datasets, RTL layouts, dark mode. Minimum 80% coverage for credible packages.
- Maintaining Open Source — Issues & PRs — Good maintenance includes: issue templates (bug report, feature request), PR templates, CONTRIBUTING.md with code style and process, labeled issues (good first issue, help wanted, bug, enhancement), timely responses (within 1 week), and a clear PR review process. Use GitHub Actions for CI — auto-run tests, check formatting (dart format), analyze (dart analyze), and verify pub score. A well-maintained package with 100 stars beats an abandoned one with 1000.
- CHANGELOG & Migration Guides — Maintain CHANGELOG.md in Keep a Changelog format: ## [2.0.0] - 2026-03-11 / ### Breaking / ### Added / ### Fixed. For major versions, write a migration guide showing old vs new API with code examples. pub.dev displays the changelog prominently. Many developers check the changelog before the README. A good changelog reduces issues and support requests by 50% — users can self-serve answers to 'what changed in v2?'
- pub.dev Scoring & Package Quality — pub.dev scores packages on 5 criteria: Follow Dart file conventions (20pts), Provide documentation (20pts), Support multiple platforms (20pts), Pass static analysis (20pts), Support up-to-date dependencies (20pts). Maximum 140 'pub points' plus popularity and likes. To maximize score: add dartdoc to all public APIs, support all platforms, fix all analyzer warnings, keep dependencies current, include an example. High pub points = more visibility = more adoption.
- Monorepo Strategies with Melos — For multi-package projects, use Melos — a monorepo management tool. It handles: versioning all packages together, running commands across packages, managing inter-package dependencies with path overrides, automated changelogs, and coordinated publishing. Configure melos.yaml at the repo root. Commands: melos bootstrap (link packages), melos run test (test all), melos version (bump versions). Flutter's own packages repo uses a similar monorepo approach.
Code example
// Complete package structure example: a reusable form validator
// === pubspec.yaml ===
// name: form_validator_pro
// description: Production-ready form validation with built-in rules,
// async validation, and composable validators.
// version: 2.1.0
// homepage: https://github.com/yourname/form_validator_pro
// repository: https://github.com/yourname/form_validator_pro
// issue_tracker: https://github.com/yourname/form_validator_pro/issues
// topics:
// - form
// - validation
// - input
//
// environment:
// sdk: '>=3.0.0 <4.0.0'
// flutter: '>=3.10.0'
//
// dependencies:
// flutter:
// sdk: flutter
//
// dev_dependencies:
// flutter_test:
// sdk: flutter
// flutter_lints: ^3.0.0
// mocktail: ^1.0.0
// === lib/form_validator_pro.dart (barrel export) ===
/// A production-ready form validation library for Flutter.
///
/// Basic usage:
/// ```dart
/// final validator = Validator.compose([
/// Validator.required('Email is required'),
/// Validator.email('Invalid email format'),
/// ]);
///
/// // Use in TextFormField
/// TextFormField(validator: validator.call);
/// ```
library form_validator_pro;
export 'src/validator.dart';
export 'src/rules/rules.dart';
export 'src/async_validator.dart';
export 'src/extensions.dart';
// Note: src/internal_utils.dart is NOT exported — it's private
// === lib/src/validator.dart ===
/// A composable form field validator.
///
/// Validators can be combined using [compose] and [composeOR]:
/// ```dart
/// final emailValidator = Validator.compose([
/// Validator.required('Required'),
/// Validator.email('Invalid email'),
/// Validator.maxLength(100, 'Too long'),
/// ]);
/// ```
class Validator {
final String? Function(String?) _validate;
const Validator._(this._validate);
/// Validates the given [value] and returns an error message or null.
String? call(String? value) => _validate(value);
/// Composes multiple validators — ALL must pass (AND logic).
/// Returns the first error encountered, or null if all pass.
static Validator compose(List<Validator> validators) {
return Validator._((value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) return error;
}
return null;
});
}
/// Composes validators with OR logic — at least one must pass.
static Validator composeOR(
List<Validator> validators, {
required String errorMessage,
}) {
return Validator._((value) {
for (final validator in validators) {
if (validator(value) == null) return null;
}
return errorMessage;
});
}
/// Value must not be null or empty.
static Validator required(String message) {
return Validator._((value) {
if (value == null || value.trim().isEmpty) return message;
return null;
});
}
/// Value must be a valid email format.
static Validator email(String message) {
return Validator._((value) {
if (value == null || value.isEmpty) return null; // Use required() for null check
final regex = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.]+$');
return regex.hasMatch(value) ? null : message;
});
}
/// Value length must be at least [min] characters.
static Validator minLength(int min, String message) {
return Validator._((value) {
if (value == null || value.length < min) return message;
return null;
});
}
/// Value length must not exceed [max] characters.
static Validator maxLength(int max, String message) {
return Validator._((value) {
if (value != null && value.length > max) return message;
return null;
});
}
/// Value must match the given [pattern].
static Validator pattern(RegExp pattern, String message) {
return Validator._((value) {
if (value == null || value.isEmpty) return null;
return pattern.hasMatch(value) ? null : message;
});
}
/// Custom validation logic.
static Validator custom(String? Function(String?) validate) {
return Validator._(validate);
}
}
// === lib/src/async_validator.dart ===
/// Async validator for server-side checks (e.g., username availability).
///
/// Example:
/// ```dart
/// final usernameValidator = AsyncValidator(
/// validator: Validator.compose([
/// Validator.required('Required'),
/// Validator.minLength(3, 'Too short'),
/// ]),
/// asyncCheck: (value) => api.isUsernameAvailable(value),
/// asyncErrorMessage: 'Username already taken',
/// debounce: Duration(milliseconds: 500),
/// );
/// ```
class AsyncValidator {
final Validator validator;
final Future<bool> Function(String) asyncCheck;
final String asyncErrorMessage;
final Duration debounce;
const AsyncValidator({
required this.validator,
required this.asyncCheck,
required this.asyncErrorMessage,
this.debounce = const Duration(milliseconds: 300),
});
/// Validates synchronously first, then async if sync passes.
Future<String?> validate(String? value) async {
// Sync validation first (fast fail)
final syncError = validator(value);
if (syncError != null) return syncError;
if (value == null || value.isEmpty) return null;
// Async validation (e.g., server check)
try {
final isValid = await asyncCheck(value);
return isValid ? null : asyncErrorMessage;
} catch (e) {
return 'Validation failed: please try again';
}
}
}
// === test/validator_test.dart ===
// void main() {
// group('Validator.required', () {
// final validator = Validator.required('Required');
//
// test('returns error for null', () {
// expect(validator(null), 'Required');
// });
//
// test('returns error for empty string', () {
// expect(validator(''), 'Required');
// });
//
// test('returns null for valid input', () {
// expect(validator('hello'), isNull);
// });
// });
//
// group('Validator.compose', () {
// final validator = Validator.compose([
// Validator.required('Required'),
// Validator.email('Invalid email'),
// ]);
//
// test('returns first error in chain', () {
// expect(validator(''), 'Required');
// expect(validator('notanemail'), 'Invalid email');
// });
//
// test('returns null when all pass', () {
// expect(validator('test@example.com'), isNull);
// });
// });
// }Line-by-line walkthrough
- 1. The pubspec.yaml shows package-specific fields: version (required for pub.dev), homepage/repository (links), topics (for searchability), and SDK constraints with ranges (not exact pins).
- 2. The barrel export file explicitly exports only public APIs — src/internal_utils.dart is intentionally NOT exported, keeping it private to the package.
- 3. Library-level dartdoc (///) with code examples appears on pub.dev — this is the first thing users see and directly impacts adoption.
- 4. The Validator class uses a private constructor (Validator._) with static factory methods — this forces users to use the composable API rather than subclassing.
- 5. Validator.compose() chains validators with AND logic — the first error wins. This is the builder pattern adapted for validation.
- 6. The email validator returns null for empty strings — responsibility separation. Use required() for null/empty checks, email() for format checks. Compose them together.
- 7. AsyncValidator shows the sync-first-then-async pattern — fail fast on cheap checks before making expensive network calls.
- 8. The debounce field prevents firing async validation on every keystroke — essential for server-side checks like username availability.
- 9. The test examples show the testing pattern: group by validator type, test error cases AND success cases, test composition behavior.
- 10. Note the overall architecture: small, focused classes with clear single responsibilities, composed together at the call site. This is what makes packages reusable.
Spot the bug
// pubspec.yaml issues
// name: My Cool Package // Bug 1
// version: 1.0
// dependencies:
// http: 1.2.0 // Bug 2
// lib/my_cool_package.dart
export 'src/'; // Bug 3: exports everything
// lib/src/helper.dart
class _InternalHelper { // Bug 4
static String format(String s) => s.trim();
}
class PublicApi {
// Bug 5: no dartdoc
String process(dynamic input) { // Bug 6: dynamic
return _InternalHelper.format(input.toString());
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Creating Dart Packages — Official Guide (dart.dev)
- Publishing to pub.dev (dart.dev)
- Federated Plugins — Flutter Architecture (flutter.dev)
- Melos — Dart Monorepo Tool (invertase.dev)