DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 16: libraries and package management—— the key to efficient development

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;
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// Split file (add_operation.dart)
part of math_operations; // Declare which library this belongs to

int add(int a, int b) => a + b;
Enter fullscreen mode Exit fullscreen mode
// Split file (subtract_operation.dart)
part of math_operations;

int subtract(int a, int b) => a - b;
Enter fullscreen mode Exit fullscreen mode
// 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)
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode
// 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;
}
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. Dependency Management: pub get and Version Rules

After configuring dependencies, install them with:

dart pub get
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}');
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Generate code:

dart run build_runner build
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run checks:

dart analyze
Enter fullscreen mode Exit fullscreen mode

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

  1. Split libraries logically: Each library should focus on a single function, avoiding being too large or too small.
  2. Design clear export interfaces: Use export to carefully design your library's public interface, hiding internal implementations.
  3. Control dependency count: Only depend on necessary packages to avoid project bloat.
  4. Specify precise versions: Use ^ in pubspec.yaml to constrain versions, balancing compatibility and stability.
  5. Update dependencies regularly: Check for updatable dependencies with dart pub outdated and fix security issues promptly.

Top comments (0)