agnostic_split_view

A framework-agnostic split view for Flutter. Zero Material or Cupertino dependencies. Works with any design system, any theme, or a bare WidgetsApp.

dependencies:
  agnostic_split_view: ^0.1.2

Agnostic Split View Demo

Why

Flutter's widgets.dart layer is design-neutral. material.dart and cupertino.dart are opinionated layers on top. If you build a custom design system, embed a split view in another package, or simply want a layout primitive that doesn't drag Material into your dependency tree, this package is for you.

Features

  • Two-pane split: horizontal or vertical.
  • Drag-to-resize with min/max constraints.
  • Collapsible panes with drag-threshold snapping.
  • Four layouts: left/right, right/left, top/bottom, bottom/top.
  • RTL-aware. Directionality flips horizontal layouts automatically.
  • Controller with setFraction, collapseFirst/collapseSecond, expandFirst/expandSecond, toggleFirst toggleSecond, and reset.
  • Six divider styles: line, rounded, floating, bordered, handle, none. Three size presets plus custom.
  • Fully themable via SplitViewTheme (an InheritedWidget, not a ThemeExtension).
  • Zero dependencies beyond flutter.

Usage

Minimal

SplitView(
  direction: SplitDirection.horizontal,
  first: const Sidebar(),
  second: const Content(),
)

With a controller

final controller = SplitViewController(initialFraction: 0.3);

SplitView(
  direction: SplitDirection.horizontal,
  controller: controller,
  firstCollapsible: true,
  minFirstPaneSize: 200,
  maxFirstPaneSize: 400,
  first: const Sidebar(),
  second: const Content(),
)

// Later:
controller.toggleFirst();

(Note: If you create the controller manually, remember to call controller.dispose() in your widget's dispose method.)

Vertical, reversed (bottom pane first)

SplitView(
  direction: SplitDirection.vertical,
  reverse: true,
  first: const Console(),
  second: const Editor(),
  initialFraction: 0.3,
)

Custom divider

SplitView(
  direction: SplitDirection.horizontal,
  dividerThickness: 16,
  dividerBuilder: (context, state) => SplitDivider(
    state: state,
    style: SplitDividerStyle.floating,
    size: SplitDividerSize.medium,
    borderRadius: BorderRadius.circular(8),
    hoverColor: Colors.blue,
  ),
  first: const Sidebar(),
  second: const Content(),
)

Theming a subtree

SplitViewTheme.overrideWith(
  data: const SplitViewTheme(
    dividerThickness: 16,
    defaultDividerStyle: SplitDividerStyle.rounded,
    defaultDividerColor: Color(0xFFDDDDDD),
  ),
  child: SplitView(...),
)

(Note: SplitViewTheme.overrideWith completely replaces the theme for its subtree. It does not merge with parent SplitViewTheme values.)

Responsive sizing with flutter_screenutil

The package does not depend on flutter_screenutil. Pass scaled values directly:

SplitView(
  dividerThickness: 12.w,
  dividerBuilder: (context, state) => SplitDivider(
    state: state,
    lineThickness: 2.h,
    borderRadius: BorderRadius.circular(4.r),
  ),
  first: const Sidebar(),
  second: const Content(),
)

Zero Material

runApp(
  WidgetsApp(
    color: const Color(0xFFFFFFFF),
    builder: (context, _) => const SplitView(
      direction: SplitDirection.horizontal,
      first: Sidebar(),
      second: Content(),
    ),
  ),
);

API

See the Dartdoc on SplitView, SplitViewController, SplitDivider, SplitViewTheme, SplitDirection, SplitDividerStyle, and SplitDividerSize.

Performance notes

  • Fraction changes rebuild only the divider subtree, not the panes.
  • The layout pass is a single CustomMultiChildLayout traversal.
  • The controller is silent during geometry updates, avoiding rebuilds during layout.

⚠️ Important: Bounded Constraints

SplitView requires bounded constraints to calculate pane sizes. If you place it inside a Row or Column, you must wrap it in an Expanded, Flexible, or SizedBox to avoid layout exceptions.

// ✅ Good: Wrapped in Expanded
Row(
  children: [
    Expanded(child: SplitView(...)),
  ],
)

## License

MIT — see `LICENSE`.