fl_nodes 0.4.0+1 copy "fl_nodes: ^0.4.0+1" to clipboard
fl_nodes: ^0.4.0+1 copied to clipboard

A lightweight, scalable, and highly customizable package that empowers Flutter developers to create dynamic, interactive, and visually appealing node-based UIs.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:example/data_handlers.dart';
import 'package:example/nodes.dart';
import 'package:example/utils/snackbar.dart';
import 'package:example/widgets/instructions.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:http/http.dart' as http;

import 'package:fl_nodes/fl_nodes.dart';

import './widgets/hierarchy.dart';
import './widgets/search.dart';

import 'l10n/app_localizations.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const NodeEditorExampleApp());
}

class NodeEditorExampleApp extends StatefulWidget {
  const NodeEditorExampleApp({super.key});

  @override
  State<NodeEditorExampleApp> createState() => _NodeEditorExampleAppState();
}

class _NodeEditorExampleAppState extends State<NodeEditorExampleApp> {
  late Locale _locale;

  final locales = [
    'en',
    'it',
    'fr',
    'es',
    'de',
    'ja',
    'zh',
    'ko',
    'ru',
    'ar',
  ];

  void _cycleLocale() {
    setState(() {
      final currentIndex = locales.indexOf(_locale.languageCode);
      final nextIndex = (currentIndex + 1) % locales.length;
      _locale = Locale(locales[nextIndex]);
    });
  }

  @override
  void initState() {
    super.initState();

    final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
    final supportedLanguageCodes = locales.toSet();
    final defaultLanguageCode =
        supportedLanguageCodes.contains(systemLocale.languageCode)
            ? systemLocale.languageCode
            : 'en';

    _locale = Locale(defaultLanguageCode);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        AppLocalizations.delegate,
        const FlNodeEditorLocalizationsDelegate(),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        ...locales.map((lang) => Locale(lang)),
      ],
      locale: _locale,
      title: 'Fl Nodes Example',
      theme: ThemeData.dark(),
      home: NodeEditorExampleScreen(onLocaleToggle: _cycleLocale),
      debugShowCheckedModeBanner: kDebugMode,
    );
  }
}

class NodeEditorExampleScreen extends StatefulWidget {
  const NodeEditorExampleScreen({
    super.key,
    required this.onLocaleToggle,
  });

  final VoidCallback onLocaleToggle;

  @override
  State<NodeEditorExampleScreen> createState() =>
      NodeEditorExampleScreenState();
}

class NodeEditorExampleScreenState extends State<NodeEditorExampleScreen> {
  late final FlNodeEditorController _nodeEditorController;

  bool isHierarchyCollapsed = true;

