Skip to main content

Define classes and objects

Learn about object-oriented programming in Dart, including classes, constructors, getters, and enums. Build data models for command-line arguments.

This chapter covers the fundamentals of object-oriented programming (OOP) in Dart. Explore how to create classes, define constructors and fields, compute properties with gettersGetterA special method that provides read access to an object's properties. Learn more, and use enums to represent fixed sets of values.

What you'll accomplish

Define classes to model data and behavior
Create constructors with initializing formals and named parameters
Use getters to compute properties dynamically
Use enums to represent fixed sets of values

Prerequisites

#

Before starting this chapter:

  • Complete Chapter 4 and have a working Dart development environment with the dartpedia project.
  • Understand basic programming concepts, such as variables, functions, and control flow.
  • Understand packages and libraries in Dart.

Classes and objects in Dart

#

Classes bundle two main components together:

  • State (fields): The data each object holds, such as an option's name or help text.
  • Behavior (methods and getters): The actions an object can perform or properties it computes, such as formatting a usage string.

Instead of tracking loose strings or maps across an application, modeling command-line arguments as classes ensures compile-time type safety, editor autocomplete, and a consistent data structure.

Tasks

#

A command-line interface (CLI) is defined by the commands, options, and arguments typed into a terminal.

This lesson and the next build a framework capable of parsing a command such as:

bash
$ dartpedia help --verbose --command=search

Each part serves a distinct purpose:

  • dartpedia: The executable, or the name of the application.
  • help: A command, representing an action to perform.
  • --verbose: A flag (a boolean option without a value), modifying the command's behavior.
  • --command=search: An option that accepts a value (search).

This chapter builds the data models representing options and parsed results.

Task 1: Define the OptionType enum

#

Command-line options fall into two distinct categories: flags (which evaluate to boolean true or false) and regular options (which accept a string value). Using an enum ensures that code only allows these two valid categories.

  1. Create command_runner/lib/src/arguments.dart.

  2. Define the OptionType enum:

    command_runner/lib/src/arguments.dart
    dart
    enum OptionType { flag, option }
    

    This enumeration defines two members: flag and option. Enums provide compile-time type safety, ensuring code accepts only valid option types.

Task 2: Define the Option class

#

Classes in Dart define blueprints for objects, combining state (fields) and behavior (methods and getters). The Option class models command-line options such as --verbose or --command=search.

  1. Start by defining a minimal Option class with essential fields and a constructor. Add the following code to command_runner/lib/src/arguments.dart below the enum:

    command_runner/lib/src/arguments.dart
    dart
    class Option {
      Option(this.name, {required this.type});
    
      final String name;
      final OptionType type;
    }
    
    • Fields (final String name;, final OptionType type;): Variables that store the object's data. Declaring them as final ensures they cannot change after creation.
    • Constructor (Option(...)): Instantiates new Option objects. The this.name syntax is an initializing formal—a Dart shortcut that assigns the argument directly to the instance field before the constructor body runs.
    • Named parameters ({required this.type}): Parameters inside curly braces {} are passed by name (for example, Option('verbose', type: OptionType.flag)). The required keyword makes the parameter mandatory.
  2. Now, expand the Option class with optional metadata fields and a usage getter:

    command_runner/lib/src/arguments.dart
    dart
    class Option {
      Option(
        this.name, {
        required this.type,
        this.help,
        this.abbr,
        this.defaultValue,
        this.valueHelp,
      });
    
      final String name;
      final OptionType type;
      final String? help;
      final String? abbr;
      final Object? defaultValue;
      final String? valueHelp;
    
      String get usage {
        if (abbr != null) {
          return '-$abbr,--$name: $help';
        }
    
        return '--$name: $help';
      }
    }
    
    • Nullable types (String?, Object?): The question mark ? indicates that a field is optional and can hold null. When callers omit help or abbr, they default to null.
    • Getter (get usage): A getter computes a value on demand when accessed, formatting the help string based on whether an abbreviation exists.

Task 3: Define the ArgResults class

#

The ArgResults class stores the output of parsing command-line input. It maps each Option to its user-supplied value.

  1. Add ArgResults to the bottom of command_runner/lib/src/arguments.dart:

    command_runner/lib/src/arguments.dart
    dart
    class ArgResults {
      String? command;
      String? commandArg;
      Map<Option, Object?> options = {};
    
      // Returns true if the flag exists and is true.
      bool flag(String name) {
        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);
      }
    }
    

    Highlights from the preceding code:

    • options map (Map<Option, Object?>): Associates each Option instance with its parsed user input.
    • where() method in flag(): Filters map keys to inspect only boolean flags (option.type == OptionType.flag), ignoring options that take string arguments.
    • Type cast (as bool): Tells the type checker to treat the value as a bool because flags always store boolean values.
    • Record return type (({Option option, Object? input})): Returns a lightweight, named record grouping both the Option and its value without declaring a separate class.

    The ArgResults class serves as the data contract for parsed output. Chapter 6 connects this class to the full CommandRunner.

