skip to Main Content

I’m trying to create a Kanban application using Flutter to manage tasks and projects. I would like to know how to implement a Kanban board with columns such as "To Do", "In Progress", and "Done" and allow users to reorder tasks by dragging and dropping between columns. I’ve started a Flutter project, but I need a guide or code example to implement this feature. Can anyone provide guidance or code examples? Thank you very much!

3

Answers


  1. There’s a specific package for this named: kanban_board

    It is a customizable kanban board, which can be used to reorder items and list with drag and drop.

    Login or Signup to reply.
  2. See the video link below. The video explains how to implement Kanban in Flutter.

    video link

    Login or Signup to reply.
  3. I’ve implemented the kanban as follows.

    Note: I am using Getx for Routing and state management. and I am not using any package for kanban.
    

    Following is my code snippet with GIF recording.

    enter image description here

    import 'package:adaptive_dialog/adaptive_dialog.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_svg/svg.dart';
    import 'package:get/get.dart';
    import 'package:thrivebay/Constants/colors.dart';
    import 'package:thrivebay/Controllers/Kanban/kanban_cntrlr.dart';
    import 'package:thrivebay/Controllers/Kanban/tasks_controller.dart';
    import 'package:thrivebay/Models/Kanban/kanban.dart';
    import 'package:thrivebay/Utils/colors_utils.dart';
    import 'package:thrivebay/Utils/datetime.dart';
    import 'package:thrivebay/Utils/overlays.dart';
    
    import '../../Data/Firestore/kanban.dart';
    import '../../Models/Kanban/task.dart';
    import '../Widgets/widgets.dart';
    import 'add_task.dart';
    import 'add_update_project.dart';
    
    class KanbanDetailsView extends StatefulWidget {
      const KanbanDetailsView({Key? key, required this.projectId}) : super(key: key);
      final String projectId;
    
      @override
      State<KanbanDetailsView> createState() => _KanbanDetailsViewState();
    }
    
    class _KanbanDetailsViewState extends State<KanbanDetailsView> {
      List<String> testList = [];
    
      ScrollController scrollController = ScrollController();
    
      @override
      void initState() {
        Get.put(KanbanTasksController(projectId: widget.projectId));
        super.initState();
      }
    
      int? currentTaskStatusIndex;
      Kanban? project;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: AppColors.darkBackgroundVariant,
          appBar: AppBar(
            leading: const KBackButton(),
            backgroundColor: AppColors.darkPrimary,
            title: const Text('Project Detail'),
            actions: [
              PopupMenuButton<String>(
                elevation: 1,
                shadowColor: AppColors.white,
                child: SizedBox(
                  height: 25,
                  width: 40,
                  child: SvgPicture.asset('assets/icons/more.svg', color: AppColors.white),
                ),
                itemBuilder: (context) => [
                  PopupMenuItem<String>(
                    value: 'edit',
                    child: Row(
                      children: [
                        SvgPicture.asset('assets/icons/edit.svg'),
                        const SizedBox(width: 12),
                        const Text('Edit Project'),
                      ],
                    ),
                  ),
                  const PopupMenuDivider(),
                  PopupMenuItem<String>(
                    value: 'delete',
                    child: Row(
                      children: [
                        SvgPicture.asset('assets/icons/delete.svg'),
                        const SizedBox(width: 12),
                        const Text('Delete Project'),
                      ],
                    ),
                  ),
                ],
                onSelected: (value) {
                  if (value == 'edit') {
                    Get.to(() => AddUpdateProject(project: project));
                  } else if (value == 'delete') {
                    showOkCancelAlertDialog(
                      context: context,
                      title: 'Delete Project?',
                      message: 'Are you sure you want to delete this project?',
                      okLabel: 'Delete',
                      isDestructiveAction: true,
                    ).then((value) async {
                      if (value == OkCancelResult.ok) {
                        await kOverlayWithAsync(asyncFunction: () async {
                          await KanbanFirestore.deleteProject(widget.projectId);
                        });
                        Get.back();
                      }
                    });
                  }
                },
              ),
            ],
          ),
          body: Column(
            children: [
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: GetBuilder<KanbanController>(
                  builder: (cntrlr) {
                    project = cntrlr.projects.firstWhereOrNull((element) => element.projectId == widget.projectId);
                    if (project == null) {
                      return const Center(child: CircularProgressIndicator());
                    } else {
                      return Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            project!.title,
                            style: Theme.of(context).textTheme.titleLarge,
                          ),
                          const SizedBox(height: 10),
                          Text(
                            project!.description,
                            maxLines: 4,
                            overflow: TextOverflow.ellipsis,
                            style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: AppColors.darkLight),
                          ),
                          const SizedBox(height: 20),
                          KDetailInputRow(
                            iconPath: 'assets/icons/clock.svg',
                            lable: 'Created time',
                            child: Text(
                              project!.createdAt!.formattedDateTime,
                              style: Theme.of(context).textTheme.titleSmall,
                            ),
                          ),
                          const SizedBox(height: 12),
                          KDetailInputRow(
                            iconPath: 'assets/icons/date.svg',
                            lable: 'Due date',
                            child: Text(
                              project?.dueDate != null ? project!.dueDate!.formattedDateTime : 'Continuous',
                              style: Theme.of(context).textTheme.titleSmall,
                            ),
                          ),
                          const SizedBox(height: 12),
                          KDetailInputRow(
                            iconPath: 'assets/icons/progress.svg',
                            lable: 'Progress',
                            child: Row(
                              children: [
                                Expanded(
                                    child: KProgressWidget(color: project!.theme.toColor, progress: project!.progress)),
                                const SizedBox(width: 12),
                                Text('${project?.progress.ceil()}%', style: Theme.of(context).textTheme.titleMedium),
                              ],
                            ),
                          ),
                          const SizedBox(height: 12),
                          KDetailInputRow(
                            iconPath: 'assets/icons/priority.svg',
                            lable: 'Priority',
                            child: Text(
                              project!.priority?.lable ?? 'None',
                              style: Theme.of(context).textTheme.titleSmall!.copyWith(
                                  color:
                                      project!.priority?.lable != null ? project!.priority!.color : AppColors.darkAccent),
                            ),
                          ),
                          const SizedBox(height: 12),
                          KDetailInputRow(
                            iconPath: 'assets/icons/theme.svg',
                            lable: 'Theme',
                            child: Align(
                              alignment: Alignment.centerLeft,
                              child: CircleAvatar(
                                radius: 12,
                                backgroundColor: project!.theme.toColor.lighter,
                                child: Padding(
                                  padding: const EdgeInsets.symmetric(horizontal: 2),
                                  child: CircleAvatar(backgroundColor: project!.theme.toColor),
                                ),
                              ),
                            ),
                          ),
                        ],
                      );
                    }
                  },
                ),
              ),
              const Divider(),
              Expanded(
                child: SingleChildScrollView(
                  controller: scrollController,
                  scrollDirection: Axis.horizontal,
                  child: GetBuilder<KanbanTasksController>(builder: (cntrlr) {
                    print(cntrlr.tasks.length);
                    return Row(
                      children: List.generate(
                        taskTitles.length,
                        (index) {
                          List<KanbanTask> tasks =
                              cntrlr.tasks.where((element) => element.status == taskTitles[index]).toList();
                          return DragTarget(
                            onAccept: (details) {},
                            onAcceptWithDetails: (details) {
                              cntrlr.changeTaskStatus((details.data as String?)!, taskTitles[index]);
                              setState(() {
                                currentTaskStatusIndex = null;
                              });
                            },
                            onLeave: (details) {
                              setState(() {
                                currentTaskStatusIndex = null;
                              });
                            },
                            onMove: (details) {
                              if (currentTaskStatusIndex != index) {
                                setState(() {
                                  currentTaskStatusIndex = index;
                                });
                              }
                            },
                            onWillAccept: (details) {
                              return true;
                            },
                            builder: (BuildContext context, List<Object?> candidateData, List<dynamic> rejectedData) {
                              return Container(
                                margin: const EdgeInsets.all(12),
                                width: Get.width * 0.7,
                                decoration: BoxDecoration(
                                  color: currentTaskStatusIndex == index ? AppColors.darkSurface : AppColors.darkBackground,
                                  borderRadius: BorderRadius.circular(12),
                                ),
                                child: Column(
                                  mainAxisAlignment: MainAxisAlignment.start,
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  mainAxisSize: MainAxisSize.min,
                                  children: [
                                    ListTile(
                                      contentPadding: const EdgeInsets.fromLTRB(15, 0, 10, 0),
                                      title: Text(taskTitles[index].lableWithEmoji!),
                                      trailing: PopupMenuButton<String>(
                                        itemBuilder: (context) {
                                          return [
                                            PopupMenuItem<String>(
                                              value: 'add',
                                              child: Row(
                                                children: [
                                                  SvgPicture.asset('assets/icons/add.svg'),
                                                  const SizedBox(width: 12),
                                                  const Text('Add a task'),
                                                ],
                                              ),
                                            ),
                                            const PopupMenuDivider(),
                                            PopupMenuItem<String>(
                                              value: 'delete',
                                              child: Row(
                                                children: [
                                                  SvgPicture.asset('assets/icons/delete.svg'),
                                                  const SizedBox(width: 12),
                                                  const Text('Delete All'),
                                                ],
                                              ),
                                            ),
                                          ];
                                        },
                                        onSelected: (value) {
                                          if (value == 'add') {
                                            Get.to(() =>
                                                TaskCardDetailView(status: taskTitles[index], projectId: widget.projectId));
                                          } else if (value == 'delete') {}
                                        },
                                      ),
                                    ),
                                    const Divider(height: 1, color: AppColors.darkBackgroundSecondary),
                                    Expanded(
                                      child: MediaQuery.removePadding(
                                        context: context,
                                        removeBottom: true,
                                        removeTop: true,
                                        child: ListView.separated(
                                          separatorBuilder: (context, index) => const SizedBox(height: 0),
                                          shrinkWrap: true,
                                          itemCount: tasks.length,
                                          itemBuilder: (BuildContext context, int index) {
                                            final task = tasks[index];
                                            return Listener(
                                              onPointerMove: (PointerMoveEvent event) {
                                                if (event.delta.dx != 0) {
                                                  scrollController.jumpTo(scrollController.offset + event.delta.dx * 2.5);
                                                }
                                              },
                                              child: LongPressDraggable(
                                                data: task.id!,
                                                feedback: Transform.rotate(
                                                  angle: 0.05,
                                                  child: Material(
                                                    type: MaterialType.transparency,
                                                    child: KTaskCard(task: task, widget: widget),
                                                  ),
                                                ),
                                                childWhenDragging: Opacity(
                                                  opacity: 0.6,
                                                  child: KTaskCard(task: task, widget: widget),
                                                ),
                                                child: KTaskCard(
                                                  task: task,
                                                  widget: widget,
                                                  onDone: () {
                                                    cntrlr.changeTaskStatus(task.id!, TaskStatus.done);
                                                  },
                                                ),
                                              ),
                                            );
                                          },
                                        ),
                                      ),
                                    ),
                                    ListTile(
                                      leading: const Icon(Icons.add),
                                      title: const Text('Add a task'),
                                      splashColor: AppColors.darkPrimary,
                                      onTap: () {
                                        Get.to(
                                          () => TaskCardDetailView(status: taskTitles[index], projectId: widget.projectId),
                                        );
                                      },
                                    ),
                                  ],
                                ),
                              );
                            },
                          );
                        },
                      ),
                    );
                  }),
                ),
              )
            ],
          ),
        );
      }
    }
    
    class KTaskCard extends StatelessWidget {
      const KTaskCard({
        super.key,
        required this.task,
        required this.widget,
        this.onDone,
      });
    
      final KanbanTask task;
      final KanbanDetailsView widget;
      final void Function()? onDone;
      // final TaskStatus status;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          width: Get.width * 0.7,
          margin: const EdgeInsets.fromLTRB(10, 8, 10, 0),
          // padding: const EdgeInsets.all(8.0),
          decoration: BoxDecoration(
            color: AppColors.darkBackgroundSecondary,
            borderRadius: BorderRadius.circular(8),
          ),
          child: ListTile(
            visualDensity: const VisualDensity(vertical: -4),
            contentPadding: const EdgeInsets.only(left: 15),
            title: Text(
              task.title,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              style: Theme.of(context).textTheme.bodySmall!.copyWith(color: AppColors.white),
            ),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (task.status != TaskStatus.done)
                  IconButton(
                    onPressed: onDone,
                    icon: const Icon(Icons.check),
                  ),
                IconButton(
                  onPressed: () {
                    Get.to(() => TaskCardDetailView(status: task.status, projectId: widget.projectId, task: task));
                  },
                  icon: SvgPicture.asset('assets/icons/edit.svg'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    List<TaskStatus> taskTitles = [
      TaskStatus.todo,
      TaskStatus.inProgress,
      TaskStatus.blocked,
      TaskStatus.done,
    ];
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search