Architecture
The Vyuh Dashboard Editor follows a layered architecture with a reactive MobX-based engine at its core, a command pattern for all mutations, and a pluggable extension system for cross-cutting concerns.
System Overview
Layered Architecture
DashboardEngine
The DashboardEngine is the core state kernel. It owns the reactive layout, executes commands, and emits events to extensions. It has no knowledge of UI, persistence, or history -- those are handled by extensions.
class DashboardEngine implements DashboardEngineApi {
// Reactive layout state
late final Observable<DashboardLayout> layoutObservable;
DashboardLayout get layout => layoutObservable.value;
// Extension management
void addExtension(DashboardExtension extension);
void removeExtension(String id);
E? getExtension<E extends DashboardExtension>();
// Command execution
Future<void> executeCommand(DashboardCommand command);
Future<void> applyUndo(DashboardCommand command);
Future<void> applyRedo(DashboardCommand command);
// Direct layout replacement (for persistence loads)
void replaceLayout(DashboardLayout newLayout);
// Event emission
void emitEvent(DashboardEvent event);
// Batch operations
void batch(String reason, void Function() operations);
}Command Execution Flow
When the engine executes a command, it follows this precise sequence:
All mutations happen inside a MobX runInAction, which ensures that UI rebuilds are batched. The engine emits two events per command execution: DashboardLayoutChanged (with previous and current layout) and DashboardCommandExecuted (with the command itself).
Engine API
The engine exposes a high-level mutation API that creates and executes commands internally:
| Method | Command Created | Description |
|---|---|---|
addRow() | AddRowCommand | Add a row with a single empty column |
removeRow(rowId) | RemoveRowCommand | Remove a row and all its columns |
reorderRow(old, new) | ReorderRowsCommand | Move a row to a new position |
splitRow(rowId, n) | SplitRowCommand | Replace columns with n equal columns |
addColumn(rowId) | AddColumnCommand | Add an empty column to a row |
removeColumn(rowId, colId) | RemoveColumnCommand | Remove a column from a row |
reorderColumns(rowId, old, new) | ReorderColumnsCommand | Reorder columns within a row |
updateColumnFlex(...) | UpdateColumnFlexCommand | Change a single column's flex |
resizeAdjacentColumns(...) | ResizeAdjacentColumnsCommand | Resize two adjacent columns together |
updateRowFlex(...) | UpdateRowFlexCommand | Update flex for all columns in a row |
updateRowTitle(...) | UpdateRowTitleCommand | Change or clear row title |
updateRowHeight(...) | UpdateRowHeightCommand | Set row height in pixels |
updateRowViewportFraction(...) | UpdateRowViewportFractionCommand | Set viewport-relative row height |
addComponent(...) | AddComponentCommand | Place a component in a column |
removeComponent(...) | RemoveComponentCommand | Clear a column's component |
replaceComponent(...) | ReplaceComponentCommand | Replace one component with another |
swapComponents(...) | SwapComponentsCommand | Swap components between two columns |
duplicateComponent(...) | DuplicateComponentCommand | Copy a component to the nearest empty slot |
updateComponentConfig(...) | UpdateComponentConfigCommand | Update a component's properties |
updateLayoutMetadata(...) | UpdateDashboardMetadataCommand | Patch dashboard-level metadata |
Command Pattern
Every mutation is a DashboardCommand that implements two methods:
abstract class DashboardCommand {
DashboardLayout execute(); // Apply the mutation
DashboardLayout undo(); // Reverse the mutation
String get description; // Human-readable label
}Commands are pure functions from layout to layout. They capture the current layout in their constructor and return a new layout from execute() and undo(). This makes them trivially testable:
final command = AddRowCommand(
layout: currentLayout,
rowToAdd: newRow,
position: 0,
);
final afterExecute = command.execute();
expect(afterExecute.rows.length, currentLayout.rows.length + 1);
final afterUndo = command.undo();
expect(afterUndo.rows.length, currentLayout.rows.length);Command Categories
The 20 built-in commands fall into four categories:
Row commands: AddRowCommand, RemoveRowCommand, ReorderRowsCommand, SplitRowCommand, UpdateRowTitleCommand, UpdateRowHeightCommand, UpdateRowViewportFractionCommand
Column commands: AddColumnCommand, RemoveColumnCommand, ReorderColumnsCommand, UpdateColumnFlexCommand, ResizeAdjacentColumnsCommand, UpdateRowFlexCommand
Component commands: AddComponentCommand, RemoveComponentCommand, ReplaceComponentCommand, SwapComponentsCommand, DuplicateComponentCommand, UpdateComponentConfigCommand
Dashboard commands: UpdateDashboardMetadataCommand
CommandHistory
The CommandHistory class manages the undo/redo stack:
class CommandHistory {
final int maxHistorySize; // Default: 50
void execute(DashboardCommand command); // Add to stack
DashboardCommand? undo(); // Move back
DashboardCommand? redo(); // Move forward
void clear();
bool get canUndo;
bool get canRedo;
int get historySize;
int get currentPosition;
}When a new command is executed after an undo, the redo stack is discarded (standard undo/redo behavior). The history has a configurable maximum size (default 50) to prevent unbounded memory growth.
Event System
The engine communicates with extensions through structured events:
abstract class DashboardEvent {
const DashboardEvent();
}
// Emitted on every layout change
class DashboardLayoutChanged extends DashboardEvent {
final DashboardLayout previous;
final DashboardLayout current;
}
// Emitted when a command executes
class DashboardCommandExecuted extends DashboardEvent {
final DashboardCommand command;
}
// Emitted when a command is undone
class DashboardCommandUndone extends DashboardEvent {
final DashboardCommand command;
}
// Emitted when a command is redone
class DashboardCommandRedone extends DashboardEvent {
final DashboardCommand command;
}
// Batch boundaries
class DashboardBatchStarted extends DashboardEvent {
final String reason;
}
class DashboardBatchEnded extends DashboardEvent {}Events are dispatched synchronously to all registered extensions in registration order. This means extensions can rely on seeing events in a consistent sequence.
Extension System
Extensions augment the engine without modifying its core. Each extension implements the DashboardExtension interface:
abstract class DashboardExtension {
String get id; // Unique identifier
void attach(DashboardEngineApi engine); // Called when added
void detach(); // Called when removed
void onEvent(DashboardEvent event); // React to events
}The DashboardEngineApi is a forward declaration that provides extensions with access to the layout, command execution, and event emission without creating circular dependencies.
Built-in Extensions
| Extension | ID | Purpose |
|---|---|---|
DashboardHistoryExtension | dashboard-history | Undo/redo via CommandHistory |
DashboardPersistenceExtension | dashboard-persistence | Layout JSON serialization, direct layout load, dirty tracking |
DashboardSelectionExtension | dashboard-selection | Cell, row, and component selection state |
DashboardAnalyticsExtension | dashboard-analytics | Track component additions and editor actions |
Extension Event Handling
Each extension listens for specific events:
- History listens for
DashboardCommandExecutedto record commands in the stack. - Persistence listens for
DashboardCommandExecuted,DashboardCommandUndone, andDashboardCommandRedoneto set the dirty flag. - Selection listens for
DashboardLayoutChangedto validate that selected rows/columns still exist. - Analytics listens for
DashboardCommandExecutedto track component additions.
DashboardEditorController
The DashboardEditorController is a facade that composes the engine with all four extensions and provides the complete editing API:
class DashboardEditorController {
final DashboardEngine engine;
// Extensions
late final DashboardHistoryExtension history;
late final DashboardPersistenceExtension persistence;
late final DashboardSelectionExtension selection;
late final DashboardAnalyticsExtension analytics;
// Observable state
Observable<DashboardLayout> get layout;
Observable<bool> get isDirty;
Observable<String?> get selectedCellRowId;
Observable<String?> get selectedCellColumnId;
// Layout mutations (delegates to engine)
Future<String> addRow({int? position});
Future<void> removeRow(String rowId);
Future<void> splitRow(String rowId, int numColumns);
// ... all engine methods
// Selection
void selectCell(String rowId, String columnId);
void clearSelection();
void navigateCell({int dx, int dy});
// Undo/redo
Future<void> undo();
Future<void> redo();
// Persistence / serialization
Map<String, dynamic> toJson();
void fromJson(Map<String, dynamic> json);
void loadLayout(DashboardLayout newLayout);
void markClean();
void markDirty();
}The controller also manages editor-specific draft state for the config panel, ensuring property edits are committed back to the layout through UpdateComponentConfigCommand rather than mutating the component instance in place.
Next Steps
- Layout Model -- Understand the row/column/component hierarchy
- Component System -- Master the ComponentDescriptor pattern
- Extensions Guide -- Build custom extensions