Dart: Dart Classes and Objects — Core of Object-Oriented
Last updated: 2026-08-26
A class is a blueprint for objects — a good blueprint makes good objects, and good objects build good systems.
1. What You Will Learn
- Class definition, declaration of properties and methods
- Constructors: Default / Named / Factory / Redirecting / Constant
- Inheritance and method overriding (
@override) - Abstract classes and interfaces (implicit interface)
- Bob's scenario: Order / Product / Customer model classes in DataPipeline
2. A Developer's Real Story
(1) Pain Point: Dispersed Logic Due to Chaotic Data Models
In the early days of Bob's DataPipeline, all data was handled using Maps, with no type safety. order['amout'] (a typo) returned null at runtime instead of throwing an error, causing the amount statistics for 100,000 orders to be completely zero. Worse still, the logic for creating orders was scattered across 5 files, with no unified validation.
(2) Solution with Classes
Using classes to define data models, constructors to centralize validation logic, and the type system to catch typos and type mismatches at compile time.
class Order {
final String id;
final double amount;
final DateTime date;
Order({required this.id, required this.amount, required this.date})
: assert(amount > 0, 'Amount must be positive');
double calcTax(double rate) => amount * rate;
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
(3) Benefits
- Type errors moved from runtime to compile time, reducing bugs by 80%.
- Constructors provide unified data validation, eliminating missed checks.
- Inheritance and interfaces increased code reuse rate by 60%.
3. Class Definition Basics
(1) Structure of a Class
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Basic Class Definition
class Product {
// Fields
final String name;
final double price;
String category;
// Constructor
Product({required this.name, required this.price, this.category = 'General'});
// Method
String formatPrice() => '\$${price.toStringAsFixed(2)} USD';
// Getter
bool get isExpensive => price >= 1000;
// Override toString
@override
String toString() => 'Product($name, ${formatPrice()}, $category)';
}
void main() {
final product = Product(name: 'Laptop', price: 1299.99, category: 'Electronics');
print(product); // Product(Laptop, $1299.99 USD, Electronics)
print(product.isExpensive); // true
print(product.formatPrice()); // $1299.99 USD
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
| Class Member | Declaration Syntax | Description |
|---|---|---|
| Field | Type name |
Instance variable |
| Constructor | ClassName() |
Creates an object |
| Method | returnType name() {} |
Instance method |
| Getter | Type get name => expr |
Computed property |
| Setter | set name(Type value) |
Assignment interception |
4. Five Types of Constructors
(1) DataPipeline Domain Model
classDiagram
class Order {
+String id
+double amount
+DateTime date
+calcTax(double rate) double
}
class Product {
+String name
+double price
+formatUSD() String
}
class Customer {
+String name
+List~Order~ orders
}
Customer "1" --> "*" Order : has
Order "1" --> "*" Product : contains
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Default Constructor and Named Constructors
class Order {
final String id;
final double amount;
final DateTime date;
String status;
// Default constructor
Order({required this.id, required this.amount, required this.date, this.status = 'pending'});
// Named constructor - create from CSV row
Order.fromCsv(String csvLine)
: id = csvLine.split(',')[0],
amount = double.parse(csvLine.split(',')[1]),
date = DateTime.parse(csvLine.split(',')[2]),
status = 'pending';
// Named constructor - create with default date
Order.now({required this.id, required this.amount})
: date = DateTime.now(),
status = 'pending';
double calcTax(double rate) => amount * rate;
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status)';
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Factory Constructor
class DataSource {
final String type;
final String connection;
// Private constructor
DataSource._internal(this.type, this.connection);
// Factory constructor - returns cached or custom instance
factory DataSource.create({required String type, required String connection}) {
return DataSource._internal(type, connection);
}
// Factory with default configurations
factory DataSource.api(String endpoint) =>
DataSource._internal('api', endpoint);
factory DataSource.file(String path) =>
DataSource._internal('file', path);
factory DataSource.database(String connectionString) =>
DataSource._internal('database', connectionString);
}
void main() {
final api = DataSource.api('https://api.example.com/orders');
final file = DataSource.file('/data/orders.csv');
print('${api.type}: ${api.connection}');
print('${file.type}: ${file.connection}');
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Redirecting Constructor
class Report {
final String title;
final String format;
final int recordCount;
// Main constructor
Report({required this.title, this.format = 'json', this.recordCount = 0});
// Redirecting constructors
Report.json(String title, int count) : this(title: title, format: 'json', recordCount: count);
Report.csv(String title, int count) : this(title: title, format: 'csv', recordCount: count);
Report.html(String title, int count) : this(title: title, format: 'html', recordCount: count);
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Constant Constructor
class Config {
final String appName;
final int maxRecords;
// const constructor - all fields must be final
const Config({this.appName = 'DataPipeline', this.maxRecords = 1000000});
}
void main() {
const config1 = Config(); // Compile-time constant
const config2 = Config(maxRecords: 500000);
print(identical(config1, config2)); // false (different values)
print(config1.maxRecords); // 1000000
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
| Constructor Type | Syntax | Features | Scenarios |
|---|---|---|---|
| Default | ClassName() |
Auto-generated or customized | Standard creation |
| Named | ClassName.name() |
Multiple creation methods | fromCsv, now |
| Factory | factory ClassName() |
Can return subclasses or cached instances | Singleton, caching |
| Redirecting | : this() |
Delegates to the main constructor | Convenient creation |
| Constant | const ClassName() |
Created at compile time, immutable | Configuration constants |
5. Inheritance and Method Overriding
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Inheritance Hierarchy
// Base class
class DataProcessor {
final String name;
DataProcessor(this.name);
// Method to be overridden
String process(String input) => 'Processing $input with $name';
// Non-overridable method
String getStatus() => 'Ready';
@override
String toString() => '$name Processor';
}
// Subclass
class OrderProcessor extends DataProcessor {
final double taxRate;
OrderProcessor(this.taxRate) : super('Order');
@override
String process(String input) {
final amount = double.tryParse(input) ?? 0;
final taxed = amount * (1 + taxRate);
return 'Order processed: \$${taxed.toStringAsFixed(2)} USD';
}
}
// Another subclass
class ProductProcessor extends DataProcessor {
ProductProcessor() : super('Product');
@override
String process(String input) => 'Product cataloged: $input';
}
void main() {
final orderProc = OrderProcessor(0.08);
print(orderProc.process('1500')); // Order processed: $1620.00 USD
print(orderProc.getStatus()); // Ready
final productProc = ProductProcessor();
print(productProc.process('Laptop')); // Product cataloged: Laptop
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
6. Abstract Classes and Interfaces
(1) Abstract Class
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Abstract Class and Implementation
// Abstract class - cannot be instantiated
abstract class ReportGenerator {
final String format;
ReportGenerator(this.format);
// Abstract method - must be implemented
String generate(List<Map<String, dynamic>> data);
// Concrete method - shared implementation
String header(String title) => '=== $title ($format) ===';
}
class JsonReportGenerator extends ReportGenerator {
JsonReportGenerator() : super('json');
@override
String generate(List<Map<String, dynamic>> data) {
final buffer = StringBuffer();
buffer.writeln(header('DataPipeline Report'));
for (final entry in data) {
buffer.writeln(' ${entry['id']}: ${entry['amount']}');
}
return buffer.toString();
}
}
class CsvReportGenerator extends ReportGenerator {
CsvReportGenerator() : super('csv');
@override
String generate(List<Map<String, dynamic>> data) {
final lines = ``<String>``['id,amount'];
for (final entry in data) {
lines.add('${entry['id']},${entry['amount']}');
}
return lines.join('\n');
}
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
(2) Implicit Interface
Every class in Dart implicitly defines an interface, containing all its instance methods and fields. Other classes can implements this interface.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
: Implementing an Implicit Interface
class FileDataSource {
final String path;
FileDataSource(this.path);
List``<String>`` readLines() => ['line1', 'line2'];
bool get exists => true;
}
// Implement the implicit interface of FileDataSource
class MockDataSource implements FileDataSource {
@override
String path = '/mock/data.csv';
@override
List``<String>`` readLines() => ['mock1', 'mock2', 'mock3'];
@override
bool get exists => true;
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
| Aspect | abstract class | interface (implements) | mixin |
|---|---|---|---|
| Instantiation | No | — | No |
| Method Implementation | Optional | Must re-implement all | Optional |
| Multiple Inheritance | Single inheritance | Can implement multiple | Can mix in multiple |
| Constructor | Yes | No | No |
7. Complete Example: DataPipeline Domain Model
// ============================================
// DataPipeline Domain Model
// Order, Product, Customer with full OOP
// ============================================
class Product {
final String id;
final String name;
final double price;
final String category;
const Product({
required this.id,
required this.name,
required this.price,
this.category = 'General',
});
String formatUSD() => '\$${price.toStringAsFixed(2)} USD';
@override
String toString() => 'Product($name, ${formatUSD()})';
}
class Order {
final String id;
final double amount;
final DateTime date;
String status;
final List``<Product>`` products;
Order({
required this.id,
required this.amount,
required this.date,
this.status = 'pending',
this.products = const [],
}) : assert(amount > 0, 'Amount must be positive');
Order.now({required this.id, required this.amount, this.products = const []})
: date = DateTime.now(),
status = 'pending';
double calcTax(double rate) => amount * rate;
double calcTotal(double taxRate) => amount + calcTax(taxRate);
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status, ${products.length} items)';
}
class Customer {
final String name;
final String email;
final List``<Order>`` orders;
Customer({required this.name, required this.email, List``<Order>``? orders})
: orders = orders ?? [];
double get totalSpent => orders.fold(0.0, (sum, o) => sum + o.amount);
int get orderCount => orders.length;
double get averageOrderValue => orderCount > 0 ? totalSpent / orderCount : 0;
void addOrder(Order order) => orders.add(order);
String tier() => switch (totalSpent) {
>= 50000 => 'Enterprise',
>= 10000 => 'Premium',
>= 1000 => 'Standard',
_ => 'Free',
};
@override
String toString() => 'Customer($name, ${tier()}, ${orderCount} orders, \$${totalSpent.toStringAsFixed(2)} total)';
}
void main() {
final laptop = Product(id: 'P001', name: 'Laptop', price: 1299.99, category: 'Electronics');
final mouse = Product(id: 'P002', name: 'Mouse', price: 29.99, category: 'Electronics');
final alice = Customer(name: 'Alice', email: 'alice@example.com');
final bob = Customer(name: 'Bob', email: 'bob@example.com');
alice.addOrder(Order(id: 'ORD-001', amount: 1500.0, date: DateTime(2024, 1, 15), products: [laptop]));
alice.addOrder(Order(id: 'ORD-002', amount: 3200.0, date: DateTime(2024, 2, 20)));
bob.addOrder(Order(id: 'ORD-003', amount: 890.0, date: DateTime(2024, 3, 10), products: [mouse]));
print('=== DataPipeline Customers ===');
print(alice);
print(bob);
print('\n--- Alice Orders ---');
for (final order in alice.orders) {
print(' $order (tax: \$${order.calcTax(0.08).toStringAsFixed(2)} USD)');
}
print('\nTier Summary:');
print(' Alice: ${alice.tier()} (\$${alice.totalSpent.toStringAsFixed(2)} total)');
print(' Bob: ${bob.tier()} (\$${bob.totalSpent.toStringAsFixed(2)} total)');
}
> **Output:** Run locally in DartPad or via `dart run`. All Dart course examples are based on Dart 3.x / Flutter 3.x; results may vary slightly with different SDK versions.
Output:
=== DataPipeline Customers ===
Customer(Alice, Premium, 2 orders, $4700.00 total)
Customer(Bob, Standard, 1 orders, $890.00 total)
--- Alice Orders ---
Order(ORD-001, $1500.00, pending, 1 items) (tax: $120.00 USD)
Order(ORD-002, $3200.00, pending, 0 items) (tax: $256.00 USD)
Tier Summary:
Alice: Premium ($4700.00 total)
Bob: Standard ($890.00 total)
❓ FAQ
Q: Does Dart have multiple inheritance? A: No. Dart only supports single inheritance (
extends), but you can implement multiple interfaces withimplements, or mix in multiple mixins withwith.
Q: What's the difference between a factory constructor and a regular constructor? A: A regular constructor always creates a new instance. A factory constructor can return a cached instance, a subclass instance, or even execute complex logic before deciding what to return. It's declared with the
factorykeyword.
Q: What is a
constconstructor for? A: Aconstconstructor creates a compile-time constant object.constconstructors with the same arguments will return the same (identical) instance. It's suitable for immutable objects like configuration classes or enum values.
Q: How to choose between
abstract classandinterface? A: Useabstract classwithextendswhen you need to share implementation code. Useinterface(viaimplements) when you only need to define a contract. Dart has no dedicatedinterfacekeyword; every class itself is an interface.
Q: When should I use
@override? A: You should add@overridewhen overriding a parent class method. Although not strictly required, adding it allows the compiler to help you check if you are correctly overriding (it will report an error if the parent class doesn't have that method).
Q: Is Dart's
asserteffective in production? A: No.assertis only executed in debug mode and is ignored in release mode. It's used for development-time validation and cannot replace proper parameter validation.
Q: Can a named constructor be a factory? A: Yes. For example,
factory Order.fromCsv(String line)is valid. It can return a default instance instead of throwing an exception if parsing fails.
📖 Summary
- Class definition, including fields, constructors, methods, getters/setters, is the core of data models.
- The five types of constructors have different uses: named (multiple creation methods), factory (caching/subclasses), redirecting (convenience), constant (immutability).
- Single inheritance + multiple interface implementations + multiple mixin compositions allow flexible combination.
- Abstract classes provide shared implementation, while implicit interfaces define contracts.
- DataPipeline's Order/Product/Customer form the domain model, supporting business logic.
📝 Exercises
- Basic (Difficulty ⭐): Define a
Productclass withname,price,categoryfields. Add aformatUSD()method and anisExpensivegetter. Create 3 product instances and print them. - Intermediate (Difficulty ⭐⭐): Add a named constructor
Order.fromMap(create from a Map) and a factory constructorOrder.safe(return a default order if parsing fails) to theOrderclass. Test edge cases. - Challenge (Difficulty ⭐⭐⭐): Design an abstract
ReportGeneratorclass. Create two subclasses,JsonReportandCsvReport, to implement polymorphic report generation. Then create aMockReportthatimplementsthe same interface for testing.