voo_kanban 0.0.2
voo_kanban: ^0.0.2 copied to clipboard
A highly customizable Kanban board widget for Flutter with drag-and-drop, swimlanes, WIP limits, and extensive theming
VooKanban #
A highly customizable Kanban board widget for Flutter with drag-and-drop, swimlanes, WIP limits, and extensive theming.
Features #
- Generic Card Support: Type-safe cards with arbitrary data
<T> - Drag-and-Drop: Move cards within and between lanes with smooth animations
- Swimlanes: Horizontal groupings with filter functions and collapsible rows
- WIP Limits: Visual indicators when approaching or exceeding lane limits
- Selection: Single and multi-select modes with Ctrl/Cmd+Click support
- Keyboard Navigation: Arrow keys, Enter, Escape for accessibility
- Undo/Redo: Full history stack with Ctrl+Z / Ctrl+Y shortcuts
- Theming: Material 3 integration with light/dark presets and full customization
- Responsive: Tab-based mobile layout, horizontal scrolling desktop layout
- Serialization:
toJson()/fromJson()for state persistence
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
voo_kanban: ^0.0.2
Then run:
flutter pub get
Quick Start #
import 'package:flutter/material.dart';
import 'package:voo_kanban/voo_kanban.dart';
class MyKanbanBoard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VooKanbanBoard<String>(
lanes: [
KanbanLane(
id: 'todo',
title: 'To Do',
cards: [
KanbanCard(id: '1', data: 'Task 1', laneId: 'todo', index: 0),
KanbanCard(id: '2', data: 'Task 2', laneId: 'todo', index: 1),
],
),
KanbanLane(
id: 'doing',
title: 'In Progress',
wipLimit: 3,
),
KanbanLane(
id: 'done',
title: 'Done',
),
],
cardBuilder: (context, card, isSelected) {
return Card(
color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(card.data),
),
);
},
onCardMoved: (card, fromLane, toLane, index) {
print('Moved ${card.data} from $fromLane to $toLane');
},
);
}
}
Usage #
Basic Board with Custom Data #
// Define your task model
class Task {
final String title;
final String description;
final Priority priority;
const Task({
required this.title,
required this.description,
required this.priority,
});
}
enum Priority { low, medium, high }
// Create the board
VooKanbanBoard<Task>(
lanes: [
KanbanLane<Task>(
id: 'backlog',
title: 'Backlog',
icon: Icons.inbox,
cards: [
KanbanCard<Task>(
id: 'task-1',
data: Task(
title: 'Research competitors',
description: 'Analyze market landscape',
priority: Priority.low,
),
laneId: 'backlog',
index: 0,
),
],
),
KanbanLane<Task>(
id: 'in-progress',
title: 'In Progress',
icon: Icons.pending_actions,
color: Colors.blue,
wipLimit: 3,
),
KanbanLane<Task>(
id: 'done',
title: 'Done',
icon: Icons.check_circle,
color: Colors.green,
),
],
cardBuilder: (context, card, isSelected) => TaskCard(task: card.data),
onCardMoved: (card, fromLane, toLane, index) {
// Handle card movement
},
onCardTap: (card) {
// Handle card tap
},
)
Using KanbanController #
For full control over board state, use KanbanController:
class _MyBoardState extends State<MyBoard> {
late KanbanController<Task> _controller;
@override
void initState() {
super.initState();
_controller = KanbanController<Task>(
initialState: KanbanState<Task>(
lanes: _createLanes(),
config: const KanbanConfig(
enableUndo: true,
enableKeyboardNavigation: true,
showCardCount: true,
showWipLimitIndicators: true,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _controller,
builder: (context, _) {
return Column(
children: [
// Undo/Redo toolbar
Row(
children: [
IconButton(
icon: const Icon(Icons.undo),
onPressed: _controller.canUndo ? _controller.undo : null,
),
IconButton(
icon: const Icon(Icons.redo),
onPressed: _controller.canRedo ? _controller.redo : null,
),
],
),
Expanded(
child: VooKanbanBoard<Task>(
controller: _controller,
lanes: _controller.lanes,
cardBuilder: _buildCard,
),
),
],
);
},
);
}
}
Controller Operations #
// Move a card
_controller.moveCard(
cardId: 'task-1',
toLaneId: 'in-progress',
toIndex: 0,
);
// Add a card
_controller.addCard(KanbanCard<Task>(
id: 'new-task',
data: Task(title: 'New Task', ...),
laneId: 'backlog',
));
// Remove a card
_controller.removeCard('task-1');
// Selection
_controller.selectCard('task-1');
_controller.selectCard('task-2', addToSelection: true);
_controller.clearSelection();
// Undo/Redo
_controller.undo();
_controller.redo();
// Serialization
final json = _controller.toJson(dataToJson: (task) => task.toJson());
_controller.fromJson(json, dataFromJson: Task.fromJson);
Swimlanes #
Group cards horizontally using swimlanes with filter functions:
VooKanbanBoard<Task>(
lanes: _lanes,
swimlanes: [
KanbanSwimlane<Task>(
id: 'high-priority',
title: 'High Priority',
filter: (card) => card.data.priority == Priority.high,
),
KanbanSwimlane<Task>(
id: 'medium-priority',
title: 'Medium Priority',
filter: (card) => card.data.priority == Priority.medium,
),
KanbanSwimlane<Task>(
id: 'low-priority',
title: 'Low Priority',
filter: (card) => card.data.priority == Priority.low,
),
],
swimlaneHeaderBuilder: (context, swimlane) {
return Text(swimlane.title, style: Theme.of(context).textTheme.titleMedium);
},
)
Theming #
VooKanban integrates with Material 3 and provides extensive theming options:
// Automatic Material 3 theming
VooKanbanBoard<Task>(
theme: KanbanTheme.fromContext(context),
// ...
)
// Light/Dark presets
VooKanbanBoard<Task>(
theme: KanbanTheme.light(),
// or
theme: KanbanTheme.dark(),
)
// Custom theme
VooKanbanBoard<Task>(
theme: KanbanTheme.fromContext(context).copyWith(
boardBackgroundColor: Colors.grey[100],
laneBackgroundColor: Colors.white,
cardBackgroundColor: Colors.white,
wipWarningColor: Colors.orange,
wipExceededColor: Colors.red,
laneBorderRadius: BorderRadius.circular(16),
cardBorderRadius: BorderRadius.circular(12),
),
)
Configuration Options #
const KanbanConfig(
// Drag behavior
dragMode: DragMode.betweenLanes, // or DragMode.withinLane, DragMode.disabled
// Selection
selectionMode: SelectionMode.single, // or SelectionMode.multiple
// Lane behavior
allowLaneReorder: true,
allowSwimlaneReorder: false,
// Undo/Redo
enableUndo: true,
maxUndoSteps: 50,
// Navigation
enableKeyboardNavigation: true,
// Visual options
showWipLimitIndicators: true,
showCardCount: true,
enableAnimations: true,
animationDuration: Duration(milliseconds: 300),
// Layout
defaultLaneWidth: 300,
minLaneWidth: 250,
maxLaneWidth: 400,
laneSpacing: 12,
cardSpacing: 8,
)
WIP Limits #
Configure Work-In-Progress limits per lane:
KanbanLane<Task>(
id: 'in-progress',
title: 'In Progress',
wipLimit: 3, // Maximum 3 cards
cards: [...],
)
// Handle WIP limit exceeded
VooKanbanBoard<Task>(
onWipLimitExceeded: (lane, currentCount) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('WIP limit exceeded: $currentCount/${lane.wipLimit}'),
backgroundColor: Colors.orange,
),
);
},
)
Custom Builders #
VooKanbanBoard<Task>(
// Custom card rendering
cardBuilder: (context, card, isSelected) {
return TaskCardWidget(task: card.data, isSelected: isSelected);
},
// Custom lane header
laneHeaderBuilder: (context, lane) {
return Row(
children: [
Icon(lane.icon),
Text(lane.title),
Chip(label: Text('${lane.cardCount}')),
],
);
},
// Custom lane footer
laneFooterBuilder: (context, lane) {
return TextButton(
onPressed: () => _addCard(lane.id),
child: const Text('+ Add Card'),
);
},
// Empty lane placeholder
emptyLaneBuilder: (context, lane) {
return Center(
child: Text('Drop cards here', style: TextStyle(color: Colors.grey)),
);
},
)
Callbacks #
VooKanbanBoard<Task>(
// Card moved between lanes or reordered
onCardMoved: (card, fromLaneId, toLaneId, newIndex) { },
// Card tapped
onCardTap: (card) { },
// Card long pressed
onCardLongPress: (card) { },
// Selection changed
onCardsSelected: (selectedIds) { },
// Lanes reordered
onLaneReordered: (oldIndex, newIndex) { },
// WIP limit exceeded
onWipLimitExceeded: (lane, currentCount) { },
// Undo/Redo triggered
onUndo: () { },
onRedo: () { },
)
Keyboard Shortcuts #
| Shortcut | Action |
|---|---|
| Arrow Up/Down | Navigate between cards |
| Enter/Space | Select focused card |
| Escape | Clear selection and focus |
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Shift + Z | Redo |
| Ctrl/Cmd + Y | Redo |
| Ctrl/Cmd + Click | Multi-select cards |
Serialization #
Save and restore board state:
// Save state
final json = controller.toJson(
dataToJson: (task) => {
'title': task.title,
'description': task.description,
'priority': task.priority.name,
},
);
// Restore state
controller.fromJson(
json,
dataFromJson: (data) => Task(
title: data['title'],
description: data['description'],
priority: Priority.values.byName(data['priority']),
),
);
Platform Support #
| Platform | Support |
|---|---|
| Android | Yes |
| iOS | Yes |
| Web | Yes |
| macOS | Yes |
| Windows | Yes |
| Linux | Yes |
Dependencies #
voo_ui_core: ^0.1.3voo_responsive: ^0.1.4voo_tokens: ^0.0.8voo_motion: ^0.0.2equatable: ^2.0.5
Example App #
Check out the example app for comprehensive demonstrations:
cd example
flutter run
The example includes:
- Basic board setup
- Drag and drop interactions
- Swimlane grouping
- Theming customization
License #
This package is part of the VooFlutter monorepo. See the root LICENSE file for details.
Built by VooStack #
Need help with Flutter development or custom Kanban solutions?
VooStack builds enterprise Flutter applications and developer tools. We're here to help with your next project.