In previous lessons, we've mastered Dart's core syntax and key technologies like asynchronous programming. As project scales grow, organizing and reusing code becomes increasingly important. Today we'll learn about libraries and package management – core knowledge for solving code modularization, reuse, and dependency management challenges, which are crucial for efficient development.
I. Custom Libraries: Organizing Code Modularly
In Dart, a library is the basic unit of code organization. A library can contain multiple classes, functions, variables, etc., and uses keywords like library, export, and part to split and combine code.
1. Basic Library Definition and Import
The simplest library is a single Dart file, which we can import directly using the import statement:
// Define a utility library (utils.dart)
class StringUtils {
static bool isEmpty(String? str) => str == null || str.trim().isEmpty;
}
int add(int a, int b) => a + b;
// Import and use the library (main.dart)
import 'utils.dart'; // Import utils.dart from the current directory
void main() {
print(StringUtils.isEmpty("")); // Output: true
print(add(2, 3)); // Output: 5
}
import path rules:
- Relative paths: 'utils.dart' (same directory), 'lib/utils.dart' (subdirectory)
- Absolute paths: Full paths from the project root (rarely used)
- Package paths: 'package:my_project/utils.dart' (for libraries in pub packages)
2. Controlling Visibility: Private Members with _
In libraries, members (classes, functions, variables) starting with _ are private members and only visible within the current library:
// Utility library (utils.dart)
class _PrivateClass {
void method() => print("Private class method");
}
void publicFunction() => print("Public function");
void _privateFunction() => print("Private function");
// Import and use (main.dart)
import 'utils.dart';
void main() {
publicFunction(); // Successfully calls: Public function
// _privateFunction(); // Compile error: Cannot access private function
// var obj = _PrivateClass(); // Compile error: Cannot access private class
}
The private member mechanism ensures library encapsulation, exposing only necessary interfaces.
3. Splitting and Combining Libraries: part/part of
When a library grows large, it can be split into multiple files and connected using part and part of:
// Main library (math_operations.dart)
library math_operations; // Declare library name
part 'add_operation.dart'; // Include split files
part 'subtract_operation.dart';
int multiply(int a, int b) => a * b;
// Split file (add_operation.dart)
part of math_operations; // Declare which library this belongs to
int add(int a, int b) => a + b;
// Split file (subtract_operation.dart)
part of math_operations;
int subtract(int a, int b) => a - b;
// Use the combined library (main.dart)
import 'math_operations.dart';
void main() {
print(add(5, 3)); // Output: 8 (from add_operation.dart)
print(subtract(5, 3)); // Output: 2 (from subtract_operation.dart)
print(multiply(5, 3)); // Output: 15 (from main library)
}
This approach is ideal for splitting a logically cohesive library into multiple files while maintaining clear structure.
4. Exporting Libraries: export and Library Aggregation
The export keyword allows exporting one library's contents for use by other libraries, enabling library aggregation:
// Base utility library (base_utils.dart)
class Logger {
static void log(String message) => print("[Log] $message");
}
// Extended utility library (extended_utils.dart)
export 'base_utils.dart'; // Export the base utility library
class Validator {
static bool isNumber(String str) => double.tryParse(str) != null;
}
// Usage (main.dart)
import 'extended_utils.dart'; // Only need to import the extended library
void main() {
Logger.log("Test log"); // Available: from base_utils.dart (exported)
print(Validator.isNumber("123")); // Available: from extended_utils.dart
}
export is often used to create "aggregate libraries," allowing users to import multiple related libraries through a single entry point.
II. pubspec.yaml: Package Configuration and Dependency Management
The Dart ecosystem shares code through packages, each containing a pubspec.yaml file that describes package information and dependencies.
1. Basic Structure of pubspec.yaml
A typical pubspec.yaml looks like this:
name: my_app # Project/package name (required)
description: A sample Dart application. # Description (optional)
version: 1.0.0 # Version number (follows semantic versioning: major.minor.patch)
environment:
sdk: '>=3.0.0 <4.0.0' # Supported Dart SDK version range
dependencies:
# Production dependencies (add when needed)
http: ^0.13.5 # HTTP network request library
path: ^1.8.3 # Path handling library
dev_dependencies:
# Development dependencies (only used during development)
lints: ^2.1.0 # Code style checking tool
test: ^1.24.0 # Testing framework
2. Dependency Management: pub get and Version Rules
After configuring dependencies, install them with:
dart pub get
This command:
- Downloads specified dependencies from pub.dev (Dart's official package repository)
- Generates a pubspec.lock file recording exact dependency versions
- Caches dependencies locally (~/.pub-cache/)
Version numbers follow semantic versioning with common rules:
- ^1.2.3: Compatible with versions 1.2.3 and above but below 2.0.0 (recommended)
- 1.2.3: Exact match for version 1.2.3
- >=1.2.3 <1.3.0: Specifies a version range
3. Importing Libraries from Packages
After installing dependencies, import library files from packages using the package: path:
// Import libraries from the http package
import 'package:http/http.dart' as http;
// Import libraries from the path package
import 'package:path/path.dart' as path;
void main() async {
// Use http library to send network requests
final response = await http.get(Uri.parse('https://api.example.com'));
print('HTTP status code: ${response.statusCode}');
// Use path library to handle paths
final joinedPath = path.join('dir', 'file.txt');
print('Joined path: $joinedPath'); // Output: dir/file.txt
}
as http assigns a prefix to the library, avoiding conflicts between members with the same name from different libraries.
4. Dependency Overrides and Local Packages
During development, you may need to:
- Override dependency versions (e.g., use a specific branch of a package)
- Depend on locally developed packages Configure these in pubspec.yaml:
dependencies:
# Depend on a local package (relative path)
my_local_package:
path: ../my_local_package
# Depend on a package from a Git repository
my_git_package:
git:
url: https://github.com/username/my_git_package.git
ref: main # Branch/tag/commit
III. Introduction to Common Official and Popular Packages
The Dart ecosystem has many high-quality packages. Here are some commonly used in development:
1. http: Network Requests
http is an officially maintained HTTP client library for sending GET, POST, and other network requests:
import 'package:http/http.dart' as http;
void main() async {
// Send GET request
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/todos/1'),
);
if (response.statusCode == 200) {
print('Response content: ${response.body}');
} else {
print('Request failed with status code: ${response.statusCode}');
}
}
2. path: Path Handling
The path library provides cross-platform path handling tools, solving path format differences between operating systems (Windows/Linux/macOS):
import 'package:path/path.dart' as path;
void main() {
// Join paths
print(path.join('home', 'user', 'file.txt')); // Output: home/user/file.txt
// Get filename
print(path.basename('/home/user/file.txt')); // Output: file.txt
// Get file extension
print(path.extension('image.jpg')); // Output: .jpg
}
3. json_serializable: JSON Serialization
json_serializable is a popular package for JSON serialization, enabling type-safe JSON conversion through code generation:
Configure dependencies:
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.4
json_serializable: ^6.7.1
Define model class:
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // Generated code file
@JsonSerializable()
class User {
final String name;
final int age;
User({required this.name, required this.age});
// Generated method for JSON to object conversion
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// Generated method for object to JSON conversion
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Generate code:
dart run build_runner build
Usage:
import 'dart:convert';
import 'user.dart';
void main() {
// Convert JSON string to object
final json = '{"name":"Alice", "age":20}';
final user = User.fromJson(jsonDecode(json));
print('${user.name}, ${user.age}'); // Output: Alice, 20
// Convert object to JSON
final userJson = user.toJson();
print(userJson); // Output: {name: Alice, age: 20}
}
4. lints: Code Style Checking
lints provides code style checking to help teams maintain consistent code style:
After configuring dependencies, create analysis_options.yaml:
include: package:lints/recommended.yaml
Run checks:
dart analyze
The tool will highlight code that doesn't meet style guidelines (like unused variables or non-standard naming).
IV. Best Practices for Libraries and Packages
- Split libraries logically: Each library should focus on a single function, avoiding being too large or too small.
- Design clear export interfaces: Use export to carefully design your library's public interface, hiding internal implementations.
- Control dependency count: Only depend on necessary packages to avoid project bloat.
- Specify precise versions: Use ^ in pubspec.yaml to constrain versions, balancing compatibility and stability.
- Update dependencies regularly: Check for updatable dependencies with dart pub outdated and fix security issues promptly.
Top comments (0)