Serialization
Save and load your node flow graphs
Serialization
Vyuh Node Flow provides built-in support for serializing and deserializing graphs to/from JSON, making it easy to save and load workflows.
Basic Serialization
Export to JSON
// Get the graph as JSON
final json = controller.graph.toJson();
// Convert to JSON string
final jsonString = jsonEncode(json);
// Save to file, database, etc.
await saveToFile(jsonString);Import from JSON
// Load JSON string
final jsonString = await loadFromFile();
// Parse JSON
final json = jsonDecode(jsonString);
// Load into graph
controller.graph.fromJson(json);Complete Example
class FlowEditorWithSaveLoad extends StatefulWidget {
@override
State<FlowEditorWithSaveLoad> createState() =>
_FlowEditorWithSaveLoadState();
}
class _FlowEditorWithSaveLoadState
extends State<FlowEditorWithSaveLoad> {
late final NodeFlowController<MyNodeData> controller;
@override
void initState() {
super.initState();
controller = NodeFlowController<MyNodeData>();
}
Future<void> _saveGraph() async {
try {
// Serialize graph
final json = controller.graph.toJson();
final jsonString = jsonEncode(json);
// Save to SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('saved_graph', jsonString);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Graph saved successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error saving graph: $e')),
);
}
}
Future<void> _loadGraph() async {
try {
// Load from SharedPreferences
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString('saved_graph');
if (jsonString == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('No saved graph found')),
);
return;
}
// Deserialize
final json = jsonDecode(jsonString);
controller.graph.fromJson(json);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Graph loaded successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading graph: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flow Editor'),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: _saveGraph,
tooltip: 'Save',
),
IconButton(
icon: Icon(Icons.folder_open),
onPressed: _loadGraph,
tooltip: 'Load',
),
],
),
body: NodeFlowEditor<MyNodeData>(
controller: controller,
nodeBuilder: (context, node) => MyNodeWidget(node: node),
enablePanning: true,
enableZooming: true,
),
);
}
}Custom NodeData Serialization
Your NodeData class must implement toJson() and fromJson():
class WorkflowNodeData extends NodeData {
final String title;
final String description;
final Map<String, dynamic> config;
WorkflowNodeData({
required this.title,
this.description = '',
this.config = const {},
});
@override
Map<String, dynamic> toJson() {
return {
'title': title,
'description': description,
'config': config,
};
}
@override
void fromJson(Map<String, dynamic> json) {
// Note: This method is called on an already-constructed instance
// to update its values. For immutable data, you may need to
// use a factory constructor instead.
}
// Factory constructor for creating from JSON
factory WorkflowNodeData.fromJson(Map<String, dynamic> json) {
return WorkflowNodeData(
title: json['title'] as String,
description: json['description'] as String? ?? '',
config: json['config'] as Map<String, dynamic>? ?? {},
);
}
}File-Based Serialization
Save to File
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<void> saveGraphToFile(String filename) async {
try {
// Get documents directory
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename.json');
// Serialize graph
final json = controller.graph.toJson();
final jsonString = jsonEncode(json);
// Write to file
await file.writeAsString(jsonString);
} catch (e) {
print('Error saving: $e');
}
}Load from File
Future<void> loadGraphFromFile(String filename) async {
try {
// Get documents directory
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename.json');
// Check if file exists
if (!await file.exists()) {
throw Exception('File not found');
}
// Read from file
final jsonString = await file.readAsString();
// Deserialize
final json = jsonDecode(jsonString);
controller.graph.fromJson(json);
} catch (e) {
print('Error loading: $e');
}
}Export/Import with Metadata
Add metadata to your saved graphs:
class GraphDocument {
final String id;
final String name;
final DateTime createdAt;
final DateTime modifiedAt;
final Map<String, dynamic> graphData;
final Map<String, dynamic> metadata;
GraphDocument({
required this.id,
required this.name,
required this.createdAt,
required this.modifiedAt,
required this.graphData,
this.metadata = const {},
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'createdAt': createdAt.toIso8601String(),
'modifiedAt': modifiedAt.toIso8601String(),
'graphData': graphData,
'metadata': metadata,
};
}
factory GraphDocument.fromJson(Map<String, dynamic> json) {
return GraphDocument(
id: json['id'] as String,
name: json['name'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
modifiedAt: DateTime.parse(json['modifiedAt'] as String),
graphData: json['graphData'] as Map<String, dynamic>,
metadata: json['metadata'] as Map<String, dynamic>? ?? {},
);
}
}
// Save with metadata
Future<void> saveDocument(String name) async {
final document = GraphDocument(
id: Uuid().v4(),
name: name,
createdAt: DateTime.now(),
modifiedAt: DateTime.now(),
graphData: controller.graph.toJson(),
metadata: {
'version': '1.0',
'author': 'user@example.com',
'nodeCount': controller.graph.nodes.length,
},
);
final jsonString = jsonEncode(document.toJson());
await saveToFile(jsonString);
}Cloud Storage Integration
Firebase Example
import 'package:cloud_firestore/cloud_firestore.dart';
Future<void> saveToFirebase(String userId, String graphName) async {
try {
final graphData = controller.graph.toJson();
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('graphs')
.doc(graphName)
.set({
'data': graphData,
'updatedAt': FieldValue.serverTimestamp(),
});
} catch (e) {
print('Error saving to Firebase: $e');
}
}
Future<void> loadFromFirebase(String userId, String graphName) async {
try {
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('graphs')
.doc(graphName)
.get();
if (doc.exists) {
final graphData = doc.data()!['data'] as Map<String, dynamic>;
controller.graph.fromJson(graphData);
}
} catch (e) {
print('Error loading from Firebase: $e');
}
}Versioning
Handle different graph versions:
class VersionedGraph {
static const currentVersion = 2;
final int version;
final Map<String, dynamic> data;
VersionedGraph({
required this.version,
required this.data,
});
Map<String, dynamic> toJson() => {
'version': version,
'data': data,
};
factory VersionedGraph.fromJson(Map<String, dynamic> json) {
final version = json['version'] as int? ?? 1;
var data = json['data'] as Map<String, dynamic>;
// Migrate from old versions
if (version < currentVersion) {
data = _migrateFromVersion(version, data);
}
return VersionedGraph(
version: currentVersion,
data: data,
);
}
static Map<String, dynamic> _migrateFromVersion(
int fromVersion,
Map<String, dynamic> data,
) {
var migratedData = Map<String, dynamic>.from(data);
// Migrate v1 to v2
if (fromVersion == 1) {
// Add new fields, rename old ones, etc.
migratedData = _migrateV1ToV2(migratedData);
}
return migratedData;
}
static Map<String, dynamic> _migrateV1ToV2(Map<String, dynamic> data) {
// Migration logic
return data;
}
}Best Practices
- Version Your Data: Include version numbers in serialized data
- Validate: Validate loaded data before applying to graph
- Error Handling: Wrap serialize/deserialize in try-catch
- Backup: Keep backups before loading new data
- Compression: Consider compressing large graphs
- Encryption: Encrypt sensitive graph data
- Testing: Test save/load with various graph configurations