super_tree_field 2.0.0 copy "super_tree_field: ^2.0.0" to clipboard
super_tree_field: ^2.0.0 copied to clipboard

Super Tree — a GeniusLink design-system Flutter package providing a recursive, generic, keyboard-first hierarchy component for arbitrary typed data. Supports search, expansion, selection, inline editi [...]

super_tree_field #

A recursive, generic, keyboard-first hierarchy component for Flutter with search, selection, editing, drag/drop, scrolling, localization, and reusable nested context menus.

Current version: 2.0.0

super_tree_field provides a typed hierarchy model, pure tree algorithms, controller-owned interaction state, a recursive SuperTree<T> view, reusable rows and controls, selection, editing, drag/drop, localization, and nested context menus.

SuperTree<T> is intentionally focused on the hierarchy and its nodes. Titles, subtitles, column headings, totals, cards, toolbars, and other screen-level presentation belong to the host application.

Features #

  • Generic TreeNode<T> payloads.
  • Recursive hierarchy rendering.
  • Expand/collapse and subtree expansion.
  • Keyboard-first navigation and focus.
  • Automatic scrolling to keep the focused node visible.
  • Search filtering and matched-text highlighting.
  • None, single, and multi selection modes.
  • Tri-state group selection.
  • Readable and editable modes.
  • Inline rename and node insertion/deletion.
  • Drag-and-drop reordering.
  • Optional external SuperTreeControls<T>.
  • Reusable standalone context menus.
  • Recursive nested context menus.
  • Per-node menu items, style, accent, and enabled state.
  • LTR/RTL-aware menu placement.
  • English and Arabic package localization.
  • Configurable Flutter scroll behavior.
  • super_core design-system integration.

Getting started #

Add the package:

dependencies:
  super_tree_field: ^2.0.0

Import the public barrel:

import 'package:super_tree_field/super_tree.dart';

Quick start #

class FileMeta {
  const FileMeta({
    required this.isDirectory,
    this.size,
  });

  final bool isDirectory;
  final int? size;
}

final roots = <TreeNode<FileMeta>>[
  TreeNode<FileMeta>(
    code: 'lib',
    name: 'lib',
    value: const FileMeta(isDirectory: true),
    children: const [
      TreeNode<FileMeta>(
        code: 'lib/main.dart',
        name: 'main.dart',
        value: FileMeta(
          isDirectory: false,
          size: 2048,
        ),
      ),
    ],
  ),
];

final controller = SuperTreeController<FileMeta>(
  roots: roots,
  searchText: (node) =>
      '${node.code} ${node.name} ${node.ar ?? ''}',
  defaultExpandDepth: 1,
  onOpenLeaf: (node) {
    // Open the selected item.
  },
);

SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: (context, node, info) {
    return Icon(
      node.hasChildren
          ? Icons.folder_outlined
          : Icons.insert_drive_file_outlined,
    );
  },
  trailingBuilder: (context, node, info) {
    final size = node.value?.size;
    return size == null ? null : Text('$size B');
  },
);

Dispose controllers owned by the host widget:

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

Public API #

