flutter_folderview 0.11.2
flutter_folderview: ^0.11.2 copied to clipboard
A customizable Flutter widget for displaying hierarchical data in tree and folder views with rich theming support.
Flutter FolderView #
A customizable Flutter widget for displaying hierarchical data in tree and folder views.
Installation #
dependencies:
flutter_folderview: ^0.11.2
Requires Flutter 3.13.0 (Dart 3.1.0) or newer, raised from 3.10.0 in 0.11.0.
Basic Usage #
import 'package:flutter_folderview/flutter_folderview.dart';
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final Set<String> _expandedIds = {};
@override
Widget build(BuildContext context) {
return FolderView(
data: [
Node(
id: '1',
label: 'Documents',
type: NodeType.folder,
children: [
Node(
id: '2',
label: 'Work',
type: NodeType.parent,
children: [
Node(id: '3', label: 'Report.pdf', type: NodeType.child),
],
),
],
),
],
expandedNodeIds: _expandedIds,
mode: ViewMode.folder,
onNodeTap: (node) {
if (node.type != NodeType.child) {
setState(() {
if (_expandedIds.contains(node.id)) {
_expandedIds.remove(node.id);
} else {
_expandedIds.add(node.id);
}
});
}
},
);
}
}
Node Types #
NodeType.folder: Top-level container nodesNodeType.parent: Mid-level nodes that can have childrenNodeType.child: Leaf nodes
View Modes #
ViewMode.folder: Hierarchical folder structure (folder → parent → child)ViewMode.tree: Flattened tree structure (parent → child)
Line Styles #
theme: FlutterFolderViewTheme(
lineTheme: FolderViewLineTheme(
lineStyle: LineStyle.connector, // ├─ └─ connectors
// lineStyle: LineStyle.scope, // Vertical scope lines
// lineStyle: LineStyle.none, // No lines
),
)
Theming #
FolderView(
data: nodes,
expandedNodeIds: expandedIds,
theme: FlutterFolderViewTheme(
folderTheme: FolderNodeTheme(
widget: Icon(Icons.folder),
openWidget: Icon(Icons.folder_open),
textStyle: TextStyle(fontSize: 14),
),
parentTheme: ParentNodeTheme(
widget: Icon(Icons.description),
textStyle: TextStyle(fontSize: 14),
),
childTheme: ChildNodeTheme(
widget: Icon(Icons.insert_drive_file),
textStyle: TextStyle(fontSize: 14),
selectedTextStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
),
)
Scale #
Scale all content proportionally. Scrollbars and tooltips are not affected — they are treated as chrome (interactive overlays sized for input devices, not for content density).
FolderView(
data: nodes,
mode: ViewMode.folder,
scale: 1.5, // default 1.0
expandedNodeIds: expandedIds,
)
| Scaled | Not Scaled |
|---|---|
| Row height, row spacing | Scrollbar thickness, track width |
| Icon sizes, padding, margin | Scrollbar colors |
| Text fontSize, letterSpacing | Tooltip dimensions (padding, arrow, offset, text) |
| Line width, indentation | Animation durations, click intervals |
| Content padding, border radius | Colors |
Each theme class also exposes a scale() method directly — useful for callers who need a scaled copy of a theme outside FolderView (custom previews, integration tests, etc.):
final scaledTheme = myTheme.scaledForContext(context, 1.5);
// or, without a BuildContext:
final scaledTheme = myTheme.scale(factor: 1.5, defaultFontSize: 14.0);
Ctrl (Windows/Linux) or Cmd (macOS) + scroll wheel is blocked by default (blockModifierScroll: true) so zoom-by-scroll works without unintended scrolling. Set blockModifierScroll: false to allow normal scrolling while the modifier key is held.
The library also exports isScaleModifierPressed() — a platform-aware helper that returns true when Ctrl (Windows/Linux) or Cmd (macOS) is pressed. Use it to implement custom zoom logic:
Listener(
onPointerSignal: (event) {
if (event is PointerScrollEvent && isScaleModifierPressed()) {
final delta = event.scrollDelta.dy > 0 ? -0.1 : 0.1;
setState(() => _scale = (_scale + delta).clamp(0.5, 3.0));
}
},
child: FolderView(
data: nodes,
mode: ViewMode.folder,
scale: _scale,
expandedNodeIds: expandedIds,
),
)
FolderView(
data: nodes,
mode: ViewMode.folder,
scale: 1.5,
blockModifierScroll: true, // default: true
expandedNodeIds: expandedIds,
)
Tooltip #
There are two, and they differ by where you declare them and what they explain.
| Declared on | Styled by | Attaches to | Explains | |
|---|---|---|---|---|
| Label tooltip | NodeTooltipTheme, per node type |
the same object | The label's glyphs | The label — hover truncated text to read the rest |
| Row tooltip | FolderView.rowTooltipBuilder, once |
FolderView.rowTooltipTheme |
The rest of the row | The node — a card of its details |
Enable both. Only one is ever visible — the innermost under the pointer — and the label tooltip claims exactly the glyphs it explains, so the row card gets the indent, the icon, the expand chevron, and the space beside a short label. See Row tooltip.
Label tooltip #
Each node type supports tooltip via NodeTooltipTheme:
FolderView(
data: nodes,
expandedNodeIds: expandedIds,
theme: FlutterFolderViewTheme(
folderTheme: FolderNodeTheme(
tooltipTheme: NodeTooltipTheme(
useTooltip: true,
message: 'Folder node',
direction: TooltipDirection.top,
alignment: TooltipAlignment.center,
interactive: true,
waitDuration: Duration(milliseconds: 500),
showDuration: Duration(seconds: 3),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8)],
),
),
),
)
| Property | Type | Description |
|---|---|---|
useTooltip |
bool |
Enable tooltip (default: false) |
message |
String? |
Static tooltip text |
tooltipBuilder |
WidgetBuilder? |
Custom tooltip widget |
tooltipBuilderResolver |
Function? |
Node-specific tooltip widget resolver |
direction |
TooltipDirection |
Position: top, bottom, left, right |
alignment |
TooltipAlignment |
Alignment: start, center, end, startTargetCenter, endTargetCenter |
anchor |
TooltipAnchor |
Anchor the tooltip to the row's rect or the cursor: child (default), pointer |
offset |
double |
Distance from widget (default: 8.0) |
crossAxisOffset |
double |
Cross-axis offset (default: 0.0) |
backgroundColor |
Color? |
Background color |
elevation |
double? |
Shadow depth |
boxShadow |
List<BoxShadow>? |
Custom shadow (overrides elevation) |
borderRadius |
BorderRadius? |
Corner rounding |
padding |
EdgeInsets? |
Inner padding |
enableTap |
bool? |
Tap to show (default: false) |
enableHover |
bool? |
Hover to show (default: true) |
interactive |
bool? |
Keep visible on tooltip hover |
waitDuration |
Duration? |
Delay before showing on hover |
showDuration |
Duration? |
Auto-hide after duration (resets on re-enter) |
animationDuration |
Duration? |
Show/hide animation duration |
controller |
JustTooltipController? |
Programmatic show/hide control |
onShow |
VoidCallback? |
Callback when shown |
onHide |
VoidCallback? |
Callback when hidden |
showArrow |
bool? |
Show arrow on tooltip (default: false) |
arrowBaseWidth |
double? |
Arrow base width (default: 12.0) |
arrowLength |
double? |
Arrow length (default: 6.0) |
arrowPositionRatio |
double? |
Arrow position ratio 0.0~1.0 (default: 0.25) |
borderColor |
Color? |
Tooltip border color |
borderWidth |
double? |
Tooltip border width (default: 0.0) |
screenMargin |
double? |
Min margin from screen edges (default: 8.0) |
animation |
TooltipAnimation? |
Animation type: none, fade, scale, slide, fadeScale, fadeSlide, rotation |
animationCurve |
Curve? |
Custom curve for animation |
fadeBegin |
double? |
Starting opacity for fade animations (default: 0.0) |
scaleBegin |
double? |
Starting scale for scale animations (default: 0.0) |
slideOffset |
double? |
Slide distance as fraction of tooltip size (default: 0.3) |
rotationBegin |
double? |
Starting rotation in turns (default: -0.05) |
hideOnEmptyMessage |
bool? |
Suppress tooltip when message is empty (default: true) |
Anchoring to the cursor #
A node's tooltip attaches to its icon-and-label content, not to the whole rendered row. But a row is as wide as the tree's longest label, and each label grows to fill its row — so a long label's rect spans the row, and a tooltip anchored to that rect appears at the row's centre, far from where the user is actually pointing. TooltipAnchor.pointer keeps the same hover region but places the tooltip at the cursor:
NodeTooltipTheme(
useTooltip: true,
message: 'Report.pdf',
anchor: TooltipAnchor.pointer, // default: TooltipAnchor.child
)
The anchor is captured when the tooltip is shown and does not follow the pointer, so interactive tooltips stay reachable. Tap-triggered tooltips anchor at the tap, and a controller-driven show with no pointer present falls back to the label's rect.
Against a point there are no target edges to align to, so under TooltipAnchor.pointer the alignment field selects which of the tooltip's own edges lands on the pointer.
Note that anchor does not widen the hover region. A short label occupies only the left part of its row, and the space to its right raises no tooltip under either anchor. To make the whole row hoverable, use the row tooltip below.
When a tree is wide enough to scroll horizontally, a long label's rect extends past the visible FolderView. TooltipAnchor.child aims at the centre of the part still on screen, so the tooltip stays inside the view.
Row tooltip #
rowTooltipBuilder returns a card shown while the pointer is anywhere over a node's row — the indent, the expand chevron, the empty space beside a short label. Return null for a node that should not have one.
FolderView(
data: nodes,
expandedNodeIds: expandedIds,
rowTooltipBuilder: (context, node) {
if (node.type == NodeType.folder) return null;
return Card(child: Padding(
padding: const EdgeInsets.all(12),
child: Text('${node.label} — ${node.children.length} children'),
));
},
)
The card supplies its own surface, so by default the tooltip around it draws no background, padding, or elevation. Give it a Card, not a bare Text.
It is anchored at the pointer, and that is the one knob you cannot change. A row is laid out at the tree's content width rather than the viewport's, so anchoring to the row's rect would aim at a centre that leaves the screen the moment the view scrolls horizontally.
Styling the card #
Everything else is rowTooltipTheme:
FolderView(
data: nodes,
expandedNodeIds: expandedIds,
rowTooltipBuilder: (context, node) => Card(child: Text(node.label)),
rowTooltipTheme: RowTooltipTheme(
waitDuration: Duration(milliseconds: 400), // don't flash on a quick sweep
interactive: true, // default: reach into the card
direction: TooltipDirection.bottom,
offset: 12,
),
)
| Property | Type | Description |
|---|---|---|
interactive |
bool |
Card stays while the cursor is over it (default: true) |
waitDuration |
Duration? |
Delay before showing. Null shows immediately |
showDuration |
Duration? |
Auto-hide after this long |
enableHover |
bool |
Whether hovering raises the card at all (default: true) |
direction |
TooltipDirection |
Side of the pointer; flips when there is no room |
alignment |
TooltipAlignment |
Which of the card's own edges lands on the pointer |
offset |
double |
Distance from the pointer (default: 8.0) |
crossAxisOffset |
double |
Offset along the cross-axis of direction |
screenMargin |
double |
Minimum distance from the Overlay's edges |
surface |
JustTooltipTheme |
The tooltip's own chrome. Defaults to JustTooltipTheme.bare() — nothing drawn |
animation, animationCurve, animationDuration |
Show/hide animation | |
fadeBegin, scaleBegin, slideOffset, rotationBegin |
double |
Animation start values |
onShow, onHide |
VoidCallback? |
Shown / hidden callbacks |
If your builder returns unadorned content rather than a Card, give surface a real JustTooltipTheme and let the tooltip draw the box.
Enabling both #
Enable both. They divide the row between them.
Only one tooltip is visible at a time — the innermost under the pointer — and the label tooltip claims exactly the glyphs it explains. Hover the label text and you get the label tooltip; hover anywhere else on the row (the indent, the icon, the expand chevron, the space beside a short label) and you get the row card.
The one place the card cannot appear is over the glyphs themselves. On the tree's widest row the label runs the length of the row, so there the card is reachable over the indent and the icon only. That is the geometry, not a setting.
Example #
See example/ for complete examples including:
- Theme customization
- Dynamic styling with resolver functions
- Data generation for testing
cd example
flutter run