Organizing Dart code with packages and libraries
Learn how to organize your Dart code into reusable libraries and packages.
In this chapter, you'll level up from basic Dart syntax to building command-line
applications "the Dart way," embracing best practices. You'll learn to refactor
your code into reusable components by creating a dedicated package for handling
command-line arguments. This step sets you up for building a more advanced
command-line application in future chapters, which will integrate specialized
packages for Wikipedia logic and a robust command_runner framework. This
chapter helps you understand Dart libraries, export statements, and how to
structure your project for better organization and maintainability.
Prerequisites
#- Completion of Chapter 3, which covered asynchronous programming and HTTP requests.
Tasks
#
In this chapter, you'll be refactoring the existing dartpedia CLI application
by extracting the command-line argument parsing logic into a separate package
called command_runner. This will improve the structure of your project, making
it more modular and maintainable.
Task 1: Create the command_runner package
#First, create a new Dart package to house the command-line argument parsing logic.
Navigate to the root directory of your project (
/dartpedia).-
Run the following command in your terminal:
bashdart create -t package command_runnerThis command creates a new directory named
command_runnerwith the basic structure of a Dart package. You should now see a new foldercommand_runnerin your project root, alongsidecli.
Task 2: Implement the CommandRunner class
#
Now that you have created the command_runner package, add a
placeholder class that will eventually handle the command-line argument parsing
logic.
-
Open the
command_runner/lib/command_runner.dartfile. Remove any existing placeholder code and add the following:dart/// A simple command runner to handle command-line arguments. /// /// More extensive documentation for this library goes here. library; export 'src/command_runner_base.dart'; // TODO: Export any other libraries intended for clients of this package.Highlights from the preceding code:
library;declares this file as a library, which helps define the boundaries and public interface of a reusable unit of Dart code.export 'src/command_runner_base.dart';is a crucial line that makes declarations fromcommand_runner_base.dartavailable to other packages that import thecommand_runnerpackage. Without thisexportstatement, the classes and functions withincommand_runner_base.dartwould be private to thecommand_runnerpackage, and you wouldn't be able to use them in yourdartpediaapplication.
Open the file
command_runner/lib/src/command_runner_base.dart.-
Remove any existing placeholder code and add the following
CommandRunnerclass tocommand_runner/lib/src/command_runner_base.dart:dartclass CommandRunner { /// Runs the command-line application logic with the given arguments. Future<void> run(List<String> input) async { print('CommandRunner received arguments: $input'); } }Highlights from the preceding code:
CommandRunneris a class that serves as a simplified stand-in for now. Itsrunmethod currently just prints the arguments it receives. In later chapters, you'll expand this class to handle complex command parsing.Future<void>is a return type that indicates that this method might perform asynchronous operations, but doesn't return a value.
Task 3: Add command_runner as a dependency
#
Now that you've created the command_runner package and added a placeholder
CommandRunner class, you need to tell your cli application that it depends
on command_runner. Because the command_runner package is located locally
within your project, use the path dependency option.
Open the
cli/pubspec.yamlfile.-
Locate the
dependenciessection. Add the following lines:yamldependencies: http: ^1.3.0 # Keep your existing http dependency command_runner: path: ../command_runner # Points to your local command_runner packageThis section tells the
cliapplication to depend on thecommand_runnerpackage, and specifies that the package is located in the../command_runnerdirectory (relative to theclidirectory). -
Run
dart pub getin the/dartpedia/clidirectory of your terminal to fetch the new dependency.
Task 4: Import and use the command_runner package
#
Now that you've added command_runner as a dependency, you can import it into
your cli application and replace your existing argument-handling logic with
the new CommandRunner class. This step also fixes the program exit behavior
discussed at the end of Chapter 3.
Open the
cli/bin/cli.dartfile.-
Add the following import statement at the top of the file, alongside your other imports:
dartimport 'package:command_runner/command_runner.dart';This statement imports the
command_runnerpackage, making theCommandRunnerclass available for use. -
Refactor the
mainfunction and remove old logic: Currently, yourmainfunction from Chapter 3 directly handles commands likeversion,help, andwikipedia, and then callssearchWikipedia. You'll now replace all of this custom command-handling logic with a single call to the newCommandRunnerclass.Your
cli/bin/cli.dartfile (from Chapter 3) should currently look like this:dartimport 'dart:io'; import 'package:http/http.dart' as http; import 'package:command_runner/command_runner.dart'; const version = '0.0.1'; void main(List<String> arguments) { if (arguments.isEmpty || arguments.first == 'help') { printUsage(); } else if (arguments.first == 'version') { print('Dartpedia CLI version $version'); } else if (arguments.first == 'wikipedia') { final inputArgs = arguments.length > 1 ? arguments.sublist(1) : null; searchWikipedia(inputArgs); } else { printUsage(); } } void searchWikipedia(List<String>? arguments) async { /* ... existing logic ... */ } void printUsage() { /* ... existing logic ... */ } Future<String> getWikipediaArticle(String articleTitle) async { /* ... existing logic ... */ }Now, replace the entire contents of
cli/bin/cli.dart(except for thehttpimport) with the following updated version:dartimport 'dart:io'; import 'package:http/http.dart' as http; import 'package:command_runner/command_runner.dart'; void main(List<String> arguments) async { // main is now async and awaits the runner var runner = CommandRunner(); // Create an instance of your new CommandRunner await runner.run(arguments); // Call its run method, awaiting its Future<void> }Highlights from the preceding code:
void main(List<String> arguments) asyncdirectly addresses the program not exiting cleanly issue from Chapter 3. Notice thatmainis now declaredasync. This is essential becauserunner.run()returns aFuture, andmainmustawaitits completion to ensure the program waits for all asynchronous tasks to finish before exiting.var runner = CommandRunner();creates an instance of theCommandRunnerclass from your newcommand_runnerpackage.await runner.run(arguments);calls therunmethod on theCommandRunnerinstance, passing in the command-line arguments.
Removed Functions:
The
printUsage,searchWikipedia, andgetWikipediaArticlefunctions are now completely removed fromcli/bin/cli.dart. Their logic will be redesigned and moved into thecommand_runnerpackage in future chapters, as part of building the full command-line framework.
Task 5: Run the application
#
Now that you've refactored the code and updated the cli application to use the
command_runner package, run the application to verify that everything
is working correctly at this stage.
Open your terminal and navigate to the
clidirectory.-
Run the
wikipediacommand:bashdart run bin/cli.dart wikipedia Computer_programming -
Ensure that the application now executes without errors and print the arguments to the console, demonstrating that the control has successfully transferred to your new
command_runnerpackage.bashCommandRunner received arguments: [wikipedia, Computer_programming]
Review
#In this chapter, you learned about:
- Creating Dart packages using
dart create -t package. -
Using
exportstatements to make declarations from one library available in another. -
Adding local packages as dependencies using the
pathoption inpubspec.yaml. - Importing packages into your Dart code using
importstatements. -
Refactoring code to improve organization and maintainability, including making
mainasyncto correctlyawaitasynchronous operations.
Quiz
#Check your understanding
1 / 3export statement in a Dart library?
-
To make declarations from other files available through your library's public API.
That's right!
Export lets you re-expose declarations. For example,
export 'src/command.dart';makesCommandaccessible to anyone importing your package. -
To hide implementation details from other libraries.
Not quite.
The
exportstatement does the opposite of hiding. To hide implementation-only members, don't include them in any export statement. -
To import multiple libraries with a single statement.
Not quite.
importandexportserve different purposes.importmakes code available to your library, whileexportdoes something for consumers of your library. -
To specify which Dart SDK versions your package supports.
Not quite.
SDK constraints go in
pubspec.yaml, not in Dart code.exportis a Dart language feature, not a configuration option.
-
Use a path dependency like
command_runner: {path: ../command_runner}.That's right!
Path dependencies point to a local directory. This is perfect for multi-package projects or developing packages before publishing.
-
Use a version constraint like
command_runner: ^1.0.0.Not quite.
Version constraints are for packages published on pub.dev. Local packages require a different kind of dependency specification.
-
Copy the package folder into your project's
lib/directory.Not quite.
Copying creates duplication and versioning problems. Dart provides a way to reference packages by their location without copying them.
-
Run
dart pub add command_runner --localNot quite.
There's no
--localflag fordart pub add. You need to specify the location differently inpubspec.yamlor by passing a source descriptor todart pub add.
lib/src/parser.dart. Why might you create lib/parser.dart that just contains export 'src/parser.dart';?
-
To allow users to import from a clean path while keeping implementation files organized in
src/.That's right!
Users can write
import 'package:mylib/parser.dart';instead of reaching intosrc/. This separates your public API from internal organization. -
To make the code run faster by reducing import depth.
Not quite.
Export doesn't affect runtime performance. The number of directories in an import path has no impact on how fast code runs.
-
Because Dart requires all public files to be in the
lib/root directory.Not quite.
Dart doesn't require this structure. It's a convention, not a language requirement.
-
To prevent other packages from importing the
src/files directly.Not quite.
Files in
src/can technically still be imported directly. Dart best practices discourage it, but don't prevent it.
Next lesson
#
In the next chapter, you'll dive into object-oriented programming (OOP) concepts
in Dart. You'll learn how to create classes, define inheritance relationships,
and build a more robust command-line argument parsing framework using OOP
principles within your new command_runner package.
Unless stated otherwise, the documentation on this site reflects Dart 3.10.3. Page last updated on 2025-12-21. View source or report an issue.