voo_kanban 0.0.2 copy "voo_kanban: ^0.0.2" to clipboard
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.

pub package style: flutter_lints

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.3
  • voo_responsive: ^0.1.4
  • voo_tokens: ^0.0.8
  • voo_motion: ^0.0.2
  • equatable: ^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?

Contact Us

VooStack builds enterprise Flutter applications and developer tools. We're here to help with your next project.

1
likes
160
points
174
downloads

Publisher

verified publishervoostack.com

Weekly Downloads

A highly customizable Kanban board widget for Flutter with drag-and-drop, swimlanes, WIP limits, and extensive theming

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #kanban #board #drag-drop #widget

Documentation

API reference

License

MIT (license)

Dependencies

equatable, flutter, voo_motion, voo_responsive, voo_tokens, voo_ui_core

More

Packages that depend on voo_kanban