novident_split_view 1.0.1
novident_split_view: ^1.0.1 copied to clipboard
A powerful split view for Flutter with id-based pane management, drag & drop splitting, pane moving/swapping, resizable panes, and duplicated-pane sync.
Novident Split View #
The library manages which buffers are visible and how they are organized — opening, splitting, stacking, swapping, resizing and closing panes — while you decide what every pane renders (an editor, a free form editor, a binder, anything).
Drag a node from your binder tree, drop it on a pane, and the view splits exactly where the cursor says.
See images
How it works — the 30-second model #
Three pieces, each with exactly one job:
| Piece | Job |
|---|---|
SplitViewController |
What is open and how it is arranged. A plain ChangeNotifier you create and keep. |
SplitViewProvider + SplitViewWeights |
The sizes of panes/columns. Mounted once, above your MaterialApp. |
paneBuilder |
What each pane renders. You receive an id, you return a widget. |
The controller only stores ids (plain Strings). It never touches your widgets or your data: an id means whatever you want it to mean.
Getting started #
dependencies:
novident_split_view: <latest>
A minimal, complete app #
Copy-paste and run — no nodes, no trees, no extra setup:
import 'package:flutter/material.dart';
import 'package:novident_split_view/novident_split_view.dart';
void main() {
runApp(
// 1. Pane sizes live ABOVE the app, so they survive any rebuild
// or navigation below. One line, done once.
SplitViewProvider(
weights: SplitViewWeights.twoGrid(),
child: const MaterialApp(home: Workspace()),
),
);
}
class Workspace extends StatefulWidget {
const Workspace({super.key});
@override
State<Workspace> createState() => _WorkspaceState();
}
class _WorkspaceState extends State<Workspace> {
// 2. The controller decides WHAT is open and WHERE.
final SplitViewController controller = SplitViewController();
@override
void initState() {
super.initState();
controller.open('notes'); // first buffer: takes the whole area
controller.insertPane( // second buffer: stacked below the first
column: 0,
pane: 0,
zone: SplitZone.bottom,
nodeId: 'draft',
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
// 3. The split view calls your paneBuilder for every open id.
body: NovSplitView(
controller: controller,
configuration: SplitViewConfiguration(
paneBuilder: (context, pane) => PaneHeader(
draggable: true, // drag the bar to move/swap panes
title: Text(pane.nodeId), // the close button needs NO wiring
child: Center(child: Text('Content of "${pane.nodeId}"')),
),
),
),
);
}
}
That is the whole integration. Running it you already have:
- Two stacked panes (by default) with a draggable divider between them.
- Headers with a working close button (
PaneHeadercloses its own pane through the library — no callback needed). - Pane moving: drag a header onto the other pane's center to swap them; with a single column, drag it to a screen edge to create a second column.
pane.nodeId is just the string you passed to open/insertPane —
map it to your own models however you like.
Optional: accept drops from your own tree #
Only needed when users drag things into the split view (e.g. rows
from a novident_tree_view
binder). The drag payload is a novident_nodes Node; implement the
mixin on the classes that may be dropped:
class File extends Node implements SplitDragAndDropMixin {
@override
bool isPaneDraggable() => true; // may enter the split view
@override
bool isSplitZoneValid(SplitZone zone) => true; // accepts every drop zone
}
Nothing else: while dragging, translucent shadows preview exactly
where the node will land, and on drop its node.id becomes the pane
id your paneBuilder receives.
Everyday operations #
controller.open('id'); // Focus, Replace or Open a new column when required
controller.forceOpen('id'); // Always open a new column at the end
controller.close('id'); // close the first pane showing it
controller.isOpen('id'); // is it visible somewhere?
controller.locate('id'); // (column, pane) or null
controller.focusPane(column, pane); // move the focus
controller.notifyNodeChanged('id'); // sync duplicated panes (opt-in)
// From anywhere inside a pane subtree:
SplitViewScope.rebuildPane(context); // refresh my pane
SplitViewScope.rebuildPanesShowing(context, 'id'); // refresh a node's panes
Features #
- Two-level grid layout by default: a global row of columns; each column stacks panes vertically with weights isolated from the rest.
- Id-based state: your nodes never leave their own tree — the controller stores strings, rendering is always yours.
- Drag & drop splitting with zone shadows, cursor tolerance bands and no dead zones.
- Pane dragging: move panes between columns or swap two panes by dropping one on the center of the other.
- Resizable panes with per-app persistable weights.
- Stable pane identity: the same document open in several panes, each with its own size and widget state.
- Duplicated-pane sync via content revisions or your own store.
- Limits (
maxPanes,maxColumns) enforced everywhere — invalid drops paint the shadow red before you release.
Documentation #
| Doc | What it covers |
|---|---|
| Architecture | The two-level grid model, id-based state, stable pane identity — and why each decision was made |
| Controller | Every SplitViewController operation, the duplicate rules, limits and content revisions |
| Zones & drag and drop | SplitZone, tolerance bands, global edges, both drag payloads, indicators |
| Weights & resizing | SplitViewWeights, the provider, dividers, and duplicated-pane weight policies |
| Widgets | NovSplitView, PaneHeader, PaneDragHandle, SplitViewScope, PaneContext |
| Recipes | The full example walkthrough: how it is built and why |
Ecosystem #
novident_nodes— the node model this package builds on.novident_tree_view— the binder tree; its node drags drop straight into the split view.
Additional information #
Issues and contributions are welcome at the Novident repositories