Lesson 76 of 77 advanced

Package Development, Publishing & Open Source Maintenance

Build, Publish & Maintain Packages Like a Pro

Open interactive version (quiz + challenge)

Real-world analogy

Creating a Dart package is like opening a specialty food store. Your recipe (library code) needs to be perfect, but that's only half the battle. You need attractive packaging (README & docs), clear ingredient labels (API documentation), nutritional info (example app), quality certifications (tests passing on CI), a barcode for distribution (pub.dev listing), and a commitment to handling customer complaints (issue management). The best store in the world fails if no one knows it exists or the labels are wrong.

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

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. 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. 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. 3. Library-level dartdoc (///) with code examples appears on pub.dev — this is the first thing users see and directly impacts adoption.
  4. 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. 5. Validator.compose() chains validators with AND logic — the first error wins. This is the builder pattern adapted for validation.
  6. 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. 7. AsyncValidator shows the sync-first-then-async pattern — fail fast on cheap checks before making expensive network calls.
  8. 8. The debounce field prevents firing async validation on every keystroke — essential for server-side checks like username availability.
  9. 9. The test examples show the testing pattern: group by validator type, test error cases AND success cases, test composition behavior.
  10. 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?
Six issues: package naming, version format, dependency pinning, over-exporting, missing documentation, and poor type safety.
Show answer
1) Package name must be lowercase with underscores — 'my_cool_package' not 'My Cool Package'. 2) Version must be full semver — '1.0.0' not '1.0'. Dependencies should use caret syntax (^1.2.0) not exact pins, which prevents consumers from resolving compatible versions. 3) Never export an entire directory — explicitly export specific files to control your public API surface. 4) _InternalHelper has an underscore prefix making it file-private, but it's used by PublicApi in the same file which is fine — however, the real issue is that exporting 'src/' would expose non-underscore-prefixed classes too. 5) All public APIs need /// dartdoc comments — pub.dev pana scoring penalizes missing documentation. 6) 'dynamic input' defeats Dart's type system — use a proper type like String to provide compile-time safety.

Explain like I'm 5

Imagine you bake amazing cookies at home. A package is like putting your cookie recipe in a nice box so other people can make the same cookies. You write clear instructions (documentation), list all ingredients (dependencies), put a version number on the box (1.0.0), and ship it to the store (pub.dev). If you change the recipe and someone's cookies taste different — that's a breaking change, and you need a new box number (2.0.0). Once a box is on the shelf, you can't sneak in and change it — you have to put out a new one!

Fun fact

The most depended-on package on pub.dev (excluding SDK packages) is 'collection' with over 90% of all Flutter apps depending on it transitively. The pub.dev scoring algorithm (pana) was named after the Hindi/Urdu word for 'to obtain/get' — fitting for a package manager. And the 'dart pub publish' command is irreversible by design — once published, a version exists forever, which is why the Flutter team internally calls publishing 'the point of no return.'

Hands-on challenge

Create a complete Dart package called 'result_type' that provides a Rust-inspired Result type for error handling without exceptions. Include: 1) Result sealed class with Ok and Err subclasses, 2) map(), flatMap(), mapError(), fold() methods, 3) Full dartdoc documentation with examples, 4) Comprehensive tests (15+ test cases covering all methods and edge cases), 5) A proper barrel export file, 6) README with installation, basic usage, and advanced patterns, 7) CHANGELOG.md for version 1.0.0, 8) Run 'dart pub publish --dry-run' and fix all issues until it passes cleanly.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery