flowy_graph 0.1.2
flowy_graph: ^0.1.2 copied to clipboard
A Flutter package for building interactive node-based editors, flow diagrams, and graph visualizations with ease.
Flowy Graph #
Flowy Graph is a Flutter package for building interactive node-based editors, flow diagrams, and graph visualizations β entirely with Flutter widgets.
It provides a flexible, customizable, and performant API for creating workflows, data pipelines, visual programming tools, and more. Inspired by libraries like React Flow, but built natively for Flutter.
β¨ Features #
- π¦ Customizable Nodes & Edges β build nodes using any Flutter widget
- π Interactive Connections β connect nodes via drag-and-drop ports
- π Infinite Canvas with Pan & Zoom β navigate large diagrams easily
- π¨ Theming & Styling β full control over colors, shapes, and ports
- β‘ Optimized Performance β supports large graphs efficiently
- π§© Extensible API β extend with your own widgets and behaviors
π Installation #
Add the package to your pubspec.yaml
:
dependencies:
flowy_graph: ^0.0.1
Then run:
flutter pub get
Import it in your Dart code:
import 'package:flowy_graph/flowy_graph.dart';
πΈ Examples #
Example 1: Simple Canvas with Custom Nodes #
This example demonstrates a basic canvas with three custom nodes added programmatically via a floating action button. Each node has input and output ports styled with distinct colors for connections.
import 'dart:math';
import 'package:flowy_graph/flowy_graph.dart';
import 'package:flutter/material.dart';
class Example1Page extends StatefulWidget {
const Example1Page({super.key});
@override
State<Example1Page> createState() => _Example1PageState();
}
class _Example1PageState extends State<Example1Page> {
final FlowyNodeController _controller = FlowyNodeController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
updateListener();
super.initState();
}
void onUpdate() {}
void updateListener() {
_controller.addListener(onUpdate);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flowy Graph')),
body: FlowyCanvas(
controller: _controller,
focusNode: _focusNode,
infiniteCanvasSize: 10000,
background: const GridBackground(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.addNode(
exampleNode("MyFirstNode${Random.secure().nextInt(20000000)}"),
NodePosition.custom(const Offset(100, 100)),
);
_controller.addNode(
exampleNode("MyFirstNode2${Random.secure().nextInt(20000000)}"),
NodePosition.custom(const Offset(200, 230)),
);
_controller.addNode(
exampleNode("MyFirstNode8${Random.secure().nextInt(20000000)}"),
NodePosition.custom(const Offset(320, 330)),
);
},
child: const Icon(Icons.add),
),
);
}
NodeWidgetBase exampleNode(String name) {
return ContainerNodeWidget(
name: name,
typeName: 'custom_node',
backgroundColor: Colors.blue.shade800,
radius: 10,
width: 230,
contentPadding: const EdgeInsets.all(8),
selectedBorder: Border.all(color: Colors.white, width: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InPortWidget(
name: 'in1',
onConnect: (node, port) => true,
icon: const Icon(Icons.circle_outlined, color: Colors.yellow),
iconConnected: const Icon(Icons.circle, color: Colors.yellow),
connectionTheme: ConnectionTheme(
color: Colors.yellow,
strokeWidth: 2,
),
multiConnections: true,
),
InPortWidget(
name: 'in2',
onConnect: (node, port) => true,
icon: const Icon(Icons.circle_outlined, color: Colors.yellow),
iconConnected: const Icon(Icons.circle, color: Colors.yellow),
connectionTheme: ConnectionTheme(
color: Colors.yellow,
strokeWidth: 2,
),
multiConnections: true,
),
const Icon(Icons.link),
OutPortWidget(
name: 'out1',
multiConnections: false,
icon: const Icon(Icons.circle_outlined, color: Colors.red),
iconConnected: const Icon(Icons.circle, color: Colors.red),
connectionTheme: ConnectionTheme(color: Colors.red, strokeWidth: 2),
),
],
),
);
}
}
Example 2: Multiple Node Types with Properties #
This example showcases different node types (componentNode
, receiverNode
, binaryNode
,
sinkNode
) with varied properties, such as dropdown menus, checkboxes, and text input fields,
demonstrating the package's flexibility.
import 'dart:math';
import 'package:example/Example2/widgets/node.dart';
import 'package:flowy_graph/flowy_graph.dart';
import 'package:flutter/material.dart';
class Example2Page extends StatefulWidget {
const Example2Page({super.key});
@override
State<Example2Page> createState() => _Example2PageState();
}
class _Example2PageState extends State<Example2Page> {
final FlowyNodeController controller = FlowyNodeController();
final FocusNode _focusNode = FocusNode();
final FocusNode _focusNode2 = FocusNode();
final TextEditingController textEditingController = TextEditingController();
@override
void initState() {
controller.addSelectListener((Connection conn) {
debugPrint("ON SELECT inNode: ${conn.inNode}, inPort: ${conn.inPort}");
});
controller.addNode(componentNode('node_1_1'), NodePosition.afterLast);
controller.addNode(componentNode('node_1_2'), NodePosition.afterLast);
controller.addNode(componentNode('node_1_3'), NodePosition.afterLast);
controller.addNode(
receiverNode('node_2_1', _focusNode2, textEditingController),
NodePosition.afterLast,
);
controller.addNode(binaryNode('node_3_1'), NodePosition.afterLast);
controller.addNode(sinkNode('node_4_1'), NodePosition.afterLast);
super.initState();
}
void _addNewNode() {
var randomId = Random.secure().nextInt(20000000);
controller.addNode(
componentNode("new_node$randomId"),
NodePosition.afterLast,
);
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme
.of(context)
.colorScheme
.inversePrimary,
title: Text("Flowy Graph Example 2"),
actions: [
IconButton(
onPressed: () {
debugPrint('controller.toMap(): ${controller.toJson()}');
},
icon: const Icon(Icons.abc),
),
],
),
body: FlowyCanvas(
focusNode: _focusNode,
controller: controller,
background: const GridBackground(),
infiniteCanvasSize: 10000,
),
floatingActionButton: FloatingActionButton(
onPressed: _addNewNode,
child: const Icon(Icons.add),
),
);
}
}
NodeWidgetBase componentNode(String name) {
return TitleBarNodeWidget(
name: name,
typeName: 'node_1',
backgroundColor: Colors.black87,
radius: 10,
selectedBorder: Border.all(color: Colors.white),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('Output 1'),
OutPortWidget(
name: 'PortOut1',
icon: const Icon(
Icons.play_arrow_outlined,
color: Colors.red,
size: 24,
),
iconConnected: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.red),
),
child: Container(
margin: EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.red,
strokeWidth: 2,
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('Output 2'),
SizedBox(
width: 24,
height: 24,
child: OutPortWidget(
name: 'PortOut2',
icon: const Icon(
Icons.circle_outlined,
color: Colors.yellowAccent,
size: 20,
),
iconConnected: const Icon(
Icons.circle,
color: Colors.yellowAccent,
size: 20,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.yellowAccent,
strokeWidth: 2,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const CheckBoxProperty(name: 'check_port'),
const Text('Output 3'),
OutPortWidget(
name: 'PortOut3',
icon: const Icon(
Icons.play_arrow_outlined,
color: Colors.green,
size: 24,
),
iconConnected: const Icon(
Icons.play_arrow,
color: Colors.green,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.green,
strokeWidth: 2,
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Container(
height: 30,
padding: const EdgeInsets.only(left: 4),
decoration: const BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
child: Builder(
builder: (context) {
return Theme(
data: Theme.of(
context,
).copyWith(canvasColor: Colors.black),
child: DropdownMenuProperty<int>(
underline: const SizedBox(),
name: 'select',
dropdownColor: Colors.white,
style: const TextStyle(color: Colors.white),
items: const [
DropdownMenuItem(
value: 0,
child: Text(
'Item1',
style: TextStyle(color: Colors.black),
),
),
DropdownMenuItem(
value: 1,
child: Text(
'Item2',
style: TextStyle(color: Colors.black),
),
),
DropdownMenuItem(
value: 2,
child: Text(
'Item3',
style: TextStyle(color: Colors.black),
),
),
],
onChanged: (int? v) {},
),
);
},
),
),
),
],
),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('check 1:'),
CheckBoxProperty(name: 'check_prop1'),
],
),
],
),
title: const Text('Components'),
iconTileSpacing: 5,
titleBarPadding: const EdgeInsets.all(4.0),
titleBarGradient: const LinearGradient(
colors: [Color.fromRGBO(0, 23, 135, 1.0), Colors.lightBlue],
),
icon: const Icon(Icons.rectangle_outlined, color: Colors.white),
width: 200,
);
}
NodeWidgetBase receiverNode(String name,
FocusNode focusNode,
TextEditingController controller,) {
return TitleBarNodeWidget(
name: name,
typeName: 'node_2',
backgroundColor: Colors.black87,
radius: 10,
selectedBorder: Border.all(color: Colors.white),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
InPortWidget(
name: 'PortIn1',
onConnect: (String name, String port) => true,
icon: const Icon(
Icons.play_arrow_outlined,
color: Colors.red,
size: 24,
),
iconConnected: const Icon(
Icons.play_arrow,
color: Colors.red,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.red,
strokeWidth: 2,
),
),
const Text('Input 1'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('Output 3'),
OutPortWidget(
name: 'PortOut3',
icon: const Icon(
Icons.play_arrow_outlined,
color: Colors.blue,
size: 24,
),
iconConnected: const Icon(
Icons.play_arrow,
color: Colors.blue,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.blue,
strokeWidth: 2,
),
),
],
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
InPortWidget(
name: 'PortIn2',
onConnect: (String name, String port) => true,
icon: const Icon(
Icons.play_arrow_outlined,
color: Colors.red,
size: 24,
),
iconConnected: const Icon(
Icons.play_arrow,
color: Colors.red,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.red,
strokeWidth: 2,
),
),
const Text('Input 2'),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Value: '),
SizedBox(
width: 50,
height: 25,
child: TextEditProperty(
name: 'text_prop',
focusNode: focusNode,
controller: controller,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 14,
),
decoration: InputDecoration(
filled: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 0.0,
horizontal: 5.0,
),
fillColor: Colors.white10,
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.white),
borderRadius: BorderRadius.circular(5),
),
),
),
),
],
),
],
),
title: const Text('Receiver'),
iconTileSpacing: 5,
titleBarPadding: const EdgeInsets.all(4.0),
titleBarGradient: const LinearGradient(
colors: [Color.fromRGBO(12, 100, 6, 1.0), Colors.greenAccent],
),
icon: const Icon(Icons.receipt_rounded, color: Colors.white),
width: 200,
);
}
NodeWidgetBase binaryNode(String name) {
return ContainerNodeWidget(
name: name,
typeName: 'node_3',
backgroundColor: Colors.blue.shade800,
radius: 10,
width: 200,
contentPadding: const EdgeInsets.all(4),
selectedBorder: Border.all(color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InPortWidget(
name: 'PortIn1',
onConnect: (String name, String port) => true,
icon: const Icon(
Icons.circle_outlined,
color: Colors.yellowAccent,
size: 20,
),
iconConnected: const Icon(
Icons.circle,
color: Colors.yellowAccent,
size: 20,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.yellowAccent,
strokeWidth: 2,
),
),
InPortWidget(
name: 'PortIn2',
onConnect: (String name, String port) => true,
icon: const Icon(
Icons.circle_outlined,
color: Colors.yellowAccent,
size: 20,
),
iconConnected: const Icon(
Icons.circle,
color: Colors.yellowAccent,
size: 20,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.yellowAccent,
strokeWidth: 2,
),
),
],
),
const Icon(Icons.safety_divider),
OutPortWidget(
name: 'PortOut1',
icon: const Icon(
Icons.pause_circle_outline,
color: Colors.deepOrange,
size: 24,
),
iconConnected: const Icon(
Icons.pause_circle,
color: Colors.deepOrange,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.deepOrange,
strokeWidth: 2,
),
),
],
),
);
}
NodeWidgetBase sinkNode(String name) {
return TitleBarNodeWidget(
name: name,
typeName: 'node_4',
backgroundColor: Colors.green.shade800,
radius: 10,
selectedBorder: Border.all(color: Colors.white),
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
InPortWidget(
name: 'PortIn1',
onConnect: (String name, String port) => true,
icon: const Icon(
Icons.add_circle_outline,
color: Colors.blueAccent,
size: 24,
),
iconConnected: const Icon(
Icons.add_circle_outlined,
color: Colors.blueAccent,
size: 24,
),
multiConnections: false,
connectionTheme: ConnectionTheme(
color: Colors.blueAccent,
strokeWidth: 2,
),
),
const Text('Input 2'),
],
),
],
),
],
),
),
title: const Text(
'Sinker',
style: TextStyle(color: Colors.deepOrange, fontWeight: FontWeight.bold),
),
iconTileSpacing: 5,
titleBarPadding: const EdgeInsets.all(4.0),
titleBarGradient: const LinearGradient(
colors: [Colors.yellowAccent, Colors.yellow],
),
icon: const Icon(Icons.calculate_rounded, color: Colors.deepOrange),
width: 200,
);
}
π Example Project #
This package includes a full example/
app with multiple demos.
Run it with:
cd example
flutter run
π API Overview #
- FlowyCanvas β Infinite canvas container with pan/zoom
- FlowyNodeController β Manages nodes and connections
- NodeWidgetBase β Base class for creating custom nodes
- InPortWidget / OutPortWidget β Ports for connections
- ConnectionTheme β Style for edges
π Use Cases #
- Workflow & process editors
- Visual programming tools
- Data pipelines & ETL designers
- Mind maps & organizational charts
- Custom graph-based UIs
π§© Roadmap #
- Export/import graph as JSON
- Advanced edge routing (bezier, straight, orthogonal)
- Mini-map widget
- Undo/redo history
- Collaborative editing support
π€ Contributing #
Contributions are welcome! π
- Fork the repo
- Create a new branch (
feature/my-feature
) - Commit changes (
git commit -m 'Add my feature'
) - Push and create a PR
Please make sure your code is formatted (flutter format .
) and passes analysis (
flutter analyze
).
π License #
This project is licensed under the MIT License. See the LICENSE file for details.
β€οΈ Credits #
Created by Tanveer Ahmad.
Inspired by React Flow but reimagined for the Flutter ecosystem.
π Exclusive Software Deals #
Check out Dealsbe for exclusive software deals tailored for developers and startups. Find tools to boost your productivity and streamline your workflow.
Fresh Recommendations #
- Explore the latest deals on developer tools and services.
- Post a Deal to share your own software or service with the community.