  @override
  void initState() {
    super.initState();

    _nodeEditorController = FlNodeEditorController(
      projectSaver: (jsonData) async {
        final String? outputPath = await FilePicker.platform.saveFile(
          dialogTitle: AppLocalizations.of(context)!.saveProjectDialogTitle,
          fileName: 'node_project.json',
          type: FileType.custom,
          allowedExtensions: ['json'],
          bytes: utf8.encode(jsonEncode(jsonData)),
        );

        if (outputPath != null || kIsWeb) {
          return true;
        } else {
          return false;
        }
      },
      projectLoader: (isSaved) async {
        if (!isSaved) {
          final bool? proceed = await showDialog<bool>(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text(AppLocalizations.of(context)!.unsavedChangesTitle),
                content: Text(
                  AppLocalizations.of(context)!.unsavedChangesMsg,
                ),
                actions: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(false),
                    child: Text(AppLocalizations.of(context)!.cancel),
                  ),
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(true),
                    child: Text(AppLocalizations.of(context)!.proceed),
                  ),
                ],
              );
            },
          );

          if (proceed != true) return null;
        }

        final FilePickerResult? result = await FilePicker.platform.pickFiles(
          type: FileType.custom,
          allowedExtensions: ['json'],
        );

        if (result == null) return null;

        late final String fileContent;

        if (kIsWeb) {
          final byteData = result.files.single.bytes!;
          fileContent = utf8.decode(byteData.buffer.asUint8List());
        } else {
          final File file = File(result.files.single.path!);
          fileContent = await file.readAsString();
        }

        return jsonDecode(fileContent);
      },
      projectCreator: (isSaved) async {
        if (isSaved) return true;

        final bool? proceed = await showDialog<bool>(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text(AppLocalizations.of(context)!.unsavedChangesTitle),
              content: Text(
                AppLocalizations.of(context)!.unsavedChangesMsg,
              ),
              actions: <Widget>[
                TextButton(
                  onPressed: () => Navigator.of(context).pop(false),
                  child: Text(AppLocalizations.of(context)!.cancel),
                ),
                TextButton(
                  onPressed: () => Navigator.of(context).pop(true),
                  child: Text(AppLocalizations.of(context)!.proceed),
                ),
              ],
            );
          },
        );

        return proceed == true;
      },
      onCallback: (type, message) =>
          showNodeEditorSnackbar(context, message, type),
    );

    registerDataHandlers(_nodeEditorController);
    registerNodes(context, _nodeEditorController);

    _loadSampleProject();
  }

  Future<void> _loadSampleProject() async {
    const sampleProjectLink =
        'https://raw.githubusercontent.com/WilliamKarolDiCioccio/fl_nodes/refs/heads/main/example/assets/www/node_project.json';

    final response = await http.get(Uri.parse(sampleProjectLink));

    if (response.statusCode == 200 && mounted) {
      _nodeEditorController.project.load(
        data: jsonDecode(response.body),
        context: context,
      );
    } else {
      if (!mounted) return;

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            AppLocalizations.of(context)!.failedToLoadSampleProject,
          ),
          backgroundColor: Colors.red,
        ),
      );
    }
  }

  @override
  void dispose() {
    _nodeEditorController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            HierarchyWidget(
              controller: _nodeEditorController,
              isCollapsed: isHierarchyCollapsed,
            ),
            Expanded(
              child: FlNodeEditorWidget(
                controller: _nodeEditorController,
                expandToParent: true,
                overlay: () {
                  return [
                    FlOverlayData(
                      child: Padding(
                        padding: const EdgeInsets.all(8),
                        child: Row(
                          spacing: 8,
                          children: [
                            // Hierarchy toggle button
                            IconButton.filled(
                              tooltip: AppLocalizations.of(context)!
                                  .toggleHierarchyTooltip,
                              style: IconButton.styleFrom(
                                backgroundColor: Colors.blue,
                              ),
                              onPressed: () => setState(() {
                                isHierarchyCollapsed = !isHierarchyCollapsed;
                              }),
                              icon: Icon(
                                isHierarchyCollapsed
                                    ? Icons.keyboard_arrow_right
                                    : Icons.keyboard_arrow_left,
                                size: 32,
                                color: Colors.white,
                              ),
                            ),
                            // Search widget
                            SearchWidget(controller: _nodeEditorController),
                            const Spacer(),
                            // Locale toggle button
                            IconButton.filled(
                              tooltip: AppLocalizations.of(context)!
                                  .cycleLocaleTooltip,
                              style: IconButton.styleFrom(
                                backgroundColor: Colors.blue,
                              ),
                              onPressed: widget.onLocaleToggle,
                              icon: const Icon(
                                Icons.translate,
                                size: 32,
                                color: Colors.white,
                              ),
                            ),
                            // Snap to grid toggle button
                            IconButton.filled(
                              tooltip: AppLocalizations.of(context)!
                                  .toggleSnapToGridTooltip,
                              style: IconButton.styleFrom(
                                backgroundColor: Colors.blue,
                              ),
                              onPressed: () =>
                                  _nodeEditorController.enableSnapToGrid(
                                !_nodeEditorController.config.enableSnapToGrid,
                              ),
                              icon: Icon(
                                _nodeEditorController.config.enableSnapToGrid
                                    ? Icons.grid_on
                                    : Icons.grid_off,
                                size: 32,
                                color: Colors.white,
                              ),
                            ),
                            // Execute graph button
                            IconButton.filled(
                              tooltip: AppLocalizations.of(context)!
                                  .executeGraphTooltip,
                              style: IconButton.styleFrom(
                                backgroundColor: Colors.blue,
                              ),
                              onPressed: () => _nodeEditorController.runner
                                  .executeGraph(context: context),
                              icon: const Icon(
                                Icons.play_arrow,
                                size: 32,
                                color: Colors.white,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ),
                    FlOverlayData(
                      bottom: 0,
                      left: 0,
                      child: const InstructionsWidget(),
                    ),
                  ];
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
38
likes
150
points
244
downloads

Publisher

unverified uploader

Weekly Downloads

A lightweight, scalable, and highly customizable package that empowers Flutter developers to create dynamic, interactive, and visually appealing node-based UIs.

Homepage
Repository (GitHub)
View/report issues

Topics

#visual-scripting #workflow-editor #node-editor

Documentation

API reference

Funding

Consider supporting this project:

studio.buymeacoffee.com

License

MIT (license)

Dependencies

flutter, flutter_context_menu, flutter_shaders, uuid

More

Packages that depend on fl_nodes