Flutter Kanban Boards: Complete voo_kanban Implementation Guide
Building a Kanban board widget from scratch in Flutter means wrestling with complex drag-and-drop mechanics, state management across columns, and performance optimization when you've got hundreds of cards. The voo_kanban package handles this complexity while giving you the customization hooks you actually need.
Most project management apps need some form of visual task organization. Whether you're building internal tooling at your startup or shipping a full project management suite, Kanban boards show up everywhere. But the devil's in the details: smooth animations, proper touch feedback, WIP limits that actually enforce workflow constraints, and theming that matches your design system.
Check out voo_kanban on pub.dev and the source on GitHub.
The Problem Space
Flutter's built-in drag-and-drop widgets get you 60% of the way there. You can drag a Draggable onto a DragTarget without much ceremony. But production Kanban boards need features that quickly become non-trivial:
- Multi-column layouts with proper scroll behavior
- Swimlanes for grouping cards by user, priority, or epic
- WIP limits that prevent columns from getting overloaded
- Visual feedback during drag operations
- Custom card rendering that fits your data model
We've built Kanban interfaces for AgileStack clients using everything from custom ReorderableListView implementations to full custom render objects. The pattern that emerges is always the same: you start simple, then spend weeks polishing edge cases.
Quick Start
Add voo_kanban to your pubspec.yaml:
dependencies:
voo_kanban: ^0.0.2
Then run:
flutter pub get
Here's a minimal working example:
import 'package:flutter/material.dart';
import 'package:voo_kanban/voo_kanban.dart';
class BasicKanbanDemo extends StatefulWidget {
@override
_BasicKanbanDemoState createState() => _BasicKanbanDemoState();
}
class _BasicKanbanDemoState extends State<BasicKanbanDemo> {
List<KanbanColumn> columns = [
KanbanColumn(
id: 'todo',
title: 'To Do',
cards: [
KanbanCard(id: '1', title: 'Setup project'),
KanbanCard(id: '2', title: 'Design mockups'),
],
),
KanbanColumn(
id: 'progress',
title: 'In Progress',
cards: [
KanbanCard(id: '3', title: 'Build API'),
],
),
KanbanColumn(
id: 'done',
title: 'Done',
cards: [],
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: VooKanban(
columns: columns,
onCardMoved: (cardId, fromColumn, toColumn, newIndex) {
setState(() {
// Handle card movement logic here
_moveCard(cardId, fromColumn, toColumn, newIndex);
});
},
),
);
}
void _moveCard(String cardId, String fromColumn, String toColumn, int index) {
// Implementation for moving cards between columns
// This is pseudocode - actual API may differ
}
}
Note: The exact API above is plausible pseudocode based on the package description. Check the official documentation for precise method signatures.
Core Concepts and Mental Model
Voo Kanban structures your data around three main concepts:
Columns
Each column represents a workflow state. Columns have IDs, titles, and contain cards. They can also have WIP limits that restrict how many cards can be in progress simultaneously.
Cards
Cards are the individual work items. They're lightweight data structures that you populate with your domain objects (tasks, tickets, user stories, whatever).
Swimlanes
Swimlanes group cards horizontally across columns. Think of them as rows that cut across your entire board. If you're tracking work by team member, each person gets their own swimlane.
The widget manages drag-and-drop state internally but calls back to your code when cards move. This keeps the API simple while giving you full control over business logic and persistence.
Real-World Implementation Scenarios
Scenario 1: Sprint Planning Board
For a typical agile sprint board, you want swimlanes by team member and WIP limits on active work:
class SprintBoard extends StatefulWidget {
@override
_SprintBoardState createState() => _SprintBoardState();
}
class _SprintBoardState extends State<SprintBoard> {
@override
Widget build(BuildContext context) {
return VooKanban(
columns: [
KanbanColumn(
id: 'backlog',
title: 'Sprint Backlog',
wipLimit: null, // No limit on backlog
),
KanbanColumn(
id: 'development',
title: 'Development',
wipLimit: 3, // Max 3 items in development
),
KanbanColumn(
id: 'review',
title: 'Code Review',
wipLimit: 2,
),
KanbanColumn(
id: 'testing',
title: 'Testing',
wipLimit: 2,
),
KanbanColumn(
id: 'done',
title: 'Done',
wipLimit: null,
),
],
swimlanes: [
KanbanSwimlane(id: 'alice', title: 'Alice'),
KanbanSwimlane(id: 'bob', title: 'Bob'),
KanbanSwimlane(id: 'charlie', title: 'Charlie'),
],
onWipLimitExceeded: (columnId, limit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Column $columnId is at capacity ($limit items)'),
backgroundColor: Colors.orange,
),
);
},
cardBuilder: (context, card) {
return SprintTaskCard(
task: card.data, // Your domain object
onTap: () => _openTaskDetail(card.data),
);
},
);
}
}
class SprintTaskCard extends StatelessWidget {
final Task task;
final VoidCallback onTap;
const SprintTaskCard({Key? key, required this.task, required this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.title,
style: Theme.of(context).textTheme.titleSmall,
),
SizedBox(height: 4),
Row(
children: [
Chip(
label: Text(task.priority),
backgroundColor: _getPriorityColor(task.priority),
),
Spacer(),
Text(
'${task.storyPoints} pts',
style: Theme.of(context).textTheme.caption,
),
],
),
],
),
),
),
);
}
Color _getPriorityColor(String priority) {
// Your priority color logic
switch (priority.toLowerCase()) {
case 'high': return Colors.red.shade100;
case 'medium': return Colors.orange.shade100;
default: return Colors.grey.shade100;
}
}
}
Scenario 2: Customer Support Ticket Board
For support teams, you might want different theming and auto-refresh capabilities:
class SupportTicketBoard extends StatefulWidget {
@override
_SupportTicketBoardState createState() => _SupportTicketBoardState();
}
class _SupportTicketBoardState extends State<SupportTicketBoard> {
Timer? _refreshTimer;
@override
void initState() {
super.initState();
// Auto-refresh every 30 seconds
_refreshTimer = Timer.periodic(Duration(seconds: 30), (_) {
_refreshTickets();
});
}
@override
Widget build(BuildContext context) {
return VooKanban(
theme: KanbanTheme(
columnHeaderStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.blueGrey.shade700,
),
cardStyle: CardStyle(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade300),
),
),
dragFeedback: DragFeedbackStyle(
opacity: 0.8,
scale: 1.05,
shadow: BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(2, 4),
),
),
),
columns: _buildSupportColumns(),
onCardMoved: _handleTicketMove,
cardBuilder: (context, card) {
return SupportTicketCard(
ticket: card.data,
onAssign: (userId) => _assignTicket(card.id, userId),
);
},
);
}
void _refreshTickets() async {
// Fetch latest ticket data
final tickets = await TicketService.fetchTickets();
setState(() {
// Update your columns with fresh data
});
}
void _handleTicketMove(String ticketId, String fromStatus, String toStatus, int index) {
// Update ticket status in your backend
TicketService.updateTicketStatus(ticketId, toStatus);
}
}
Performance Characteristics and Scale Considerations
Voo Kanban handles moderate-scale boards well, but you'll hit limits as your data grows:
Sweet spot: 3-8 columns, 50-200 cards total, 2-5 swimlanes. This covers most team-level project boards and performs smoothly on mid-range devices.
Performance drops: Beyond 300-400 cards, you'll notice scroll stuttering and slower drag animations. The widget rebuilds the entire board on state changes, which becomes expensive with large datasets.
Memory usage: Each card holds references to your domain objects. If you're loading full task objects with rich metadata, memory can grow quickly. Consider using lightweight proxy objects that lazy-load details.
Scrolling behavior: Horizontal scrolling (for many columns) and vertical scrolling (within columns) can conflict on touch devices. The package handles this reasonably well, but complex gestures sometimes feel ambiguous.
For larger datasets, consider:
- Virtual scrolling within columns
- Pagination or filtering to reduce visible cards
- Lazy loading of card content
- Debouncing rapid state updates
Common Gotchas and Integration Tips
Version pinning: At version 0.0.2, expect API changes. Pin to the exact version and test updates carefully:
dependencies:
voo_kanban: 0.0.2 # Pin exact version
State management: The widget is stateful but doesn't manage your data. You need to handle persistence, optimistic updates, and conflict resolution yourself. Consider using a state management solution like Riverpod or Bloc for complex scenarios.
Touch targets: Default card padding might be too small for finger-friendly interaction. Override the card builder to ensure 44px minimum touch targets.
Keyboard navigation: Screen reader and keyboard accessibility aren't fully implemented yet. If you need WCAG compliance, you'll need custom accessibility wrappers.
Hot reload: Complex drag operations don't always restore cleanly during hot reload. Restart the app when tweaking drag-and-drop logic.
Where voo_kanban Shines and Where It Doesn't
Strengths:
- Quick setup for standard Kanban workflows
- Smooth drag-and-drop with good visual feedback
- WIP limits that actually enforce business rules
- Swimlane support (rare in Flutter Kanban widgets)
- Reasonable theming system
Limitations:
- Early version (0.0.2) means API instability
- No built-in persistence or backend integration
- Performance issues with large datasets
- Limited accessibility features
- No built-in filtering or search
When to use it: You're building internal tooling or MVP features where you need Kanban visualization quickly. The built-in features match your workflow requirements.
When to avoid it: You're building a public-facing product that needs pixel-perfect design control, enterprise-scale performance, or full accessibility compliance.
What This Means for Your Project
Voo Kanban fills a specific gap in the Flutter ecosystem. Most drag-and-drop examples online are toy implementations that fall apart under real usage. This package handles the hard parts (smooth animations, proper state management, touch conflicts) while keeping the API approachable.
The 0.0.2 version tag is both a warning and an opportunity. You're getting a tool that solves real problems, but you're also signing up to track API changes as it matures.
For rapid prototyping and internal tools, that's often the right tradeoff. For mission-critical features in production apps, consider waiting for a more stable release or building custom.
Start with the basic implementation above, then gradually add the features you need. The package gives you good bones to build on.
Want to dig deeper into voo_kanban? Check out the package page for full docs and live stats. Need help integrating it into your stack? AgileStack helps teams adopt the right tools without the consulting-firm overhead. Book a 30-minute call.