Component Purpose
TreeNode<T> Immutable generic hierarchy node.
TreeLogic Pure tree algorithms and immutable transforms.
DropPosition Before/inside/after drag target position.
SearchText<T> Node search-text extractor.
LeafValue<T> Numeric leaf extractor for rollups.
SuperTreeController<T> Expansion, search, focus, selection, and editing state.
SuperTreeMode Readable or editable mode.
SuperTreeSelectionMode None, single, or multi selection.
TreeCheckState Unchecked, partial, or checked state.
SuperTree<T> Main hierarchy viewport.
TreeRow<T> Reusable recursive row.
TreeRowInfo Depth/open/children metadata supplied to builders.
TreeSlotBuilder<T> Leading-cell builder.
TreeTrailingBuilder<T> Trailing-content builder.
TreeCheckbox Standalone tri-state tree checkbox.
HighlightText Search-match highlighting widget.
SuperTreeControlsController Search state for external controls.
SuperTreeControls<T> Optional search/edit/help/expand toolbar.
TreeContextMenuAction Stable IDs for built-in menu actions.
TreeContextMenuItem Reusable menu leaf or submenu branch.
TreeContextMenuItemsBuilder<T> Per-node menu-items builder.
TreeContextMenuStyle Menu geometry and visual configuration.
TreeContextMenuNodeContext<T> Full node context for menu policy.
TreeContextMenuConfig Per-node menu overrides.
TreeContextMenuConfigBuilder<T> Complete per-node menu configurator.
buildDefaultTreeContextMenuItems() Builds localized package-default actions.
showTreeContextMenu() Opens a tree-aware or standalone menu.
showShortcutsHelp() Opens the keyboard-shortcuts dialog.
SuperTreeLocalization Package localization API.
lookupSuperTreeLocalization() Resolves package localization for an explicit locale.
SuperTreeLocalizationBuildContext Localization convenience extension.

TreeNode #

TreeNode<T> is the immutable hierarchy entity. code must be unique across the tree.

const node = TreeNode<double>(
  code: 'assets/cash',
  name: 'Cash',
  ar: 'النقدية',
  value: 12500,
);

final renamed = node.renamed(
  'Cash and equivalents',
  ar: 'النقد وما في حكمه',
);

final branch = TreeNode<String>(
  code: 'settings',
  name: 'Settings',
  children: const [
    TreeNode<String>(
      code: 'settings/profile',
      name: 'Profile',
      value: 'profile',
    ),
  ],
);

print(branch.hasChildren);
print(branch.isLeaf);

Use copyWith() and withChildren() for immutable transforms.

TreeLogic #

TreeLogic is widget-free and can be used independently.

final count = TreeLogic.leafCount(branch);
final leafCodes = TreeLogic.leafCodes(branch);

final groups = TreeLogic.groupCodes(
  roots,
  maxDepth: 2,
);

final filtered = TreeLogic.filter(
  roots,
  'main',
  (node) => '${node.code} ${node.name}',
);

final visible = TreeLogic.flattenVisible(
  roots,
  {'lib', 'lib/src'},
  false,
);

final parent = TreeLogic.parentOf(
  roots,
  'lib/main.dart',
);

final found = TreeLogic.findNode(
  roots,
  'lib/main.dart',
);

Numeric rollup:

LeafValue<double> leafValue =
    (node) => node.value ?? 0;

final total = TreeLogic.rollup<double>(
  accountRoot,
  leafValue,
);

Immutable editing:

final renamedRoots = TreeLogic.mapNode(
  roots,
  'lib/main.dart',
  (node) => node.renamed('app.dart'),
);

final withoutNode = TreeLogic.removeNode(
  roots,
  'lib/old.dart',
);

final withChild = TreeLogic.insertChild(
  roots,
  'lib',
  newNode,
);

final withSibling = TreeLogic.insertSibling(
  roots,
  'lib/main.dart',
  newNode,
  after: true,
);

final moved = TreeLogic.moveNode(
  roots,
  'lib/a.dart',
  'lib/b.dart',
  DropPosition.after,
);

DropPosition #

const drop = DropPosition.inside;

switch (drop) {
  case DropPosition.before:
    break;
  case DropPosition.inside:
    break;
  case DropPosition.after:
    break;
}

SearchText #

SearchText<FileMeta> searchText =
    (node) => '${node.code} ${node.name}';

LeafValue #

LeafValue<double> amount =
    (node) => node.value ?? 0;

SuperTreeController #

final controller = SuperTreeController<FileMeta>(
  roots: roots,
  searchText: (node) => '${node.code} ${node.name}',
  mode: SuperTreeMode.editable,
  selectionMode: SuperTreeSelectionMode.multi,
  initialChecked: {'lib/main.dart'},
  onOpenLeaf: (node) => openFile(node),
  onTreeChanged: (newRoots) => saveTree(newRoots),
  onSelectionChanged: (checked) {
    debugPrint('$checked');
  },
  newNodeBuilder: (code) => TreeNode<FileMeta>(
    code: code,
    name: 'Untitled',
    value: const FileMeta(isDirectory: false),
  ),
);