Task 4: Export arguments from the package

#

Export arguments.dart from the package entry point to allow other packages to import Option and ArgResults.

  1. Open command_runner/lib/command_runner.dart.

  2. Export arguments.dart alongside command_runner_base.dart:

    command_runner/lib/command_runner.dart
    dart
    /// Support for command-line parsing and execution.
    library;
    
    export 'src/arguments.dart';
    export 'src/command_runner_base.dart';
    

    This export statement makes declarations in arguments.dart accessible to any package that imports package:command_runner/command_runner.dart.

Task 5: Test the classes in cli.dart

#

Verify that Option instantiates and computes its usage string correctly.

  1. Open cli/bin/cli.dart.

  2. Replace the file contents with the following test code:

    cli/bin/cli.dart
    dart
    import 'package:command_runner/command_runner.dart';
    
    void main() {
      final verboseOption = Option(
        'verbose',
        type: OptionType.flag,
        abbr: 'v',
        help: 'Display extra logging information.',
      );
    
      print('Defined option: ${verboseOption.name}');
      print('Usage: ${verboseOption.usage}');
    }
    

    This code calls the Option constructor to create an instance, and then prints its name field and computed usage getter.

  3. Run the application from the cli directory:

    bash
    dart run bin/cli.dart
    

    The console outputs:

    bash
    Defined option: verbose
    Usage: -v,--verbose: Display extra logging information.
    

    This confirms that the Option class and getter function as expected.

Review

#

What you accomplished

A summary of the concepts and code introduced in this lesson.
Defined classes to model data and behavior

Created the Option and ArgResults classes to structure command-line arguments and parsed output.

Created constructors with initializing formals and named parameters

Used this.fieldName syntax for automatic field initialization and defined optional and required named parameters with {} and required.

Used getters to compute properties dynamically

Implemented the usage getter to format option help text on demand without storing duplicate string state.

Used enums to represent fixed sets of values

Defined the OptionType enum to represent valid option types (flag and option), ensuring compile-time safety.

Quiz

#

Check your understanding

1 / 3
In the constructor Option(this.name, {required this.type}), what is the purpose of the this.name syntax?
  1. It automatically assigns the constructor argument directly to the instance field name.

    That's right!

    In Dart, this.fieldName in a constructor's parameter list is an "initializing formal" that assigns the argument value directly to the field before the constructor body runs.

  2. It creates a new global variable named name.

    Not quite.

    this.name refers specifically to the instance field of the class, not a global variable.

  3. It makes the name property private to the library.

    Not quite.

    Privacy in Dart is designated by a leading underscore (_name), not by this.

  4. It marks the parameter as optional.

    Not quite.

    Optional parameters are enclosed in square brackets [] or curly braces {}. Positional this.name is required.

What is the primary benefit of using a getter like String get usage instead of storing a String usage field?
  1. It dynamically computes the value whenever accessed, ensuring it reflects current state without redundant storage.

    That's right!

    Getters behave like properties from the caller's perspective, but execute code on demand to calculate their value.

  2. Getters can only be accessed from within the same file.

    Not quite.

    Getters have the same visibility rules as normal fields and methods.

  3. Getters run faster than reading a stored field.

    Not quite.

    Reading a stored field is typically faster than executing logic in a getter.

  4. Getters allow the property to be modified from outside the class.

    Not quite.

    A getter provides read-only access unless a corresponding setter (set usage(...)) is also defined.

When should you use an enum instead of a class or a set of constants?
  1. When you need a type that can only be one of a fixed, known set of values.

    That's right!

    Enums are perfect when you have a closed set of options, like flag versus option, days of the week, or status codes. The compiler ensures you handle all cases.

  2. When you need to store user input that could be any string.

    Not quite.

    User input is unpredictable and could be anything. Enums work best when you can list every possible value in advance.

  3. When you want to create multiple instances with different properties.

    Not quite.

    That's what classes are for. Each enum value is a singleton, there's only one instance for each value.

  4. When you need to perform arithmetic operations on the values.

    Not quite.

    Use int or double for arithmetic. Enums represent distinct categories, not numeric values (though they do have an index).

Next lesson

#

The next chapter covers sharing behavior between classes using inheritance and abstract classes. Define an abstract CliElement base class, and build out the Command and CommandRunner architecture.