Generated Data Classes
Understanding Drift's generated data classes and companions
What is it?
Generated Data Classes are the typed Dart classes that Drift automatically creates from your table definitions. These include the data class itself (representing a row), the companion class (for inserts and updates), and various helper classes. They provide type-safe access to your database rows and eliminate manual mapping code.
Think of Generated Data Classes like "automatic formatters" – they take raw database rows and convert them into beautiful, type-safe Dart objects that are easy to work with.
// 👇 Your table definition
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get email => text().unique()();
IntColumn get age => integer().nullable()();
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
}
// 👇 Drift generates these classes
// 1️⃣ Data class (represents a row)
class User {
final int id;
final String name;
final String email;
final int? age;
final bool isActive;
User({
required this.id,
required this.name,
required this.email,
this.age,
required this.isActive,
});
}
// 2️⃣ Companion class (for inserts/updates)
class UsersCompanion extends UpdateCompanion<User> {
final Value<int> id;
final Value<String> name;
final Value<String> email;
final Value<int?> age;
final Value<bool> isActive;
const UsersCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.email = const Value.absent(),
this.age = const Value.absent(),
this.isActive = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
required String name,
required String email,
this.age = const Value.absent(),
this.isActive = const Value.absent(),
}) : super.insert();
}
// 3️⃣ Table manager (provides query builder)
class $UsersTable extends TableManager {
// ... query methods
}
What's happening here? - Data Class – Typed representation of a database row - Companion Class – For inserts and updates with
Value<T>- Table Manager – Query builder and CRUD operations - Type Safety – All operations are fully typed
Why does it exist?
- Type Safety – No manual casting or mapping
- Code Generation – Zero boilerplate to write
- IDE Support – Autocomplete and refactoring
- Error Prevention – Catch errors at compile-time
- Consistency – Generated code follows patterns
- Productivity – Focus on business logic, not mapping
Data Classes
The main generated class representing a row
Standard Data Class
// lib/database/tables/users.dart
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get email => text().unique()();
IntColumn get age => integer().nullable()();
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Generated User class (simplified)
class User {
final int id;
final String name;
final String email;
final int? age;
final bool isActive;
final DateTime createdAt;
User({
required this.id,
required this.name,
required this.email,
this.age,
required this.isActive,
required this.createdAt,
});
// 👇 Copy with modifications
User copyWith({
int? id,
String? name,
String? email,
int? age,
bool? isActive,
DateTime? createdAt,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
);
}
// 👇 To JSON
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'age': age,
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
};
// 👇 From JSON
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
age: json['age'] as int?,
isActive: json['isActive'] as bool,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
Key insights: - All fields are final – Immutable by default - Required fields – Non-nullable for NOT NULL columns - Optional fields – Nullable for columns with
.nullable()-copyWithmethod – Create modified copies -toJson/fromJson– Serialization support
Data Class with Custom Row Class
Using your own data class with
@UseRowClass
// 👇 Your custom data class
@UseRowClass
class AppUser {
final int id;
final String name;
final String email;
final int? age;
final bool isActive;
final DateTime createdAt;
AppUser({
required this.id,
required this.name,
required this.email,
this.age,
required this.isActive,
required this.createdAt,
});
// 👇 Optional: Custom mapping
factory AppUser.fromRow(Row row) {
return AppUser(
id: row.read('id'),
name: row.read('name'),
email: row.read('email'),
age: row.readOrNull('age'),
isActive: row.read('is_active') == 1,
createdAt: row.read('created_at'),
);
}
}
// 👇 Table definition with custom row class
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get email => text().unique()();
IntColumn get age => integer().nullable()();
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Type get rowClass => AppUser; // 👈 Tell Drift to use AppUser
}
// Now queries return AppUser instead of User
Future<List<AppUser>> getUsers() async {
return await select(users).get(); // 👈 Returns List<AppUser>
}
Data Class with Custom Methods
// 👇 Custom data class with business logic
@UseRowClass
class AppUser {
final int id;
final String name;
final String email;
final int? age;
final bool isActive;
final DateTime createdAt;
AppUser({
required this.id,
required this.name,
required this.email,
this.age,
required this.isActive,
required this.createdAt,
});
// 👇 Business logic methods
bool get isAdult => age != null && age! >= 18;
bool get isTeenager => age != null && age! >= 13 && age! < 18;
bool get isChild => age != null && age! < 13;
String get displayName => name.isNotEmpty ? name : email.split('@').first;
String get initials {
final parts = name.split(' ');
if (parts.length >= 2) {
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
}
return name.isNotEmpty ? name[0].toUpperCase() : '?';
}
// 👇 Computed properties
Duration get accountAge => DateTime.now().difference(createdAt);
String get accountAgeText {
final days = accountAge.inDays;
if (days < 30) return '${days} days';
if (days < 365) return '${days ~/ 30} months';
return '${days ~/ 365} years';
}
// 👇 Copy with modifications
AppUser copyWith({
int? id,
String? name,
String? email,
int? age,
bool? isActive,
DateTime? createdAt,
}) {
return AppUser(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
);
}
}
// Usage in your app
final user = AppUser(
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 25,
isActive: true,
createdAt: DateTime.now().subtract(Duration(days: 100)),
);
print(user.isAdult); // true
print(user.initials); // JD
print(user.accountAgeText); // 3 months
Companion Classes
For inserts and updates with
Value<T>
Standard Companion
// Generated UsersCompanion (simplified)
class UsersCompanion extends UpdateCompanion<User> {
// 👇 All columns as Value<T>
final Value<int> id;
final Value<String> name;
final Value<String> email;
final Value<int?> age;
final Value<bool> isActive;
final Value<DateTime> createdAt;
// 👇 Constructor with all absent
const UsersCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.email = const Value.absent(),
this.age = const Value.absent(),
this.isActive = const Value.absent(),
this.createdAt = const Value.absent(),
});
// 👇 Constructor for inserts
UsersCompanion.insert({
this.id = const Value.absent(),
required String name,
required String email,
this.age = const Value.absent(),
this.isActive = const Value.absent(),
this.createdAt = const Value.absent(),
}) : super.insert();
}
Using Companions
// 1️⃣ Insert with required fields (uses defaults)
await into(users).insert(
UsersCompanion.insert(
name: 'John Doe',
email: 'john@example.com',
// isActive: uses default (true)
// createdAt: uses default (current time)
),
);
// 2️⃣ Insert with all fields
await into(users).insert(
UsersCompanion.insert(
name: 'Jane Smith',
email: 'jane@example.com',
age: Value(28),
isActive: Value(true),
// createdAt: uses default
),
);
// 3️⃣ Insert with null for optional field
await into(users).insert(
UsersCompanion.insert(
name: 'Bob Wilson',
email: 'bob@example.com',
age: const Value(null), // 👈 Explicitly insert NULL
),
);
// 4️⃣ Update specific fields
await (update(users)..where((u) => u.id.equals(1)))
.write(UsersCompanion(
name: Value('John Updated'),
// Other fields remain unchanged
));
// 5️⃣ Update with multiple fields
await (update(users)..where((u) => u.id.equals(1)))
.write(UsersCompanion(
name: Value('John Updated'),
age: Value(26),
isActive: Value(false),
));
Value Explained
Understanding Drift's Value wrapper
What is Value?
// 👇 Value<T> has three states
// 1️⃣ Absent (use default)
const Value.absent() // 👈 Don't update this field
// 2️⃣ Present (explicit value)
Value('John') // 👈 Set to this value
Value(25) // 👈 Set to this value
Value(true) // 👈 Set to this value
// 3️⃣ Explicit null
const Value(null) // 👈 Set to NULL (only for nullable columns)
Using Value
class UsersCompanion extends UpdateCompanion<User> {
final Value<String> name;
final Value<int?> age;
final Value<bool> isActive;
const UsersCompanion({
this.name = const Value.absent(),
this.age = const Value.absent(),
this.isActive = const Value.absent(),
});
}
// 👇 Different use cases
// 1️⃣ Don't update (keep existing)
UsersCompanion(
// name = absent
age: const Value.absent(),
)
// 2️⃣ Update with value
UsersCompanion(
name: Value('New Name'),
age: const Value.absent(),
)
// 3️⃣ Set to NULL (only nullable columns)
UsersCompanion(
age: const Value(null), // age is int? (nullable)
)
// 4️⃣ Update with null check
int? newAge = getAgeFromUser();
UsersCompanion(
age: newAge != null ? Value(newAge) : const Value.absent(),
)
// 5️⃣ Conditional update
UsersCompanion(
name: shouldUpdateName ? Value(newName) : const Value.absent(),
age: shouldUpdateAge ? Value(newAge) : const Value.absent(),
)
Real-World Example
Complete data classes for e-commerce application
// lib/database/tables/users.dart
import 'package:drift/drift.dart';
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get username => text().unique()();
TextColumn get email => text().unique()();
TextColumn get fullName => text().nullable().named('full_name')();
TextColumn get passwordHash => text().named('password_hash')();
IntColumn get age => integer().nullable()();
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
BoolColumn get isVerified => boolean().withDefault(const Constant(false))();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get lastLogin => dateTime().nullable()();
}
// lib/database/data_classes/app_user.dart
import 'package:drift/drift.dart';
@UseRowClass
class AppUser {
final int id;
final String username;
final String email;
final String? fullName;
final String passwordHash;
final int? age;
final bool isActive;
final bool isVerified;
final bool isAdmin;
final DateTime createdAt;
final DateTime? updatedAt;
final DateTime? lastLogin;
AppUser({
required this.id,
required this.username,
required this.email,
this.fullName,
required this.passwordHash,
this.age,
required this.isActive,
required this.isVerified,
required this.isAdmin,
required this.createdAt,
this.updatedAt,
this.lastLogin,
});
// 👇 Display name
String get displayName => fullName ?? username;
// 👇 Initials
String get initials {
if (fullName != null && fullName!.isNotEmpty) {
final parts = fullName!.split(' ');
if (parts.length >= 2) {
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
}
return fullName![0].toUpperCase();
}
return username[0].toUpperCase();
}
// 👇 Age checks
bool get isAdult => age != null && age! >= 18;
bool get isTeenager => age != null && age! >= 13 && age! < 18;
bool get isChild => age != null && age! < 13;
// 👇 Account age
Duration get accountAge => DateTime.now().difference(createdAt);
String get accountAgeText {
final days = accountAge.inDays;
if (days < 1) return 'Today';
if (days < 7) return '$days days';
if (days < 30) return '${days ~/ 7} weeks';
if (days < 365) return '${days ~/ 30} months';
return '${days ~/ 365} years';
}
// 👇 Status
bool get isOnline => lastLogin != null &&
DateTime.now().difference(lastLogin!).inMinutes < 5;
bool get canLogin => isActive && isVerified;
// 👇 Copy with
AppUser copyWith({
int? id,
String? username,
String? email,
String? fullName,
String? passwordHash,
int? age,
bool? isActive,
bool? isVerified,
bool? isAdmin,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? lastLogin,
}) {
return AppUser(
id: id ?? this.id,
username: username ?? this.username,
email: email ?? this.email,
fullName: fullName ?? this.fullName,
passwordHash: passwordHash ?? this.passwordHash,
age: age ?? this.age,
isActive: isActive ?? this.isActive,
isVerified: isVerified ?? this.isVerified,
isAdmin: isAdmin ?? this.isAdmin,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
lastLogin: lastLogin ?? this.lastLogin,
);
}
// 👇 JSON
Map<String, dynamic> toJson() => {
'id': id,
'username': username,
'email': email,
'fullName': fullName,
'age': age,
'isActive': isActive,
'isVerified': isVerified,
'isAdmin': isAdmin,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
'lastLogin': lastLogin?.toIso8601String(),
};
factory AppUser.fromJson(Map<String, dynamic> json) => AppUser(
id: json['id'] as int,
username: json['username'] as String,
email: json['email'] as String,
fullName: json['fullName'] as String?,
passwordHash: json['passwordHash'] as String,
age: json['age'] as int?,
isActive: json['isActive'] as bool,
isVerified: json['isVerified'] as bool,
isAdmin: json['isAdmin'] as bool,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
lastLogin: json['lastLogin'] != null
? DateTime.parse(json['lastLogin'] as String)
: null,
);
}
// lib/database/tables/products.dart
class Products extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get sku => text().unique()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
RealColumn get price => real().customConstraint('CHECK (price >= 0)')();
IntColumn get stock => integer().withDefault(const Constant(0))();
BoolColumn get isActive => boolean().withDefault(const Constant(true))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().nullable()();
}
// lib/database/data_classes/app_product.dart
import 'package:drift/drift.dart';
@UseRowClass
class AppProduct {
final int id;
final String sku;
final String name;
final String? description;
final double price;
final int stock;
final bool isActive;
final DateTime createdAt;
final DateTime? updatedAt;
AppProduct({
required this.id,
required this.sku,
required this.name,
this.description,
required this.price,
required this.stock,
required this.isActive,
required this.createdAt,
this.updatedAt,
});
// 👇 Computed properties
bool get isInStock => stock > 0;
bool get isLowStock => stock > 0 && stock < 5;
bool get isOutOfStock => stock == 0;
String get stockStatus {
if (isOutOfStock) return 'Out of Stock';
if (isLowStock) return 'Low Stock';
return 'In Stock';
}
String get priceFormatted => '\$${price.toStringAsFixed(2)}';
String get skuShort => sku.length > 8 ? '...${sku.substring(sku.length - 5)}' : sku;
// 👇 Business logic
bool canOrder(int quantity) => stock >= quantity && isActive;
AppProduct copyWith({
int? id,
String? sku,
String? name,
String? description,
double? price,
int? stock,
bool? isActive,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return AppProduct(
id: id ?? this.id,
sku: sku ?? this.sku,
name: name ?? this.name,
description: description ?? this.description,
price: price ?? this.price,
stock: stock ?? this.stock,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'sku': sku,
'name': name,
'description': description,
'price': price,
'stock': stock,
'isActive': isActive,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
factory AppProduct.fromJson(Map<String, dynamic> json) => AppProduct(
id: json['id'] as int,
sku: json['sku'] as String,
name: json['name'] as String,
description: json['description'] as String?,
price: json['price'] as double,
stock: json['stock'] as int,
isActive: json['isActive'] as bool,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
// lib/database/database.dart - Usage
@DriftDatabase(tables: [Users, Products])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
// 👇 Using custom data classes
Future<List<AppUser>> getActiveUsers() async {
return await (select(users)
..where((u) => u.isActive.equals(true)))
.get();
}
Future<AppUser?> getUserById(int id) async {
return await (select(users)..where((u) => u.id.equals(id)))
.getSingleOrNull();
}
// 👇 Using companions
Future<int> createUser({
required String username,
required String email,
required String passwordHash,
String? fullName,
int? age,
}) async {
return await into(users).insert(
UsersCompanion.insert(
username: username,
email: email,
passwordHash: passwordHash,
fullName: Value(fullName),
age: Value(age),
// isActive: true (default)
// isVerified: false (default)
// isAdmin: false (default)
),
);
}
Future<void> updateUserProfile(int userId, {
String? fullName,
int? age,
bool? isActive,
}) async {
await (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
fullName: fullName != null ? Value(fullName) : const Value.absent(),
age: age != null ? Value(age) : const Value.absent(),
isActive: isActive != null ? Value(isActive) : const Value.absent(),
updatedAt: Value(DateTime.now()),
));
}
// 👇 Using data class methods
Future<void> verifyUser(int userId) async {
final user = await getUserById(userId);
if (user != null && !user.isVerified) {
await (update(users)..where((u) => u.id.equals(userId)))
.write(UsersCompanion(
isVerified: const Value(true),
updatedAt: Value(DateTime.now()),
));
}
}
// 👇 Using data class in UI
String getUserDisplayName(int userId) {
final user = await getUserById(userId);
return user?.displayName ?? 'Unknown User';
}
bool canUserLogin(int userId) {
final user = await getUserById(userId);
return user?.canLogin ?? false;
}
}
// lib/ui/user_profile.dart
class UserProfile extends StatelessWidget {
final AppUser user;
const UserProfile({required this.user});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text(user.initials),
),
title: Text(user.displayName),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.email),
Text('Age: ${user.age ?? 'Not specified'}'),
Text('Account: ${user.accountAgeText}'),
Text('Status: ${user.isActive ? 'Active' : 'Inactive'}'),
],
),
trailing: Icon(
user.isActive ? Icons.check_circle : Icons.cancel,
color: user.isActive ? Colors.green : Colors.red,
),
),
);
}
}
Best Practices
- Use custom data classes – For business logic methods
- Add computed properties – To data classes
- Use
copyWithmethod – For immutability - Use
Value.absent()– To skip fields in updates - Use
Value(null)– To set nullable fields to NULL - Keep companions for inserts/updates – Don't modify manually
- Add helper methods – For common operations
- Document data classes – Explain business logic
- Test data class methods – Verify logic works
Common Mistakes
Mistake 1: Trying to manually create companions
Wrong:
// 🚫 Manually creating companion (won't work)
class UsersCompanion extends UpdateCompanion<User> {
final String name; // Wrong type
}
Correct:
// ✅ Let Drift generate companions
// Don't manually define them
Mistake 2: Using Value incorrectly
Wrong:
// 🚫 Trying to use raw value
await update(users).write(UsersCompanion(
name: 'John', // ❌ Should be Value
));
Correct:
// ✅ Use Value wrapper
await update(users).write(UsersCompanion(
name: Value('John'),
));
Mistake 3: Modifying generated data classes
Wrong:
// 🚫 Editing generated file
// users.g.dart
class User {
// Adding custom method
String get displayName => name; // Will be overwritten
}
Correct:
// ✅ Use custom data class
@UseRowClass
class AppUser {
// Your custom code here
}
Summary
| Class Type | Purpose | Key Feature |
|---|---|---|
| Data Class | Represents a row | Type-safe, immutable |
| Companion | Inserts/updates | Value<T> wrapper |
| Table Manager | Query builder | CRUD operations |
| Custom Row Class | Business logic | Custom methods |
Next Steps
Now you understand generated data classes, let's dive deeper:
- Companions – Deep dive into companions
- Value
– Understanding Value wrapper - Insertable – Custom insert logic
Did You Know?
-
Data classes are fully typed – No manual casting needed
-
copyWithcreates new instances – Immutability pattern -
Companions use
Value<T>– For optional fields -
Value.absent()means "skip" – Not "set to null" -
Value(null)means "set to NULL" – For nullable columns -
Custom data classes need
@UseRowClass– Annotation -
Generated data classes are final – Cannot be extended
-
Table managers are generated – For each table