Expansion and search:

controller.toggle('lib');
controller.expandSubtree('lib');
controller.expandAll();
controller.collapseAll();

controller.setQuery('main');
controller.clearQuery();

print(controller.visible);
print(controller.matchCount);
print(controller.totalLeaves);
print(controller.visibleLeaves);

Focus and activation:

controller.setFocus('lib/main.dart');
controller.moveDown();
controller.moveUp();
controller.jumpFirst();
controller.jumpLast();
controller.stepInto();
controller.stepOut();
controller.activate();

Selection:

controller.toggleChecked(node);
controller.toggleCheckedFocused();
controller.checkAll();
controller.clearChecked();
controller.toggleCheckAll();

controller.setChecked({
  'lib/main.dart',
  'lib/app.dart',
});

final checked = controller.checked;
final nodes = controller.checkedNodes;
final state = controller.checkState('lib');

Editing:

controller.setMode(SuperTreeMode.editable);

controller.beginRename('lib/main.dart');
controller.commitRename(
  'lib/main.dart',
  'app.dart',
);

controller.addChild('lib');
controller.addSiblingBefore('lib/main.dart');
controller.addSiblingAfter('lib/main.dart');
controller.addRoot();

controller.deleteNode('lib/old.dart');

if (controller.canDrop(
  'lib/a.dart',
  'lib/b.dart',
)) {
  controller.moveNode(
    'lib/a.dart',
    'lib/b.dart',
    DropPosition.after,
  );
}

SuperTreeMode #

controller.setMode(SuperTreeMode.readable);
controller.setMode(SuperTreeMode.editable);
controller.toggleMode();

SuperTreeSelectionMode #

final controller = SuperTreeController<String>(
  roots: roots,
  searchText: (node) => node.name,
  selectionMode: SuperTreeSelectionMode.multi,
);

Available values:

SuperTreeSelectionMode.none
SuperTreeSelectionMode.single
SuperTreeSelectionMode.multi

TreeCheckState #

final state = controller.checkState(node.code);

final icon = switch (state) {
  TreeCheckState.unchecked =>
    Icons.check_box_outline_blank,
  TreeCheckState.partial =>
    Icons.indeterminate_check_box,
  TreeCheckState.checked =>
    Icons.check_box,
};

SuperTree #

SuperTree<T> owns tree and node interaction only.

Card(
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          'Project files',
          style: Theme.of(context)
              .textTheme
              .titleLarge,
        ),
        const SizedBox(height: 12),
        Expanded(
          child: SuperTree<FileMeta>(
            controller: controller,
            leadingBuilder: buildLeading,
            trailingBuilder: buildTrailing,
            showArabic: true,
            showLeafCount: true,
            onSearchRequested:
                controls.requestSearchFocus,
            onShortcutsRequested:
                () => showShortcutsHelp(context),
          ),
        ),
      ],
    ),
  ),
);

Scrolling #

The widget exposes Flutter scroll configuration directly:

final scrollController = ScrollController();

SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: buildLeading,
  scrollController: scrollController,
  primary: false,
  physics: const ClampingScrollPhysics(),
  reverse: false,
  shrinkWrap: false,
  cacheExtent: 600,
  restorationId: 'file-tree-scroll',
  keyboardDismissBehavior:
      ScrollViewKeyboardDismissBehavior.onDrag,
);

The focused row is kept visible when focus changes, including when a shrink-wrapped tree is inside another scrollable.

TreeRowInfo #

Widget buildLeading(
  BuildContext context,
  TreeNode<FileMeta> node,
  TreeRowInfo info,
) {
  return Icon(
    info.hasChildren
        ? (info.open
            ? Icons.folder_open_outlined
            : Icons.folder_outlined)
        : Icons.description_outlined,
  );
}

