Goal: Finish Models & Code Generation to make models safe & immutable
Estimated Time: 8-12 hours
Difficulty: Medium
Impact: High (developer experience, type safety)
Sprint 2 requires migrating from manual JSON serialization to code-generated, immutable models using freezed and hive_generator.
Add these packages:
dependencies:
# Add these
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0
dev_dependencies:
# Add these
freezed: ^2.5.7
json_serializable: ^6.8.0
hive_generator: ^2.0.1flutter pub getEnable code generation:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"Current: lib/models/issue_item.dart (manual JSON)
New:
// lib/models/issue_item.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'item.dart';
part 'issue_item.freezed.dart';
part 'issue_item.g.dart';
@freezed
class IssueItem with _$IssueItem {
const factory IssueItem({
required String id,
required String title,
int? number,
String? bodyMarkdown,
String? projectColumnName,
String? projectItemNodeId,
DateTime? createdAt,
String? assigneeAvatarUrl,
required ItemStatus status,
DateTime? updatedAt,
String? assigneeLogin,
@Default([]) List<String> labels,
@Default([]) List<Item> children,
@Default(false) bool isExpanded,
@Default(false) bool isLocalOnly,
DateTime? localUpdatedAt,
}) = _IssueItem;
factory IssueItem.fromJson(Map<String, dynamic> json) =>
_$IssueItemFromJson(json);
}Benefits:
- ✅ Immutable (can't accidentally modify)
- ✅
copyWith()auto-generated - ✅ Type-safe JSON serialization
- ✅ No manual
fromJson/toJsonbugs
// lib/models/item.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'item.freezed.dart';
part 'item.g.dart';
enum ItemStatus { open, closed }
@freezed
class Item with _$Item {
const factory Item({
required String id,
required String title,
required ItemStatus status,
DateTime? updatedAt,
String? assigneeLogin,
@Default([]) List<String> labels,
@Default([]) List<Item> children,
@Default(false) bool isExpanded,
@Default(false) bool isLocalOnly,
DateTime? localUpdatedAt,
}) = _Item;
factory Item.fromJson(Map<String, dynamic> json) => _$ItemFromJson(json);
}// lib/models/repo_item.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'item.dart';
part 'repo_item.freezed.dart';
part 'repo_item.g.dart';
@freezed
class RepoItem with _$RepoItem {
const factory RepoItem({
required String id,
required String name,
required String fullName,
String? description,
bool? private,
String? htmlUrl,
DateTime? updatedAt,
@Default([]) List<Item> children,
@Default(false) bool isExpanded,
}) = _RepoItem;
factory RepoItem.fromJson(Map<String, dynamic> json) =>
_$RepoItemFromJson(json);
}// lib/models/project_item.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'project_item.freezed.dart';
part 'project_item.g.dart';
@freezed
class ProjectItem with _$ProjectItem {
const factory ProjectItem({
required String id,
required String title,
String? description,
String? url,
@Default([]) List<String> columnNames,
}) = _ProjectItem;
factory ProjectItem.fromJson(Map<String, dynamic> json) =>
_$ProjectItemFromJson(json);
}flutter pub run build_runner build --delete-conflicting-outputsThis generates:
issue_item.freezed.dart- Immutable copy, copyWith()issue_item.g.dart- JSON serialization- Same for all other models
Update models with @HiveType:
// lib/models/issue_item.dart
import 'package:hive_ce/hive_ce.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'issue_item.freezed.dart';
part 'issue_item.g.dart';
part 'issue_item.hive.g.dart'; // NEW
@HiveType(typeId: 0) // Unique ID for each model
@freezed
class IssueItem with _$IssueItem implements HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String title;
@HiveField(2)
final int? number;
@HiveField(3)
final String? bodyMarkdown;
// ... all other fields with @HiveField(n)
const factory IssueItem({
// ... fields
}) = _IssueItem;
factory IssueItem.fromJson(Map<String, dynamic> json) =>
_$IssueItemFromJson(json);
}// lib/main.dart
import 'package:hive_ce_flutter/hive_ce_flutter.dart';
import 'models/issue_item.dart';
import 'models/item.dart';
import 'models/repo_item.dart';
import 'models/project_item.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
// Register Hive adapters
Hive.registerAdapter(IssueItemAdapter());
Hive.registerAdapter(ItemAdapter());
Hive.registerAdapter(RepoItemAdapter());
Hive.registerAdapter(ProjectItemAdapter());
Hive.registerAdapter(ItemStatusAdapter()); // Enum adapter
// ... rest of initialization
}// lib/models/item_status_adapter.dart
import 'package:hive_ce/hive_ce.dart';
import 'item.dart';
class ItemStatusAdapter extends TypeAdapter<ItemStatus> {
@override
final int typeId = 100; // Unique ID
@override
ItemStatus read(BinaryReader reader) {
return ItemStatus.values[reader.readInt()];
}
@override
void write(BinaryWriter writer, ItemStatus obj) {
writer.writeInt(obj.index);
}
}flutter pub run build_runner build --delete-conflicting-outputsThis generates:
issue_item.hive.g.dart- Hive adapter- Same for all models
Before:
final updated = issue.copyWith(
title: 'New Title',
status: ItemStatus.closed,
);After: (same syntax, but type-safe!)
final updated = issue.copyWith(
title: 'New Title',
status: ItemStatus.closed,
);
// ✅ Compiler catches errors nowBefore:
final issue = IssueItem.fromJson(json);After: (same syntax, but safer)
final issue = IssueItem.fromJson(json);
// ✅ Throws JsonSerializableError if invalidBefore:
final box = await Hive.openBox('issues');
await box.put('issue_1', issue.toJson()); // Store as JSON stringAfter:
final box = await Hive.openBox('issues');
await box.put('issue_1', issue); // Store as object
// ✅ Type-safe, no manual serializationAfter migration, you'll have compilation errors in:
lib/services/github_api_service.dartlib/services/local_storage_service.dartlib/services/sync_service.dartlib/screens/*.dart(all screens using models)
Fix them by:
- Updating
.toJson()calls (now auto-generated) - Updating
.fromJson()calls (now auto-generated) - Updating Hive read/write (now stores objects directly)
flutter testFix any failing tests (likely JSON parsing tests).
- Create issue offline
- Verify saved to Hive
- Reconnect
- Verify syncs to GitHub
- Login works
- Dashboard loads
- Create issue works
- Edit issue works
- Close issue works
- Offline mode works
- Sync works
- All models use
@freezed - All models have
@HiveTypeand@HiveField - No manual
fromJson/toJsonimplementations - All
copyWithcalls use generated method - Hive boxes store objects (not JSON strings)
- No
UnimplementedErrorin fromJson - All tests pass
- App compiles without errors
- Offline mode still works
- Sync still works
lib/models/
├── item.freezed.dart # Generated
├── item.g.dart # Generated
├── item.hive.g.dart # Generated
├── issue_item.freezed.dart # Generated
├── issue_item.g.dart # Generated
├── issue_item.hive.g.dart # Generated
├── repo_item.freezed.dart # Generated
├── repo_item.g.dart # Generated
├── repo_item.hive.g.dart # Generated
├── project_item.freezed.dart # Generated
├── project_item.g.dart # Generated
├── project_item.hive.g.dart # Generated
└── item_status_adapter.dart # Manual (enum adapter)
lib/models/
├── item.dart # Add @freezed, @HiveType
├── issue_item.dart # Add @freezed, @HiveType
├── repo_item.dart # Add @freezed, @HiveType
└── project_item.dart # Add @freezed, @HiveType
lib/main.dart # Register Hive adapters
lib/services/
├── github_api_service.dart # Update JSON usage
├── local_storage_service.dart # Update Hive usage
└── sync_service.dart # Update model usage
lib/screens/
├── *.dart # Update copyWith usage
Error: The typeId 0 has already been used
Solution: Each @HiveType needs a unique typeId:
@HiveType(typeId: 0) class IssueItem {}
@HiveType(typeId: 1) class RepoItem {}
@HiveType(typeId: 2) class ProjectItem {}
@HiveType(typeId: 3) class Item {}Error: Missing @JsonSerializable or wrong imports
Solution: Ensure imports:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'your_model.freezed.dart';
part 'your_model.g.dart';Error: Error: Couldn't resolve constructor
Solution: Register adapter in main.dart:
Hive.registerAdapter(IssueItemAdapter());Error: Bad state: Conflicting outputs
Solution:
flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputsDay 1:
- Add dependencies
- Migrate
Itembase class - Test compilation
Day 2:
- Migrate
IssueItem - Update usage in services
- Test
Day 3:
- Migrate
RepoItemandProjectItem - Update all screens
- Test
Day 1:
- Add
@HiveTypeto all models - Create enum adapters
- Register in main.dart
Day 2:
- Update Hive storage code
- Run code generation
- Fix compilation errors
Day 3:
- Test offline mode
- Test sync
- Run all tests
- Fix any issues
- ✅ Type Safety: Compiler catches JSON errors
- ✅ Immutability: Can't accidentally modify models
- ✅ copyWith: Auto-generated, type-safe
- ✅ Less Boilerplate: No manual fromJson/toJson
- ✅ Fewer Bugs: No manual JSON typos
- ✅ Better Testing: Models are predictable
- ✅ Easier Refactoring: Change model, see all usages
- ✅ Clearer Intent: Immutable = thread-safe
- ✅ Faster Serialization: Generated code is optimized
- ✅ Less Memory: No intermediate JSON strings
- ✅ Type Adapters: Hive reads directly to objects
- Catches bugs at compile-time
- Better developer experience
- More maintainable long-term
- Professional code quality
- 8-12 hours of work
- Risk of breaking existing features
- Can be done incrementally
- Not user-visible improvement
Defer to v1.1.0
Why:
- App works fine with manual JSON
- Not user-visible improvement
- Risk of breaking stable code before release
- Can be done incrementally post-release
Ship v1.0.0 now, add freezed in v1.1.0
# 1. Add dependencies
flutter pub add freezed_annotation json_annotation
flutter pub add --dev freezed json_serializable hive_generator build_runner
# 2. Start with one model (IssueItem)
# Edit lib/models/issue_item.dart (see example above)
# 3. Generate code
flutter pub run build_runner build --delete-conflicting-outputs
# 4. Test
flutter test
# 5. Repeat for other modelsTo finish Sprint 2:
- ✅ Add freezed + json_serializable + hive_generator
- ✅ Migrate all models to @freezed
- ✅ Add @HiveType to all models
- ✅ Register Hive adapters
- ✅ Update all usage sites
- ✅ Test everything
Time: 8-12 hours
Risk: Medium (breaking changes possible)
Recommendation: Do it post-v1.0.0 in v1.1.0
Built with ❤️ using the GitDoIt Agent System