Object-oriented Dart programming
In this chapter, you'll explore the power of object-oriented programming (OOP) in Dart. You'll learn how to create classes and define relationships between them, including inheritance and abstract classes. You'll also build a foundation for creating well-structured CLI applications.
Prerequisites
#Before you begin this chapter, ensure you:
-
Have completed Chapter 4 and have a working Dart development environment with
the
dartpediaproject. - Are familiar with basic programming concepts like variables, functions, and control flow.
- Understand the concepts of packages and libraries in Dart.
Tasks
#A command-line interface (CLI) is defined by the commands, options, and arguments a user can type into their terminal.
By the end of this lesson, you will have built a framework that can understand a command like this:
$ dartpedia help --verbose --command=search
Here is a breakdown of each part:
dartpedia: This is the executable, the name of your application.-
help: This is a command, an action you want the application to perform. -
--verbose: This is a flag (a type of option that doesn't take a value), which modifies the command's behavior. -
--command=search: This is an option that takes a value. Here, theoptionis namedcommand, and its value issearch.
The classes and logic you build in the following tasks create the foundation for parsing and executing commands just like this one.
Task 1: Define the argument hierarchy
#
First, you'll define an Argument class, an Option class, and a Command
class, establishing an inheritance relationship.
-
Create the file
command_runner/lib/src/arguments.dart. This file will contain the definitions for yourArgument,Option,Command, andArgResultsclasses. -
Define an
enumcalledOptionType.command_runner/lib/src/arguments.dartdartenum OptionType { flag, option }This
enumrepresents the type of option, which can be either aflag(a boolean option) or a regularoption(an option that takes a value). Enums are useful for representing a fixed set of possible values. -
Define an
abstract classcalledArgument.Start by defining the basic structure of your
Argumentclass. You'll declare it asabstract, which means it serves as a blueprint that other classes can extend, but it cannot be instantiated on its own.Below the
enumyou just added, paste in the following code:command_runner/lib/src/arguments.dartdart// Paste this new class below the enum you added abstract class Argument { String get name; String? get help; // In the case of flags, the default value is a bool // In other options and commands, the default value is String // NB: flags are just Option objects that don't take arguments Object? get defaultValue; String? get valueHelp; String get usage; }nameis aStringthat uniquely identifies the argument.helpis an optionalStringthat provides a description.defaultValueis of typeObject?because it can be abool(for flags) or aString.valueHelpis an optionalStringto give a hint about the expected value.- The
usagegetter will provide a string showing how to use the argument.
With the
Argumentclass fully defined, you have a common interface for all types of command-line arguments. Next, you'll build upon this by definingOption, a specific type of argument that extendsArgument. -
Define a class called
OptionthatextendsArgument.The
Optionclass will represent command-line options like--verboseor--output=file.txt. It will inherit from yourArgumentclass.Add the following
Optionclass to the bottom of your file:command_runner/lib/src/arguments.dartdartclass Option extends Argument { Option( this.name, { required this.type, this.help, this.abbr, this.defaultValue, this.valueHelp, }); @override final String name; final OptionType type; @override final String? help; final String? abbr; @override final Object? defaultValue; @override final String? valueHelp; @override String get usage { if (abbr != null) { return '-$abbr,--$name: $help'; } return '--$name: $help'; } }The
extendskeyword establishes the inheritance relationship. The constructor uses@overrideto provide concrete implementations for the properties defined inArgument. It also addstype(using theOptionTypeenum) and an optionalabbrfor a short-form of the option. Theusagegetter is implemented to provide clear instructions to the user.With
Optioncomplete, you have a specialized type of argument. Next, you'll define theCommandclass, another type of argument that will represent the main actions a user can perform in your CLI application. -
Define an
abstract classcalledCommandthat alsoextendsArgument.The
Commandclass will represent an executable action. Since it provides a template for other commands to follow, you'll declare it asabstract.command_runner/lib/src/arguments.dartdart// Add this class below the Option class abstract class Command extends Argument { // Properties and methods will go here }The
abstractkeyword means thatCommandcannot be instantiated directly. It serves as a blueprint for other classes.Now, add the core properties. A command needs a
nameanddescription. It also needs a reference back to theCommandRunnerthat executes it.command_runner/lib/src/arguments.dartdartabstract class Command extends Argument { @override String get name; String get description; bool get requiresArgument => false; late CommandRunner runner; @override String? help; @override String? defaultValue; @override String? valueHelp; }The
runnerproperty is of typeCommandRunner, which you will define later incommand_runner_base.dart. To make Dart aware of this class, you must import its defining file. Add the following import to the top ofcommand_runner/lib/src/arguments.dart:dartimport '../command_runner.dart';Next, to give commands their own set of options, you'll use a private list and expose a read-only, unmodifiable view of it. This uses the
UnmodifiableSetViewclass, which is part of Dart's core collection library. To use it, you must import that library.Update the imports at the top of your file to include
dart:collection:dartimport 'dart:collection'; // New import import '../command_runner.dart';Now, add the
optionslist and getter to yourCommandclass:dartabstract class Command extends Argument { // ... existing properties ... @override String? valueHelp; // Add the following lines to the bottom of your Command class: final List<Option> _options = []; UnmodifiableSetView<Option> get options => UnmodifiableSetView(_options.toSet()); }To make adding options easier, we'll provide two helper methods,
addFlagandaddOption.dartabstract class Command extends Argument { // ... existing properties and getters ... UnmodifiableSetView<Option> get options => UnmodifiableSetView(_options.toSet()); // Add the following lines to the bottom of your Command class: // A flag is an [Option] that's treated as a boolean. void addFlag(String name, {String? help, String? abbr, String? valueHelp}) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: false, valueHelp: valueHelp, type: OptionType.flag, ), ); } // An option is an [Option] that takes a value. void addOption( String name, { String? help, String? abbr, String? defaultValue, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: defaultValue, valueHelp: valueHelp, type: OptionType.option, ), ); } }Finally, every command must have logic to execute. The
runmethod should be flexible, allowing it to return a value either immediately (synchronously) or after a delay (asynchronously). TheFutureOr<Object?>type from Dart'sasynclibrary serves this purpose. This means the method must return a value (of any type ornull) either synchronously or asynchronously. This is your final required import.Update the imports at the top of your file to include
dart:async:dartimport 'dart:async'; // New import import 'dart:collection'; import '../command_runner.dart';Now you can add the abstract
runmethod and provide theusageimplementation to complete theCommandclass.dartabstract class Command extends Argument { // ... existing properties, getters, and methods ... void addOption( String name, { String? help, String? abbr, String? defaultValue, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: defaultValue, valueHelp: valueHelp, type: OptionType.option, ), ); } // Add the following lines to the bottom of your Command class: FutureOr<Object?> run(ArgResults args); @override String get usage { return '$name: $description'; } }run(ArgResults args): This abstract method is where a command's logic resides. Concrete subclasses must implement it.usage: This getter provides a simple usage string, combining the command'snameanddescription.
The
Commandclass now provides a robust foundation for all commands in your CLI app. With the class hierarchy in place, you're ready to defineArgResultsto hold the parsed input. -
Define a class called
ArgResults.command_runner/lib/src/arguments.dartdart// Add this class to the end of the file class ArgResults { Command? command; String? commandArg; Map<Option, Object?> options = {}; // Returns true if the flag exists. bool flag(String name) { // Only check flags, because we're sure that flags are booleans. for (var option in options.keys.where( (option) => option.type == OptionType.flag, )) { if (option.name == name) { return options[option] as bool; } } return false; } bool hasOption(String name) { return options.keys.any((option) => option.name == name); } ({Option option, Object? input}) getOption(String name) { var mapEntry = options.entries.firstWhere( (entry) => entry.key.name == name || entry.key.abbr == name, ); return (option: mapEntry.key, input: mapEntry.value); } }This class represents the results of parsing command-line arguments. It holds the detected command, any argument to that command, and a map of options. It also provides convenient helper methods like
flag,hasOption, andgetOption.Now you have defined the basic structure for handling commands, arguments, and options in your command-line application.
Task 2: Update the CommandRunner class
#Next, update the CommandRunner class to use the new Argument hierarchy.
Open the
command_runner/lib/src/command_runner_base.dartfile.-
Replace the existing
CommandRunnerclass with the following:command_runner/lib/src/command_runner_base.dartdartimport 'dart:collection'; import 'dart:io'; import 'arguments.dart'; class CommandRunner { final Map<String, Command> _commands = <String, Command>{}; UnmodifiableSetView<Command> get commands => UnmodifiableSetView<Command>(<Command>{..._commands.values}); Future<void> run(List<String> input) async { final ArgResults results = parse(input); if (results.command != null) { Object? output = await results.command!.run(results); print(output.toString()); } } void addCommand(Command command) { // TODO: handle error (Command's can't have names that conflict) _commands[command.name] = command; command.runner = this; } ArgResults parse(List<String> input) { var results = ArgResults(); results.command = _commands[input.first]; return results; } // Returns usage for the executable only. // Should be overridden if you aren't using [HelpCommand] // or another means of printing usage. String get usage { final exeFile = Platform.script.path.split('/').last; return 'Usage: dart bin/$exeFile <command> [commandArg?] [...options?]'; } }This updated
CommandRunnerclass now uses theCommandclass from theArgumenthierarchy. It includes methods for adding commands, parsing arguments, and running the appropriate command based on user input. -
Open
command_runner/lib/command_runner.dart, and add the following exports:command_runner/lib/command_runner.dartdart/// Support for doing something awesome. /// /// More dartdocs go here. library; export 'src/arguments.dart'; export 'src/command_runner_base.dart'; export 'src/help_command.dart'; // TODO: Export any libraries intended for clients of this package.This makes the
arguments.dart,command_runner_base.dart, andhelp_command.dartfiles available to other packages that depend oncommand_runner.
Task 3: Create a HelpCommand
#
Create a HelpCommand that extends the Command class and prints usage
information.
Create the file
command_runner/lib/src/help_command.dart.-
Add the following code to
command_runner/lib/src/help_command.dart:command_runner/lib/src/help_command.dartdartimport 'dart:async'; import 'arguments.dart'; // Prints program and argument usage. // // When given a command as an argument, it prints the usage of // that command only, including its options and other details. // When the flag 'verbose' is set, it prints options and details for all commands. // // This command isn't automatically added to CommandRunner instances. // Packages users should add it themselves with [CommandRunner.addCommand], // or create their own command that prints usage. class HelpCommand extends Command { HelpCommand() { addFlag( 'verbose', abbr: 'v', help: 'When true, this command will print each command and its options.', ); addOption( 'command', abbr: 'c', help: "When a command is passed as an argument, prints only that command's verbose usage.", ); } @override String get name => 'help'; @override String get description => 'Prints usage information to the command line.'; @override String? get help => 'Prints this usage information'; @override FutureOr<Object?> run(ArgResults args) async { var usage = runner.usage; for (var command in runner.commands) { usage += '\n ${command.usage}'; } return usage; } }This
HelpCommandclass extendsCommandand implements therunmethod to print usage information. It also uses theaddFlagandaddOptionmethods to define its own options for controlling the output.
Task 4: Update cli.dart to use the new CommandRunner
#
Modify cli/bin/cli.dart to use the new CommandRunner and HelpCommand.
Open the
cli/bin/cli.dartfile.-
Replace the existing code with the following:
cli/bin/cli.dartdartimport 'package:command_runner/command_runner.dart'; const version = '0.0.1'; void main(List<String> arguments) { var commandRunner = CommandRunner()..addCommand(HelpCommand()); commandRunner.run(arguments); }This code creates a
CommandRunnerinstance, adds theHelpCommandto it using method cascading (..addCommand), and then runs the command runner with the command-line arguments.
Task 5: Run the application
#Test the new CommandRunner and HelpCommand.
Open your terminal and navigate to the
clidirectory.-
Run the command
dart run bin/cli.dart help.You should see the usage information printed to the console:
bashUsage: dart bin/cli.dart <command> [commandArg?] [...options?] help: Prints usage information to the command line.This confirms that the
CommandRunnerandHelpCommandare working correctly.
Review
#In this lesson, you learned about:
- Defining
abstractclasses to create a type hierarchy. - Implementing inheritance using the
extendskeyword. - Defining and using
enumtypes to represent a fixed set of values. - Building a basic command-line argument parsing framework using OOP principles.
Quiz
#
Question 1: In the Option class, what is the purpose of the @override
notation?
- A) To create a new method that doesn't exist in the parent class.
- B) To indicate that a method is optional.
- C) To provide a specific implementation for a method or property defined in a parent class.
- D) To make a property private.
Question 2: What is the difference between an abstract class and a regular
class in Dart?
- A) An
abstractclass cannot have any methods. - B) An
abstractclass cannot be instantiated directly. - C) An
abstractclass can only have private methods. - D) There is no difference between an
abstractclass and a regular class.
Question 3: In the guide, what was the enum OptionType used for?
- A) To define the different types of commands.
- B) To represent a fixed set of option types:
flagandoption. - C) To store the default values for options.
- D) To control the color of the command-line output.
Next lesson
#
In the next lesson, you'll learn how to handle errors and exceptions in your
Dart code. You'll create a custom exception class and add error handling to your
CommandRunner to make your application more robust.
Unless stated otherwise, the documentation on this site reflects Dart 3.9.2. Page last updated on 2025-9-16. View source or report an issue.