Dart Packages & Libraries
Introduction to Dart Packages
Packages are the fundamental unit of code sharing and reuse in Dart. Whether you are adding a dependency from pub.dev (Dart’s official package registry) or creating your own reusable library, understanding the package system is essential for building maintainable Dart and Flutter applications.
Every Dart project is itself a package. The pubspec.yaml file at the project root defines the package’s name, version, dependencies, and metadata. When you run dart pub get (or flutter pub get), Dart downloads and resolves all dependencies listed in this file.
pub. You interact with it through dart pub commands (or flutter pub in Flutter projects). The central repository for Dart packages is pub.dev.The pubspec.yaml File
The pubspec.yaml file is the heart of every Dart package. It defines everything about your project: its identity, dependencies, and build configuration.
Complete pubspec.yaml Example
name: my_awesome_app
description: A command-line application for data processing.
version: 1.2.0
homepage: https://github.com/myuser/my_awesome_app
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
http: ^1.1.0
path: ^1.8.0
args: ^2.4.0
json_annotation: ^4.8.0
dev_dependencies:
test: ^1.24.0
lints: ^3.0.0
build_runner: ^2.4.0
json_serializable: ^6.7.0
executables:
myapp: main
Key Fields Explained
- name — Package name (lowercase with underscores, must be unique on pub.dev if published)
- description — Brief description (required for publishing)
- version — Semantic versioning:
MAJOR.MINOR.PATCH - environment — Dart SDK version constraints
- dependencies — Packages your code needs at runtime
- dev_dependencies — Packages needed only during development (testing, code generation, linting)
- executables — Maps executable names to entry-point files in
bin/
Dependency Version Constraints
Understanding version constraints is critical for avoiding dependency conflicts. Dart uses semantic versioning and supports several constraint formats.
Version Constraint Syntax
# Caret syntax (recommended) — allows minor and patch updates
http: ^1.1.0 # Equivalent to: >=1.1.0 <2.0.0
# Exact version (rarely needed)
http: 1.1.0
# Range constraint
http: '>=1.0.0 <2.0.0'
# Any version (dangerous — avoid in published packages)
http: any
# Git dependency (useful for unreleased packages)
my_package:
git:
url: https://github.com/user/repo.git
ref: main
path: packages/my_package
# Path dependency (local development)
my_shared_lib:
path: ../my_shared_lib
# Hosted on a custom pub server
my_internal_pkg:
hosted:
name: my_internal_pkg
url: https://pub.mycompany.com
version: ^1.0.0
^) for published packages. It allows compatible updates (patch and minor versions) while preventing breaking changes (major version bumps). This follows the Dart team’s recommended practice.Finding and Adding Packages
The pub.dev registry hosts thousands of community and official packages. Here is how to find, evaluate, and add them to your project.
Working with pub.dev
# Search for packages from the command line
dart pub search http
# Add a dependency (updates pubspec.yaml automatically)
dart pub add http
dart pub add path
dart pub add test --dev # Add as dev dependency
# Remove a dependency
dart pub remove http
# Get all dependencies (download and resolve)
dart pub get
# Upgrade dependencies to latest compatible versions
dart pub upgrade
# Show dependency tree
dart pub deps
# Check for outdated packages
dart pub outdated
# Verify your package is ready for publishing
dart pub publish --dry-run
Creating Libraries with library, export, and part
In Dart, every .dart file is a library. You can organize your code into multiple files and control what is visible to consumers using export, show, hide, and part.
Basic Library Structure
The simplest way to create a library is to use export directives in a single barrel file that re-exports your public API.
Library with Export (Barrel File Pattern)
// ====== lib/my_library.dart (barrel file) ======
/// The main entry point for the my_library package.
library my_library;
export 'src/models/user.dart';
export 'src/models/product.dart';
export 'src/services/api_client.dart';
export 'src/utils/validators.dart';
// Private implementation files are NOT exported
// ====== lib/src/models/user.dart ======
class User {
final String name;
final String email;
User({required this.name, required this.email});
@override
String toString() => 'User($name, $email)';
}
// ====== lib/src/models/product.dart ======
class Product {
final String title;
final double price;
Product({required this.title, required this.price});
}
// ====== lib/src/services/api_client.dart ======
class ApiClient {
final String baseUrl;
ApiClient(this.baseUrl);
Future<String> get(String endpoint) async {
// HTTP request implementation
return '{"status": "ok"}';
}
}
// ====== Consumer code ======
// import 'package:my_library/my_library.dart';
// Now User, Product, and ApiClient are all available
lib/src/ are considered private to the package. Other packages cannot import them directly — they can only access what you explicitly export from your barrel file. This gives you full control over your public API.Using show and hide
When importing a library, you can control which names are brought into scope using show and hide.
Selective Imports with show and hide
// Import only specific classes
import 'package:my_library/my_library.dart' show User, Product;
// Import everything EXCEPT specific classes
import 'package:my_library/my_library.dart' hide ApiClient;
// Use a prefix to avoid name collisions
import 'package:my_library/my_library.dart' as mylib;
void main() {
// With show — only User and Product are available
var user = User(name: 'Alice', email: 'alice@test.com');
// With prefix — everything is available under the prefix
var client = mylib.ApiClient('https://api.example.com');
}
// You can also use show/hide in exports
// ====== lib/my_library.dart ======
export 'src/models/user.dart' show User;
export 'src/services/api_client.dart' hide InternalHelper;
Using part and part of
The part directive splits a single library across multiple files. All parts share the same namespace and can access each other’s private members (names starting with _).
Splitting a Library with part
// ====== lib/calculator.dart (main library file) ======
library calculator;
part 'src/basic_operations.dart';
part 'src/advanced_operations.dart';
class Calculator {
double _memory = 0;
// Can use functions from parts
double add(double a, double b) => _add(a, b);
double subtract(double a, double b) => _subtract(a, b);
double power(double base, int exp) => _power(base, exp);
void store(double value) => _memory = value;
double recall() => _memory;
}
// ====== lib/src/basic_operations.dart ======
part of '../calculator.dart';
// These functions can access private members of the library
double _add(double a, double b) => a + b;
double _subtract(double a, double b) => a - b;
double _multiply(double a, double b) => a * b;
double _divide(double a, double b) {
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a / b;
}
// ====== lib/src/advanced_operations.dart ======
part of '../calculator.dart';
double _power(double base, int exponent) {
double result = 1;
for (var i = 0; i < exponent; i++) {
result = _multiply(result, base); // Can call from other parts
}
return result;
}
part/part of directive is considered legacy in modern Dart. The Dart team recommends using export and normal imports instead. Use part only when you genuinely need multiple files to share private scope — for example, in code generation scenarios like json_serializable where the generated .g.dart file is a part of your source file.Deferred (Lazy) Imports
Deferred imports allow you to load a library on demand rather than at application startup. This is particularly useful for reducing initial load time in web applications.
Deferred Imports
import 'package:heavy_library/heavy_library.dart' deferred as heavy;
Future<void> main() async {
print('App started. Heavy library not loaded yet.');
// Load the library when you actually need it
await heavy.loadLibrary();
// Now you can use its classes and functions
var processor = heavy.DataProcessor();
var result = processor.process([1, 2, 3]);
print('Result: $result');
}
// Common pattern: wrap deferred loading in a function
Future<void> processData(List<int> data) async {
await heavy.loadLibrary();
var processor = heavy.DataProcessor();
print(processor.process(data));
}
// Checking if library is loaded (call loadLibrary multiple times safely)
Future<void> ensureLoaded() async {
// loadLibrary() is safe to call multiple times
// It returns immediately if already loaded
await heavy.loadLibrary();
}
dart2js, where they enable code splitting. In native Dart applications (CLI, server, Flutter), the library is bundled in the binary regardless, so the performance benefit is minimal. However, deferred imports can still be useful for organizing initialization order.Creating Your Own Package
Creating a reusable package follows a standard directory structure and set of conventions. Let’s build a complete utility package from scratch.
Package Directory Structure
my_utils/
lib/
my_utils.dart # Main barrel file (public API)
src/
string_utils.dart # Implementation files
date_utils.dart
validators.dart
test/
string_utils_test.dart # Tests mirror lib/src/ structure
date_utils_test.dart
validators_test.dart
example/
example.dart # Usage examples
pubspec.yaml
README.md
CHANGELOG.md
LICENSE
analysis_options.yaml
Building a Utility Package
// ====== pubspec.yaml ======
// name: my_utils
// version: 1.0.0
// description: A collection of handy utility functions.
// environment:
// sdk: '>=3.0.0 <4.0.0'
// ====== lib/my_utils.dart (barrel file) ======
library my_utils;
export 'src/string_utils.dart';
export 'src/date_utils.dart';
export 'src/validators.dart';
// ====== lib/src/string_utils.dart ======
/// Utility functions for string manipulation.
extension StringUtils on String {
/// Capitalize the first letter of the string.
String get capitalized {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
/// Convert to title case (capitalize each word).
String get titleCase {
return split(' ').map((w) => w.capitalized).join(' ');
}
/// Truncate to a maximum length with ellipsis.
String truncate(int maxLength, {String ellipsis = '...'}) {
if (length <= maxLength) return this;
return '${substring(0, maxLength - ellipsis.length)}$ellipsis';
}
/// Convert to slug format (URL-friendly).
String get slugified {
return toLowerCase()
.replaceAll(RegExp(r'[^a-z0-9\s-]'), '')
.replaceAll(RegExp(r'[\s-]+'), '-')
.replaceAll(RegExp(r'^-|-$'), '');
}
}
// ====== lib/src/date_utils.dart ======
/// Utility functions for date manipulation.
extension DateUtils on DateTime {
/// Check if this date is today.
bool get isToday {
final now = DateTime.now();
return year == now.year && month == now.month && day == now.day;
}
/// Format as relative time (e.g., "2 hours ago").
String get timeAgo {
final diff = DateTime.now().difference(this);
if (diff.inDays > 365) return '${diff.inDays ~/ 365}y ago';
if (diff.inDays > 30) return '${diff.inDays ~/ 30}mo ago';
if (diff.inDays > 0) return '${diff.inDays}d ago';
if (diff.inHours > 0) return '${diff.inHours}h ago';
if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
return 'just now';
}
/// Get the start of the day (midnight).
DateTime get startOfDay => DateTime(year, month, day);
/// Get the end of the day.
DateTime get endOfDay => DateTime(year, month, day, 23, 59, 59, 999);
}
// ====== lib/src/validators.dart ======
/// Common input validators.
class Validators {
static bool isEmail(String value) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value);
}
static bool isUrl(String value) {
return Uri.tryParse(value)?.hasAbsolutePath ?? false;
}
static bool isStrongPassword(String value) {
if (value.length < 8) return false;
if (!value.contains(RegExp(r'[A-Z]'))) return false;
if (!value.contains(RegExp(r'[a-z]'))) return false;
if (!value.contains(RegExp(r'[0-9]'))) return false;
if (!value.contains(RegExp(r'[!@#\$%^&*]'))) return false;
return true;
}
}
Practical Example: Reusable API Client Package
Let’s build a more complete example: a reusable HTTP API client package with error handling, authentication, and JSON parsing.
API Client Package
// ====== lib/src/api_client.dart ======
import 'dart:convert';
import 'dart:io';
/// Configuration for the API client.
class ApiConfig {
final String baseUrl;
final Duration timeout;
final Map<String, String> defaultHeaders;
const ApiConfig({
required this.baseUrl,
this.timeout = const Duration(seconds: 30),
this.defaultHeaders = const {},
});
}
/// Custom exception for API errors.
class ApiException implements Exception {
final int statusCode;
final String message;
final String? body;
ApiException(this.statusCode, this.message, [this.body]);
@override
String toString() => 'ApiException($statusCode): $message';
}
/// A response wrapper with typed data.
class ApiResponse<T> {
final int statusCode;
final T data;
final Map<String, String> headers;
ApiResponse({
required this.statusCode,
required this.data,
required this.headers,
});
bool get isSuccess => statusCode >= 200 && statusCode < 300;
}
/// Reusable API client with authentication and error handling.
class ApiClient {
final ApiConfig config;
final HttpClient _httpClient;
String? _authToken;
ApiClient(this.config) : _httpClient = HttpClient() {
_httpClient.connectionTimeout = config.timeout;
}
/// Set the authentication bearer token.
void setAuthToken(String token) => _authToken = token;
/// Clear the authentication token.
void clearAuth() => _authToken = null;
/// Perform a GET request and decode JSON response.
Future<ApiResponse<Map<String, dynamic>>> get(String endpoint) async {
final uri = Uri.parse('${config.baseUrl}$endpoint');
final request = await _httpClient.getUrl(uri);
_addHeaders(request);
final response = await request.close();
return _processResponse(response);
}
/// Perform a POST request with a JSON body.
Future<ApiResponse<Map<String, dynamic>>> post(
String endpoint,
Map<String, dynamic> body,
) async {
final uri = Uri.parse('${config.baseUrl}$endpoint');
final request = await _httpClient.postUrl(uri);
_addHeaders(request);
request.headers.contentType = ContentType.json;
request.write(jsonEncode(body));
final response = await request.close();
return _processResponse(response);
}
void _addHeaders(HttpClientRequest request) {
config.defaultHeaders.forEach((key, value) {
request.headers.set(key, value);
});
if (_authToken != null) {
request.headers.set('Authorization', 'Bearer $_authToken');
}
}
Future<ApiResponse<Map<String, dynamic>>> _processResponse(
HttpClientResponse response,
) async {
final body = await response.transform(utf8.decoder).join();
final data = jsonDecode(body) as Map<String, dynamic>;
final headers = <String, String>{};
response.headers.forEach((name, values) {
headers[name] = values.join(', ');
});
if (response.statusCode >= 400) {
throw ApiException(response.statusCode, 'Request failed', body);
}
return ApiResponse(
statusCode: response.statusCode,
data: data,
headers: headers,
);
}
/// Close the client and release resources.
void close() => _httpClient.close();
}
// ====== Usage Example ======
// void main() async {
// final client = ApiClient(ApiConfig(
// baseUrl: 'https://api.example.com',
// defaultHeaders: {'Accept': 'application/json'},
// ));
//
// client.setAuthToken('my-jwt-token');
//
// try {
// final response = await client.get('/users/1');
// print('User: ${response.data}');
// } on ApiException catch (e) {
// print('API Error: $e');
// } finally {
// client.close();
// }
// }
Package Versioning and Publishing
If you want to share your package with the Dart community, you can publish it to pub.dev. Here is the versioning strategy and publishing workflow.
Semantic Versioning Rules
# Semantic Versioning: MAJOR.MINOR.PATCH
# 1.0.0 -> First stable release
# PATCH (1.0.0 -> 1.0.1): Bug fixes, no API changes
# - Fixed a crash in parseData()
# - Corrected documentation typos
# MINOR (1.0.0 -> 1.1.0): New features, backwards compatible
# - Added StringUtils.reverse() method
# - New optional parameter in ApiClient constructor
# MAJOR (1.0.0 -> 2.0.0): Breaking changes
# - Removed deprecated methods
# - Changed return type of get() from String to ApiResponse
# - Renamed ApiConfig to ClientConfig
# Pre-release versions
# 1.0.0-alpha.1 -> Early development
# 1.0.0-beta.1 -> Feature complete, may have bugs
# 1.0.0-rc.1 -> Release candidate
# Publishing commands
# dart pub publish --dry-run # Check for issues
# dart pub publish # Publish to pub.dev (irreversible!)
# CHANGELOG.md format:
# ## 1.1.0
# - Added `StringUtils.reverse()` extension method
# - Added `titleCase` getter to StringUtils
# - Fixed issue #42: truncate now handles empty strings
dart pub publish --dry-run to check for common issues (missing description, invalid SDK constraints, unformatted code). Once published, a version cannot be unpublished — it is permanent. Make sure your code is tested and documented before publishing.Best Practices for Dart Packages
Follow these conventions to create professional, maintainable packages:
Package Best Practices Checklist
// 1. Naming: Use lowercase_with_underscores
// Good: my_utils, api_client, date_helper
// Bad: myUtils, MyUtils, my-utils
// 2. File organization: Keep public API in lib/, implementation in lib/src/
// lib/my_package.dart # Public API (barrel file)
// lib/src/internal_stuff.dart # Private implementation
// 3. Documentation: Use /// doc comments on all public APIs
/// Validates an email address.
///
/// Returns `true` if the [email] matches a standard format.
/// Does not verify that the domain actually exists.
///
/// Example:
/// ```dart
/// isEmail('user@example.com'); // true
/// isEmail('not-an-email'); // false
/// ```
bool isEmail(String email) { ... }
// 4. Analysis: Use strict analysis options
// analysis_options.yaml:
// include: package:lints/recommended.yaml
// 5. Testing: Write tests for all public APIs
// test/my_package_test.dart
// 6. Example: Provide a runnable example
// example/example.dart
// 7. Exports: Be explicit about your public API
// DO: export 'src/models.dart' show User, Product;
// AVOID: export 'src/models.dart'; (exports everything)
Summary
In this lesson, you learned the complete Dart package ecosystem:
- pubspec.yaml — the configuration file that defines your package identity, SDK constraints, and dependencies
- pub.dev — finding, evaluating, and adding packages with
dart pub add - Version constraints — caret syntax, ranges, git dependencies, and path dependencies
- Library organization — barrel files,
export,show/hide, and prefixed imports - part/part of — splitting libraries across files (used primarily with code generation)
- Deferred imports — lazy loading libraries for web code splitting
- Creating packages — directory structure, naming conventions, documentation
- Publishing — semantic versioning, dry-run checks, and the publishing workflow
- Best practices — naming, file organization, documentation, testing, and analysis