TreeSlotBuilder #

TreeSlotBuilder<FileMeta> leadingBuilder =
    (context, node, info) {
  return Icon(
    info.hasChildren
        ? Icons.folder_outlined
        : Icons.description_outlined,
  );
};

TreeTrailingBuilder #

TreeTrailingBuilder<FileMeta> trailingBuilder =
    (context, node, info) {
  if (info.hasChildren) return null;
  return Text('${node.value?.size ?? 0} B');
};

TreeRow #

TreeRow<T> is available for advanced host compositions.

TreeRow<FileMeta>(
  node: roots.first,
  depth: 0,
  controller: controller,
  accent: Theme.of(context)
      .colorScheme
      .primary,
  leadingBuilder: buildLeading,
  trailingBuilder: buildTrailing,
  showArabic: true,
  showLeafCount: true,
  onFocusRequest:
      focusNode.requestFocus,
);

Prefer SuperTree<T> for normal usage because it wires the full viewport, keyboard model, empty/search state, and root rows.

TreeCheckbox #

TreeCheckbox(
  state: controller.checkState(node.code),
  accent: Theme.of(context)
      .colorScheme
      .primary,
  onTap: () => controller.toggleChecked(node),
);

HighlightText #

HighlightText(
  text: node.name,
  query: controller.query,
  style: Theme.of(context)
      .textTheme
      .bodyMedium!,
  overflow: TextOverflow.ellipsis,
);

SuperTreeControlsController #

final controls = SuperTreeControlsController(
  query: controller.query,
);

controls.requestSearchFocus();

@override
void dispose() {
  controls.dispose();
  controller.dispose();
  super.dispose();
}

SuperTreeControls #

The optional toolbar is composed outside SuperTree.

Column(
  children: [
    SuperTreeControls<FileMeta>(
      controller: controller,
      controlsController: controls,
      placeholder: 'Search project files…',
      samples: const [
        'lib',
        'test',
        '.dart',
      ],
      enableEditing: true,
      extra: Text(
        '${controller.visibleLeaves} visible leaves',
      ),
    ),
    const SizedBox(height: 12),
    Expanded(
      child: SuperTree<FileMeta>(
        controller: controller,
        leadingBuilder: buildLeading,
        onSearchRequested:
            controls.requestSearchFocus,
        onShortcutsRequested:
            () => showShortcutsHelp(context),
      ),
    ),
  ],
);

localizeDefaultText: false keeps fixed default control labels instead of resolving package defaults from the active locale.

Context menus #

TreeContextMenuAction #

Filter built-in actions by stable ID, not by translated label:

final withoutDelete = defaults.where(
  (item) =>
      item.id != TreeContextMenuAction.delete,
);

TreeContextMenuItem #

final items = <TreeContextMenuItem>[
  TreeContextMenuItem(
    id: 'open',
    label: 'Open',
    leading:
        const Icon(Icons.open_in_new),
    onTap: openCurrent,
  ),
  TreeContextMenuItem(
    id: 'create',
    label: 'Create',
    leading: const Icon(Icons.add),
    children: [
      TreeContextMenuItem(
        id: 'file',
        label: 'File',
        onTap: createFile,
      ),
      TreeContextMenuItem(
        id: 'folder',
        label: 'Folder',
        onTap: createFolder,
      ),
    ],
  ),
];

children can recursively contain more branch items.

TreeContextMenuItemsBuilder #

TreeContextMenuItemsBuilder<FileMeta>
    menuItemsBuilder =
    (context, controller, node) {
  final defaults =
      buildDefaultTreeContextMenuItems<FileMeta>(
    context: context,
    controller: controller,
    node: node,
  );

  return [
    ...defaults.where(
      (item) =>
          item.id !=
          TreeContextMenuAction.delete,
    ),
    TreeContextMenuItem(
      id: 'properties',
      label: 'Properties',
      dividerAbove: true,
      onTap: () => openProperties(node),
    ),
  ];
};

Use it directly on the tree:

SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: buildLeading,
  contextMenuItemsBuilder:
      menuItemsBuilder,
);

TreeContextMenuStyle #

const menuStyle = TreeContextMenuStyle(
  width: 280,
  edgePadding: 12,
  submenuGap: 6,
  itemPadding: EdgeInsets.symmetric(
    horizontal: 12,
    vertical: 9,
  ),
  borderWidth: 1,
  disabledOpacity: 0.4,
);
SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: buildLeading,
  contextMenuStyle: menuStyle,
);

TreeContextMenuNodeContext #

The per-node configuration builder receives the complete context:

TreeContextMenuConfigBuilder<FileMeta>
    inspectMenu = (menu) {
  final node = menu.node;
  final defaults =
      menu.packageDefaultItems;
  final base = menu.baseItems;

  debugPrint(node.code);
  debugPrint('${defaults.length}');
  debugPrint('${base.length}');

  return const TreeContextMenuConfig();
};

It also provides context, controller, defaultStyle, and defaultAccent.

TreeContextMenuConfig #

contextMenuConfigBuilder: (menu) {
  if (menu.node.code == 'protected') {
    return const TreeContextMenuConfig.disabled();
  }

  if (menu.node.hasChildren) {
    return TreeContextMenuConfig(
      style: const TreeContextMenuStyle(
        width: 300,
      ),
      accent: Theme.of(menu.context)
          .colorScheme
          .secondary,
      items: [
        ...menu.baseItems,
        TreeContextMenuItem(
          id: 'folder-info',
          label: 'Folder information',
          onTap: () =>
              showFolderInfo(menu.node),
        ),
      ],
    );
  }

  return const TreeContextMenuConfig();
},

TreeContextMenuConfigBuilder #

TreeContextMenuConfigBuilder<FileMeta>
    menuPolicy = (menu) {
  if (menu.node.value?.isDirectory == true) {
    return TreeContextMenuConfig(
      items: [
        ...menu.baseItems,
        TreeContextMenuItem(
          id: 'new-file',
          label: 'New file here',
          onTap: () =>
              createFileIn(menu.node),
        ),
      ],
    );
  }

  return const TreeContextMenuConfig();
};
SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: buildLeading,
  contextMenuConfigBuilder: menuPolicy,
);

buildDefaultTreeContextMenuItems() #

final defaults =
    buildDefaultTreeContextMenuItems<FileMeta>(
  context: context,
  controller: controller,
  node: node,
);

final safeItems = defaults
    .where(
      (item) =>
          item.id !=
          TreeContextMenuAction.delete,
    )
    .toList();

showTreeContextMenu() #

Use the menu independently from SuperTree:

GestureDetector(
  onSecondaryTapDown: (details) {
    showTreeContextMenu<void>(
      context: context,
      globalPosition:
          details.globalPosition,
      items: [
        TreeContextMenuItem(
          label: 'Refresh',
          leading:
              const Icon(Icons.refresh),
          onTap: refresh,
        ),
        TreeContextMenuItem(
          label: 'More',
          children: [
            TreeContextMenuItem(
              label: 'Details',
              onTap: openDetails,
            ),
          ],
        ),
      ],
    );
  },
  child: const Text('Right-click me'),
);

Keyboard shortcuts #

Key Action
/ Move between visible rows.
/ Collapse or expand according to text direction.
Home / End Jump to first or last visible row.
Enter / Space Activate the focused node.
Space Toggle checkbox selection when applicable.
/ Request focus for host search UI.
* Expand all.
\ Collapse all.
? Request keyboard help.

showShortcutsHelp() #

SuperTree<FileMeta>(
  controller: controller,
  leadingBuilder: buildLeading,
  onShortcutsRequested:
      () => showShortcutsHelp(context),
);

Or open it from host UI:

IconButton(
  icon: const Icon(
    Icons.keyboard_command_key,
  ),
  onPressed:
      () => showShortcutsHelp(context),
);

SuperTreeLocalization #

Package-owned text is localized in English and Arabic.

