This document outlines the architecture of the sliver_dashboard package. It is intended for developers who wish to contribute to the project or understand its internal workings.
The architecture is built on a foundation of modern, idiomatic Flutter principles:
- Declarative UI: The view layer is a direct representation of the state. We never manually manipulate widgets.
- Reactive State Management: State is centralized in a controller and exposed as reactive streams (
Beacons). The UI listens to these streams and rebuilds automatically. - Separation of Concerns: The codebase is cleanly divided into three distinct layers: State, Logic, and View.
- Performance First:
- Virtualization: The core view is built on Flutter's
Sliverprotocol to render only visible items. - Aggressive Caching: Individual item widgets are cached and protected from unnecessary rebuilds using a "Firewall" widget strategy.
- Paint Isolation: Use of
RepaintBoundaryensures that layout changes (moving an item) do not trigger expensive repaints of the item's content.
- Virtualization: The core view is built on Flutter's
- Immutability: State objects, particularly the
LayoutItemmodel, are immutable. - Accessibility (A11y): The dashboard is designed to be fully usable via keyboard and screen readers, treating accessibility as a first-class citizen, not an afterthought.
The package is divided into three main layers, each with a distinct responsibility.
graph TD
subgraph View Layer
A[Dashboard Widget] --> B[DashboardOverlay];
B --> C(CustomScrollView);
B -- "Gestures & Feedback" --> F[Feedback Stack];
B -- "Background" --> BG[DashboardGrid];
C -- "Focus Scope" --> D(SliverDashboard);
D --> E(RenderSliverDashboard);
E --> I["DashboardItem (Interaction Shell)"];
I --> K["FocusableActionDetector"];
K --> L["User Content (Cached & RepaintBoundary)"];
A -.-> MM[DashboardMinimap];
end
subgraph State Layer
M[DashboardController - Interface] --> N[DashboardControllerImpl]
N --> O["Beacons (State)"];
end
subgraph Logic Layer
P[LayoutEngine];
end
B -- "User Gestures (Drag/Resize)" --> M;
K -- "Keyboard Actions (Intents)" --> M;
N -- Updates State --> O;
O -- Notifies --> B;
O -- Notifies --> D;
N -- Calls Pure Functions --> P;
P -- Returns New Layout --> N;
style A fill:#cde4ff,color:#000000
style B fill:#dae8fc,color:#000000
style D fill:#d5e8d4,color:#000000
style M fill:#fff2cc,color:#000000
style P fill:#ffe6cc,color:#000000
- Location:
lib/src/controller/ - Responsibility: To be the single source of truth for the dashboard's state and to expose a clean, public API.
- Implementation:
- Interface Separation: The public
DashboardControlleris an abstract interface. The logic resides inDashboardControllerImpl. - Multi-Selection State: Manages
selectedItemIds(Set) andisDragging(bool). The concept of "Active Item" is derived: it is the Pivot during a drag, or the primary selection otherwise. - Drag Offset: Manages a
dragOffsetbeacon to provide smooth visual feedback during drags without committing every pixel change to the logical grid layout. - Orchestrator: It acts as a bridge. When an action occurs (e.g.,
onDragUpdateormoveActiveItemBy). It calculates the delta based on the Pivot Item and applies it to the entire cluster via the Engine.- Reads the current state.
- Calls the pure
LayoutEngine. - Updates the beacons with the result.
- Interface Separation: The public
- Location:
lib/src/engine/layout_engine.dart - Responsibility: To perform all pure, CPU-intensive layout calculations.
- Implementation:
- A library of top-level, pure functions (e.g.,
compact,moveElement,resizeItem). - Decoupled: Has no knowledge of Flutter widgets or the controller. Operates purely on the
LayoutItemdata model. - Deterministic: Given the same input layout and parameters, it always returns the same output layout.
- Cluster Logic: Handles group movements by calculating a Bounding Box for selected items. The engine moves this virtual box against obstacles and applies the resulting delta to all items in the cluster.
- Strategy Pattern: Compaction logic is delegated to a
CompactorDelegate. Default implementations (VerticalCompactor,HorizontalCompactor) are provided, but can be swapped at runtime.
- A library of top-level, pure functions (e.g.,
- Location:
lib/src/view/ - Responsibility: To render the state efficiently, handle user gestures, and manage focus/accessibility.
The view layer has been refactored to support native Sliver composition. It is composed of three key widgets:
- Role: Handles all pointer interactions (Gestures), visual feedback (Drag placeholders, Resize handles), Auto-scrolling, and the Trash bin.
- Placement: It must wrap the
CustomScrollView. - Logic:
- Global Key: Uses a unique
GlobalKeyon its internalStackto strictly identify the viewport boundaries for hit-testing and auto-scrolling. - Matrix Transformation: Uses
renderSliver.getTransformTo(overlay)to calculate the exact pixel position of the grid, ensuring perfect synchronization between the feedback item and the grid, even inside nested scrolling views. - Overlap-Aware Clipping: dynamically calculates a
ClipRectfor the feedback item that respectsSliverConstraints.overlap(e.g., sliding under a pinnedSliverAppBar).
- Global Key: Uses a unique
- Role: Renders the actual items within the scroll view using the Sliver protocol.
- Logic:
- Focus Scope (Parent): The parent
Dashboardwidget wraps theCustomScrollViewin aFocusTraversalGroupwithOrderedTraversalPolicyto ensure Tab navigation follows the visual grid logic (Row-major order). - Responsive Logic: Handles
breakpointsinternally using "Skip Frame" optimization. - Item Persistence: Unlike standard drag-and-drop lists, items being dragged are NOT removed from the tree. They are rendered with
Opacity(0.0). This is crucial to preserve theirFocusNodestate during keyboard interactions.
- Focus Scope (Parent): The parent
- Role: Implements
RenderSliverMultiBoxAdaptorto perform the actual layout and painting. - Virtualization: Only lays out and paints items that are currently visible in the viewport.
- Layout Protocol (Critical): The
performLayoutmethod manages a doubly linked list of children. It strictly follows this sequence to ensure stability:- Metrics: Calculate slot sizes based on constraints and aspect ratio.
- Garbage Collection: Remove invisible children before insertion to clear invalid references.
- Initial Child: Find and insert the first visible item based on scroll offset.
- Fill Trailing/Leading: Insert remaining visible items outwards from the initial child.
- Role: The atomic unit of the grid. It handles Caching, Focus, Accessibility, and Visual Decoration.
- Structure:
- Outer Shell:
FocusableActionDetectorhandling keyboard shortcuts and focus states. Rebuilt on state changes (Focus/Grab). - Inner Core: Cached User Content wrapped in
RepaintBoundary.
- Outer Shell:
DashboardItemWrapper:- Role: The final visual layer before the user's content.
- Logic: Adds visual decorations needed for editing, such as the Resize Handles.
- Integration: Wraps the content in a
GuidanceInteractorif guidance is enabled.
GuidanceInteractor:- Role: Handles contextual user guidance.
- Logic: Detects hover (desktop) and tap/long-press (mobile) events to display contextual guidance messages.
- Conflict Management: Manages gesture conflicts on mobile to ensure drag operations are not blocked.
- Role: Provides a "bird's-eye view" of the entire dashboard layout and the current viewport.
- Rendering: Uses a CustomPainter for high performance. It does not render widgets for items but draws rectangles directly on the canvas.
- Scaling: Automatically scales the logical grid dimensions to fit the widget's constraints while maintaining the aspect ratio.
- Interaction: Supports "Scrubbing" (Tap/Drag) to instantly scroll the dashboard to a specific position. It calculates the inverse ratio (Minimap Pixel -> Scroll Offset) to perform the jump.
The package implements a comprehensive A11y strategy based on Flutter's Actions and Intents.
- Intents: Abstract user intentions (
DashboardGrabItemIntent,DashboardMoveItemIntent,DashboardDropItemIntent). - Shortcuts: A configurable map binding keys to Intents (e.g.,
Space->Grab,Arrows->Move). This is customizable viaDashboardShortcuts. - Actions: The logic executed when an Intent is triggered. These call the Controller methods (
moveActiveItemBy,cancelInteraction). - Announcements: Integration with
SemanticsServiceto announce state changes (Selection, Movement coordinates) to screen readers. Messages are customizable viaDashboardGuidance.
The biggest challenge in a grid layout is preventing the reconstruction of child widgets when the parent layout changes (e.g., resizing the window or dragging an item). sliver_dashboard solves this using a Smart Caching strategy:
-
Content Isolation (The Firewall):
- The expensive part (the user's widget provided via
itemBuilder) is cached in a local state_cachedWidget. - Smart Invalidation: In
didUpdateWidget, the system compares thecontentSignatureof the new item vs. the old item.- Rule:
contentSignatureis a hash of properties that affect content (width, height, id, static status) and crucially ignores position changes (x,y).
- Rule:
- If the signature matches, the cached widget instance is returned. Flutter detects
oldWidget == newWidgetand stops the rebuild propagation immediately.
- The expensive part (the user's widget provided via
-
Lazy Loading:
- Rule: The cache is initialized lazily in the
build()method (notinitState). This ensures thatInheritedWidgets(likeThemeorProvider) are accessible during the first build, preventing runtime errors.
- Rule: The cache is initialized lazily in the
-
Shell Reconstruction:
- The "Interaction Shell" (border, focus detector, semantics) is rebuilt frequently (e.g., when gaining focus or being grabbed).
- Because the heavy user content is cached and wrapped in
RepaintBoundary, rebuilding the shell is extremely cheap (sub-millisecond).
-
RepaintBoundary:
- When an item moves, the cached widget tree includes a
RepaintBoundarywrapping the user's content. The GPU simply translates the existing texture without repainting the pixels of the child widget.
- When an item moves, the cached widget tree includes a
The system strictly separates logical grid coordinates from visual pixel coordinates to maintain precision.
- Engine: Operates strictly in Grid Coordinates (
int x, y). It never sees pixel values. - View: Handles translation to Pixel Coordinates (
double offset) usingSlotMetrics.
To support complex Sliver compositions (e.g., inside a CustomScrollView with SliverAppBar, SliverPadding, etc.), we do not rely on simple offset addition.
DashboardOverlayobtains the Transformation Matrix between theRenderSliverDashboardand the overlay root.- This accounts for scroll offsets, overlaps, and parent transforms precisely.
To prevent floating-point rounding errors and position "drift" during drag operations:
- The controller stores the
originalLayoutOnStartwhen a gesture begins. - Every
onDragUpdatecalculates the new position relative to this initial state. - The
dragOffsetbeacon handles the smooth visual translation (pixels) separately from the logical grid updates.
When an item is being dragged:
- Grid: The actual item stays in the tree but is made invisible (
Opacity 0) to keep its FocusNode alive. - Overlay: A visual copy (Feedback) is rendered in the
DashboardOverlaystack. - Clipping: The feedback item is clipped using a
ClipRectcalculated from the Sliver'soverlapconstraint. This ensures the item appears to slide "under" pinned headers like an AppBar, rather than floating over them.
When an item is being dragged:
- Grid: The actual items stay in the tree but are made invisible (
Opacity 0) to keep their FocusNodes alive. - Overlay (Cluster): The Overlay renders a
Stackcontaining visual copies of all selected items. They are positioned relative to the Pivot Item (the one under the cursor) to maintain their formation. - Synchronization: The overlay follows the finger/mouse, while the grid placeholder snaps to the nearest valid slot.
To efficiently render large grids (1000+ items) in a small widget:
- No Widgets: The minimap does not build a widget tree for items.
- Pure Painting: It iterates over the LayoutItem list and draws RRects on a single Canvas.
- Viewport Sync: It listens to the ScrollController to draw a "Viewport Indicator" that represents the currently visible area, updating at 60fps during scrolls.
sequenceDiagram
participant User
participant Overlay as DashboardOverlay
participant Controller
participant Engine as LayoutEngine
participant Sliver as SliverDashboard
User->>Overlay: Touch Down
Overlay->>Overlay: Hit Test (Find Item & Sliver)
Overlay->>Controller: onDragStart(id)
loop Dragging
User->>Overlay: Moves finger
Overlay->>Controller: onDragUpdate(offset)
Controller->>Engine: moveElement()
Engine-->>Controller: New Layout
Controller-->>Overlay: Drag Offset Beacon (Smooth)
Controller-->>Sliver: Layout Beacon (Grid Snap)
par Update Feedback
Overlay->>Overlay: Rebuild Feedback Item
and Update Grid
Sliver->>Sliver: performLayout (Move items)
end
alt Over Trash Area
Overlay->>Overlay: Detect Trash Hover
end
end
User->>Overlay: Touch Up (Drop)
alt Dropped on Armed Trash
Overlay->>Controller: removeItem(id)
else Dropped on Grid
Overlay->>Controller: onDragEnd()
Controller->>Engine: compact() (Finalize)
end