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.
Libraries
- voo_kanban
- A highly customizable Kanban board widget for Flutter.