MaterialApp(
  locale: locale,
  localizationsDelegates:
      SuperTreeLocalization.localizationsDelegates,
  supportedLocales:
      SuperTreeLocalization.supportedLocales,
  home: const MyHomePage(),
);

If the host owns the full delegate list:

MaterialApp(
  locale: locale,
  localizationsDelegates: const [
    SuperTreeLocalization.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales:
      SuperTreeLocalization.supportedLocales,
  home: const MyHomePage(),
);

lookupSuperTreeLocalization() #

Resolve package strings without a BuildContext when an explicit locale is already available:

final l10n = lookupSuperTreeLocalization(
  const Locale('ar'),
);

print(l10n.treeEmpty);

SuperTreeLocalizationBuildContext #

The exported extension provides direct access through BuildContext:

final l10n = context.superTreeLocalization;

Text(l10n.keyboardShortcuts);

superTreeEnglishLocalizationFallback #

Use the English fallback only when package localization is unavailable in the current context:

final emptyMessage =
    superTreeEnglishLocalizationFallback.treeEmpty;

Application-specific titles, node labels, custom menu labels, and page text remain owned by the host application's localization layer.

Host composition #

Screen-level presentation stays outside the tree:

AnimatedBuilder(
  animation: controller,
  builder: (context, _) {
    return Column(
      crossAxisAlignment:
          CrossAxisAlignment.stretch,
      children: [
        Text(
          'Files',
          style: Theme.of(context)
              .textTheme
              .headlineSmall,
        ),
        Text(
          '${controller.checkedCount} selected · '
          '${controller.totalLeaves} leaves',
        ),
        const SizedBox(height: 12),
        SuperTreeControls<FileMeta>(
          controller: controller,
          controlsController: controls,
        ),
        const SizedBox(height: 12),
        Expanded(
          child: SuperTree<FileMeta>(
            controller: controller,
            leadingBuilder: buildLeading,
            trailingBuilder: buildTrailing,
          ),
        ),
      ],
    );
  },
);

This lets the same tree work inside cards, dialogs, settings pages, desktop panels, mobile screens, and domain-specific layouts.

Complete component coverage #

The README generator verifies the exports from lib/super_tree.dart before writing. The package-owned API documented here includes:

TreeNode<T>
TreeLogic
DropPosition
SearchText<T>
LeafValue<T>

SuperTreeController<T>
SuperTreeMode
SuperTreeSelectionMode
TreeCheckState

SuperTree<T>
TreeRow<T>
TreeRowInfo
TreeSlotBuilder<T>
TreeTrailingBuilder<T>
TreeCheckbox
HighlightText

SuperTreeControlsController
SuperTreeControls<T>

TreeContextMenuAction
TreeContextMenuItem
TreeContextMenuItemsBuilder<T>
TreeContextMenuStyle
TreeContextMenuNodeContext<T>
TreeContextMenuConfig
TreeContextMenuConfigBuilder<T>
buildDefaultTreeContextMenuItems()
showTreeContextMenu()

showShortcutsHelp()

SuperTreeLocalization
lookupSuperTreeLocalization()
SuperTreeLocalizationBuildContext

Example application #

The example/ application demonstrates account, file, organization, permission, product, scrolling, and context-menu scenarios.

Example screens can show the live widget and its Dart usage code together, making it easier to compare behavior with the integration code.

Additional information #

For bugs and feature requests, use the package issue tracker when available. Contributions should preserve the generic hierarchy boundary: domain-specific screen presentation should remain outside the core tree API.

0
likes
140
points
312
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Super Tree — a GeniusLink design-system Flutter package providing a recursive, generic, keyboard-first hierarchy component for arbitrary typed data. Supports search, expansion, selection, inline editing, drag-and-drop, custom cells, scrolling, LTR/RTL, and light/dark themes.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #widget #treeview #hierarchy #accessibility

License

MIT (license)

Dependencies

flutter, flutter_localizations, intl, super_core, super_form_field

More

Packages that depend on super_tree_field