Overview
Icarus uses Hive as its local NoSQL database for storing strategies, folders, and all associated metadata. Hive provides fast, key-value storage with support for custom type adapters.
Hive Boxes
The application uses two primary Hive boxes defined in lib/const/hive_boxes.dart:
Location: lib/const/hive_boxes.dart:1-5
class HiveBoxNames {
static const strategiesBox = "strategy_box";
static const foldersBox = "folder_box";
}
Strategy Box
Stores all strategy documents. Each strategy is keyed by its unique ID and contains multiple pages with placed elements.
Key type: String (strategy UUID)
Value type: StrategyData
Folder Box
Stores the folder hierarchy for organizing strategies. Supports nested folders through parent/child relationships.
Key type: String (folder UUID)
Value type: Folder
Type Adapters
Hive requires type adapters for custom classes. Icarus uses code generation to automatically create adapters for all stored types.
Location: lib/hive/hive_adapters.dart:21-44
@GenerateAdapters([
AdapterSpec<StrategyData>(),
AdapterSpec<PlacedWidget>(),
AdapterSpec<PlacedAgent>(),
AdapterSpec<PlacedAbility>(),
AdapterSpec<PlacedText>(),
AdapterSpec<PlacedImage>(),
AdapterSpec<MapValue>(),
AdapterSpec<AgentType>(),
AdapterSpec<Offset>(),
AdapterSpec<FreeDrawing>(),
AdapterSpec<Line>(),
AdapterSpec<BoundingBox>(),
AdapterSpec<StrategySettings>(),
AdapterSpec<PlacedUtility>(),
AdapterSpec<UtilityType>(),
AdapterSpec<Folder>(),
AdapterSpec<IconData>(),
AdapterSpec<FolderColor>(),
AdapterSpec<StrategyPage>(),
AdapterSpec<LineUp>(),
AdapterSpec<SimpleImageData>(),
AdapterSpec<AgentState>(),
])
part 'hive_adapters.g.dart';
After modifying any Hive-stored classes, you must regenerate adapters using:
fvm flutter pub run build_runner build --delete-conflicting-outputs
Data Models
StrategyData
The root document representing a complete strategy with metadata and pages.
Location: lib/providers/strategy_provider.dart:40-142
class StrategyData extends HiveObject {
final String id;
String name;
final int versionNumber;
final List<StrategyPage> pages;
final MapValue mapData;
final DateTime lastEdited;
final DateTime createdAt;
String? folderID;
StrategyData({
required this.id,
required this.name,
required this.mapData,
required this.versionNumber,
required this.lastEdited,
required this.folderID,
this.pages = const [],
DateTime? createdAt,
}) : createdAt = createdAt ?? lastEdited;
}
Unique identifier (UUID v4)
User-visible strategy name
Schema version for migration purposes (current: 38)
All pages in the strategy document
The Valorant map this strategy is designed for
Timestamp of the most recent modification
Timestamp when the strategy was first created
ID of the parent folder, or null for root-level strategies
StrategyData includes deprecated legacy fields (drawingData, agentData, etc.) that are maintained for backward compatibility but no longer used. All new data is stored in the pages list.
StrategyPage
A single page within a strategy, containing all placed elements for that page.
Location: lib/providers/strategy_page.dart:17-44
class StrategyPage extends HiveObject {
final String id;
final int sortIndex;
final String name;
final List<DrawingElement> drawingData;
final List<PlacedAgent> agentData;
final List<PlacedAbility> abilityData;
final List<PlacedText> textData;
final List<PlacedImage> imageData;
final List<PlacedUtility> utilityData;
final bool isAttack;
final List<LineUp> lineUps;
final StrategySettings settings;
StrategyPage({
required this.id,
required this.name,
required this.drawingData,
required this.agentData,
required this.abilityData,
required this.textData,
required this.imageData,
required this.utilityData,
required this.sortIndex,
required this.isAttack,
required this.settings,
this.lineUps = const [],
});
}
Zero-based index determining page order (used for navigation)
Whether this page shows attacking or defending positions
Page-specific display settings (agent size, ability size, etc.)
Folder
Organizational container for strategies, supporting hierarchical nesting.
Location: lib/providers/folder_provider.dart:22-39
class Folder extends HiveObject {
String name;
final String id;
final DateTime dateCreated;
String? parentID;
IconData icon;
FolderColor color;
Color? customColor;
Folder({
required this.name,
required this.id,
required this.dateCreated,
required this.icon,
this.color = FolderColor.red,
this.parentID,
this.customColor,
});
bool get isRoot => parentID == null;
}
ID of parent folder. Null indicates a root-level folder.
Material icon to display for this folder
Preset color theme (red, blue, green, orange, purple, generic, custom)
Custom color value (only used when color = FolderColor.custom)
File System Storage
While metadata is stored in Hive, binary assets (images) are stored in the file system.
Directory Structure
Each strategy gets its own directory under the application support directory:
<ApplicationSupportDirectory>/
└── <strategyID>/
└── images/
├── <imageID1>.png
├── <imageID2>.jpg
└── ...
Location: lib/providers/strategy_provider.dart:241-256
Future<Directory> setStorageDirectory(String strategyID) async {
final directory = await getApplicationSupportDirectory();
final customDirectory = Directory(path.join(directory.path, strategyID));
if (!await customDirectory.exists()) {
await customDirectory.create(recursive: true);
}
return customDirectory;
}
Image Management
Images are stored as files but referenced in Hive through PlacedImage objects that contain:
- Image ID (filename)
- Position on canvas
- Size and rotation
- Display settings
The PlacedImageProvider handles loading images from disk and managing unused image cleanup.
CRUD Operations
Creating a Strategy
Location: lib/providers/strategy_provider.dart:984-1019
Future<String> createNewStrategy(String name) async {
final newID = const Uuid().v4();
final pageID = const Uuid().v4();
final newStrategy = StrategyData(
mapData: MapValue.ascent,
versionNumber: Settings.versionNumber,
id: newID,
name: name,
pages: [
StrategyPage(
id: pageID,
name: "Page 1",
drawingData: [],
agentData: [],
abilityData: [],
textData: [],
imageData: [],
utilityData: [],
lineUps: [],
sortIndex: 0,
isAttack: true,
settings: StrategySettings(),
)
],
lastEdited: DateTime.now(),
folderID: ref.read(folderProvider),
);
await Hive.box<StrategyData>(HiveBoxNames.strategiesBox)
.put(newStrategy.id, newStrategy);
return newStrategy.id;
}
Reading a Strategy
final box = Hive.box<StrategyData>(HiveBoxNames.strategiesBox);
final strategy = box.get(strategyID);
Hive boxes are synchronous for reads, making strategy retrieval instant.
Updating a Strategy
Since StrategyData extends HiveObject, you can call .save() on any modified instance:
Location: lib/providers/strategy_provider.dart:1177-1187
Future<void> renameStrategy(String strategyID, String newName) async {
final strategyBox = Hive.box<StrategyData>(HiveBoxNames.strategiesBox);
final strategy = strategyBox.get(strategyID);
if (strategy != null) {
strategy.name = newName;
await strategy.save();
}
}
Alternatively, use box.put(key, value) to replace the entire document:
final updated = strategy.copyWith(name: newName, lastEdited: DateTime.now());
await box.put(strategy.id, updated);
Deleting a Strategy
Location: lib/providers/strategy_provider.dart:1216-1226
Future<void> deleteStrategy(String strategyID) async {
await Hive.box<StrategyData>(HiveBoxNames.strategiesBox).delete(strategyID);
final directory = await getApplicationSupportDirectory();
final customDirectory = Directory(path.join(directory.path, strategyID));
if (!await customDirectory.exists()) return;
await customDirectory.delete(recursive: true);
}
Deletion removes both the Hive entry and the entire file system directory for that strategy.
Querying Strategies
Get All Strategies
final box = Hive.box<StrategyData>(HiveBoxNames.strategiesBox);
final allStrategies = box.values.toList();
Filter by Folder
final strategiesInFolder = box.values
.where((strategy) => strategy.folderID == folderID)
.toList();
Sort by Last Edited
final sortedStrategies = box.values.toList()
..sort((a, b) => b.lastEdited.compareTo(a.lastEdited));
Data Integrity
Automatic Cleanup
When loading a strategy, unused images are automatically deleted to prevent storage bloat:
Location: lib/providers/strategy_provider.dart:730-743
List<String> allImageIds = [];
for (final page in newStrat.pages) {
allImageIds.addAll(page.imageData.map((image) => image.id));
for (final lineUp in page.lineUps) {
allImageIds.addAll(lineUp.images.map((image) => image.id));
}
}
await ref
.read(placedImageProvider.notifier)
.deleteUnusedImages(newStrat.id, allImageIds);
Version Tracking
Every strategy stores a versionNumber field that matches the app version when it was last saved. This enables safe schema migrations (see Migrations).
Current version: Settings.versionNumber = 38
Lazy Box vs. Regular Box
Icarus uses regular Hive boxes (not lazy boxes) because:
- Strategies are small enough to keep in memory
- Synchronous access simplifies the provider architecture
- The entire box loads on app startup for instant navigation
Deep Copying
When switching pages or duplicating strategies, Icarus performs deep copies to avoid shared references:
Location: lib/providers/strategy_page.dart:46-83
StrategyPage copyWith({...}) {
return StrategyPage(
drawingData: DrawingProvider.fromJson(
DrawingProvider.objectToJson(drawingData ?? this.drawingData)),
agentData: AgentProvider.fromJson(AgentProvider.objectToJson(
agentData ?? this.agentData,
)),
// ... deep copy all lists
);
}
This prevents modifications on one page from affecting another.
Image storage is disabled on the web platform (kIsWeb == true). All image-related operations are skipped to avoid file system access issues.
if (!kIsWeb) {
imageData = await PlacedImageProvider.fromJson(
jsonString: jsonEncode(json['imageData']), strategyID: strategyID);
}