Dart Packages Complete Guide — Create, Publish, and Manage Dependencies
In this tutorial, you will learn about Dart Packages Complete Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Dart packages are reusable units of code distributed through the pub.dev registry, enabling modular architecture through dependency declarations in pubspec.yaml, version constraints, and semantic versioning.
What You Will Learn
- What Dart packages are and how the pub ecosystem works
- Creating a package from scratch with proper structure
- Managing dependencies with pubspec.yaml and version constraints
- Publishing a package to pub.dev
- Using package features like exports, libraries, and privacy
- Best practices for package maintenance and versioning
Why It Matters
Software is built on the shoulders of reusable components. The Dart package ecosystem contains over 40,000 packages covering networking, state management, Serialization, testing, and UI components. Understanding how to create and consume packages transforms you from a code writer into a library author who can share solutions across projects and teams. This skill is critical in production environments like Durga Antivirus Pro, where modular packages handle file scanning, signature updates, and reporting as independently versioned components that can be tested and updated separately.
Real-World Use
Doda Browser uses a custom package called doda_http that wraps the http package with automatic retry logic, token refresh, and request logging. This package is shared across the browser app, a companion desktop tool, and the backend admin panel. When the retry logic needed an update, only doda_http was republished, and all three applications picked up the fix by running dart pub upgrade.
Learning Path
flowchart LR A[Build Runner] --> B[Packages\nYou are here] B --> C[WebSockets] style B fill:#f90,color:#fff
Package Structure
A Dart package is a directory containing a pubspec.yaml file and Dart source files under lib/. The minimal structure looks like this:
my_package/
lib/
my_package.dart
test/
my_package_test.dart
pubspec.yaml
README.md
CHANGELOG.md
LICENSE
The lib/my_package.dart file is the main entry point. Everything declared in files that are exported from this entry point is visible to consumers of the package.
// lib/my_package.dart
/// A utility package for string operations.
library my_package;
export 'src/validators.dart';
export 'src/formatters.dart';
Supporting files live in lib/src/ by convention. Files in src/ are considered private — they can be imported by other files within the package but are not exposed to consumers unless re-exported.
// lib/src/validators.dart
bool isValidEmail(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
bool isValidUrl(String url) {
return Uri.tryParse(url)?.hasScheme ?? false;
}
// lib/src/formatters.dart
String capitalize(String text) {
if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1);
}
String truncate(String text, int maxLength) {
if (text.length <= maxLength) return text;
return '${text.substring(0, maxLength)}...';
}
The pubspec.yaml File
The pubspec.yaml is the manifest file that defines your package metadata, dependencies, and constraints.
name: my_package
description: A utility package for string validation and formatting.
version: 1.0.0
homepage: https://github.com/username/my_package
repository: https://github.com/username/my_package.git
issue_tracker: https://github.com/username/my_package/issues
documentation: https://github.com/username/my_package/wiki
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
collection: ^1.18.0
dev_dependencies:
test: ^1.24.0
lints: ^3.0.0
Version Constraints
Dart uses semantic versioning (major.minor.patch) with the following constraint syntax:
- Caret constraints (
^1.2.3): Allows any version from 1.2.3 to below 2.0.0. Equivalent to>=1.2.3 <2.0.0. This is the most common constraint. - Exact version (
1.2.3): Only version 1.2.3 is accepted. - Range (
>=1.2.0 <2.0.0): Explicit lower and upper bounds. - Any (
any): Accepts any version. Avoid in production packages.
dependencies:
# Caret constraint — allows patches and minors up to next major
http: ^1.2.0
# Exact version — locks to one specific version
path: 1.9.0
# Range constraint — explicit boundaries
meta: '>=1.9.0 <2.0.0'
Dependency Sources
Dependencies can come from various sources beyond pub.dev:
dependencies:
# From pub.dev
http: ^1.2.0
# Git repository
my_toolkit:
git:
url: https://github.com/username/my_toolkit.git
ref: main
# Local path (useful during development)
local_utils:
path: ../local_utils
Creating a Package Step by Step
Use the dart create command to scaffold a new package:
dart create --template package my_package
This creates the basic structure with a default pubspec.yaml, a main library file, and a test file.
// lib/src/string_utils.dart
String reverse(String input) {
return input.split('').reversed.join('');
}
int countVowels(String input) {
return input.split('').where((c) => 'aeiou'.contains(c.toLowerCase())).length;
}
bool isPalindrome(String input) {
final cleaned = input.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '').toLowerCase();
return cleaned == cleaned.split('').reversed.join('');
}
// lib/my_package.dart
export 'src/string_utils.dart';
// test/my_package_test.dart
import 'package:test/test.dart';
import 'package:my_package/my_package.dart';
void main() {
group('StringUtils', () {
test('reverse reverses the string', () {
expect(reverse('hello'), equals('olleh'));
});
test('countVowels counts correctly', () {
expect(countVowels('hello world'), equals(3));
});
test('isPalindrome detects palindromes', () {
expect(isPalindrome('A man, a plan, a canal: Panama'), isTrue);
expect(isPalindrome('hello'), isFalse);
});
});
}
Run the tests:
dart test
Output:
00:01 +1: StringUtils reverse reverses the string
00:01 +2: StringUtils countVowels counts correctly
00:01 +3: StringUtils isPalindrome detects palindromes
00:01 +3: All tests passed!
Publishing to pub.dev
Before publishing, ensure your package is ready:
- Add a descriptive
README.mdwith usage examples. - Maintain a
CHANGELOG.mddocumenting changes per version. - Include a
LICENSEfile (MIT, Apache 2.0, or BSD-3 are common). - Verify the package with
dart pub publish --dry-run.
# Dry run — checks for issues without publishing
dart pub publish --dry-run
# Actual publish
dart pub publish
The publish command creates an account on pub.dev (if needed), uploads your package, and makes it available to the Dart community. After publishing, anyone can add your package as a dependency:
dependencies:
my_package: ^1.0.0
Package Scoring
pub.dev scores packages on three dimensions:
- Popularity: Downloads and usage across the ecosystem.
- Health: Tests, documentation, license, and maintenance activity.
- Maintenance: Time since last update, open issues, and responsiveness.
Aim for a score of 100 by including tests, documentation, a license, and regularly updating dependencies.
Using Packages in Your Project
Consuming a package is straightforward — add it to your pubspec.yaml, run dart pub get, and import it.
dependencies:
http: ^1.2.0
convert: ^3.1.0
dart pub get
import 'package:http/http.dart' as http;
import 'package:convert/convert.dart';
void main() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
print('Status: ${response.statusCode}');
print('Body: ${response.body}');
}
Resolving Version Conflicts
When two dependencies require incompatible versions of the same package, pub reports a conflict. Use dart pub deps to inspect the dependency tree:
dart pub deps
Output:
my_app 1.0.0
├── http 1.2.0
│ └── async 2.11.0
├── collection 1.18.0
└── my_package 1.0.0
└── collection 1.18.0
If a conflict arises, you can either update the conflicting dependencies to compatible versions or use dependency overrides:
dependency_overrides:
collection: 1.19.0
Dependency overrides force a specific version regardless of constraints. Use them temporarily during Migration, not as a permanent solution.
Private Packages
For packages you do not want to publish publicly, use a hosted private pub server or reference them by git or path:
dependencies:
internal_tools:
git:
url: https://github.com/company/internal_tools.git
ref: v1.2.0
Some teams use pub.dev with a private scope, or self-host a pub server using Google Cloud or a custom Dart server.
Common Mistakes
Forgetting to export files: Declaring a class in
lib/src/without re-exporting it from the main library file makes it invisible to consumers. Always export public API surfaces from the main entry point.Using broad version constraints:
anyor>=0.0.0constraints allow breaking changes to be pulled in unexpectedly. Always use caret constraints like^1.2.0to cap the major version.Not running tests before publishing: A package with failing tests erodes trust. Run
dart testand ensure all tests pass before every publish.Ignoring the CHANGELOG: The changelog is the first place users look when upgrading. Document every breaking change, new feature, and bug fix with the corresponding version number.
Publishing without a license: Without a license, the default copyright laws in many jurisdictions restrict usage. Always include a permissive license like MIT or Apache 2.0.
Not following file conventions: Placing implementation files outside
lib/src/or using non-standard directory structures confuses both users and tooling. Follow the standard layout.Hard-coding absolute paths: Library code should never use absolute file paths. Use
Platformutilities fromdart:ioor accept paths as parameters for testability.
Practice Questions
- What does the caret constraint
^2.1.0mean in terms of allowable versions? - Why should implementation details go in
lib/src/rather than the main library file? - How do you resolve a dependency conflict between two packages requiring different versions of the same library?
- What is the purpose of
dart pub publish --dry-run? - Challenge: Create a package called
string_toolkitwith at least five string utility functions (camelCase, snakeCase, kebabCase, truncate, padLeft). Write tests for each function, publish the package locally, and consume it from a separate Dart application.
Mini Project
Build a Dart package called config_loader that:
- Reads YAML or JSON configuration files from a specified path
- Supports nested key access using dot notation (e.g.,
config.get('database.host')) - Falls back to environment variables when keys are not in the file
- Includes comprehensive tests with both YAML and JSON fixtures
- Publishes to pub.dev with README, CHANGELOG, and MIT license
- Add a usage example in the README showing how to load a config file and access values
FAQ
What is Next
Proceed to WebSockets for real-time communication over persistent connections. Then explore GraphQL for query-based API integration patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro