← Назад к вопросам
Реализовать Drag and Drop список
2.0 Middle🔥 91 комментариев
#Flutter виджеты#State Management#Анимации
Условие
Создайте список с возможностью перетаскивания элементов для изменения порядка.
Требования
- Список элементов с возможностью изменения порядка перетаскиванием
- Визуальная обратная связь при перетаскивании (поднятие элемента, изменение opacity)
- Анимация при перемещении элементов
- Сохранение нового порядка
Дополнительные баллы
- Удаление элемента свайпом
- Группировка элементов с перетаскиванием между группами
- Haptic feedback при перемещении
- Undo последнего действия
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Flutter Drag and Drop список с анимацией
Представляю полное решение с перетаскиванием, удалением свайпом, haptic feedback и функцией undo.
1. Модель данных (lib/models/task_model.dart)
import "package:freezed_annotation/freezed_annotation.dart";
import "package:uuid/uuid.dart";
part "task_model.freezed.dart";
part "task_model.g.dart";
@freezed
class Task with _\$Task {
const factory Task({
@Default("") String id,
required String title,
required String description,
@Default(false) bool isCompleted,
}) = _Task;
factory Task.fromJson(Map<String, dynamic> json) => _\$TaskFromJson(json);
factory Task.create(String title, String description) {
return Task(
id: const Uuid().v4(),
title: title,
description: description,
);
}
}
2. Провайдеры (lib/providers/task_providers.dart)
import "package:flutter_riverpod/flutter_riverpod.dart";
import "../models/task_model.dart";
final tasksProvider = StateNotifierProvider<TasksNotifier, List<Task>>((ref) {
return TasksNotifier([
Task.create("Купить молоко", "В супермаркете"),
Task.create("Написать письмо", "Отправить партнёру"),
Task.create("Встреча в 3 часа", "В офисе на 5 этаже"),
Task.create("Принять посылку", "Может приехать курьер"),
Task.create("Позвонить маме", "Не забыть!"),
]);
});
final taskHistoryProvider = StateNotifierProvider<HistoryNotifier, List<List<Task>>>((ref) {
return HistoryNotifier([]);
});
class TasksNotifier extends StateNotifier<List<Task>> {
TasksNotifier(List<Task> initialTasks) : super(initialTasks);
void reorderTasks(int oldIndex, int newIndex) {
final tasks = [...state];
if (oldIndex < newIndex) {
newIndex -= 1;
}
final task = tasks.removeAt(oldIndex);
tasks.insert(newIndex, task);
state = tasks;
}
void removeTask(int index) {
final tasks = [...state];
tasks.removeAt(index);
state = tasks;
}
void toggleTask(int index) {
final tasks = [...state];
tasks[index] = tasks[index].copyWith(
isCompleted: !tasks[index].isCompleted,
);
state = tasks;
}
void addTask(Task task) {
state = [...state, task];
}
}
class HistoryNotifier extends StateNotifier<List<List<Task>>> {
HistoryNotifier(List<List<Task>> initialHistory) : super(initialHistory);
void addSnapshot(List<Task> tasks) {
state = [...state, tasks];
// Ограничиваем историю 10 снимками
if (state.length > 10) {
state = state.sublist(state.length - 10);
}
}
void clear() {
state = [];
}
}
3. Виджет элемента списка (lib/widgets/draggable_task_item.dart)
import "package:flutter/material.dart";
import "../models/task_model.dart";
class DraggableTaskItem extends StatefulWidget {
final Task task;
final int index;
final VoidCallback? onDismissed;
final ValueChanged<bool>? onToggle;
final bool isDragging;
const DraggableTaskItem({
required this.task,
required this.index,
this.onDismissed,
this.onToggle,
this.isDragging = false,
Key? key,
}) : super(key: key);
@override
State<DraggableTaskItem> createState() => _DraggableTaskItemState();
}
class _DraggableTaskItemState extends State<DraggableTaskItem>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
if (widget.isDragging) {
_animationController.forward();
}
}
@override
void didUpdateWidget(DraggableTaskItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isDragging != widget.isDragging) {
if (widget.isDragging) {
_animationController.forward();
} else {
_animationController.reverse();
}
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: Opacity(
opacity: widget.isDragging ? 0.5 : 1.0,
child: Dismissible(
key: Key(widget.task.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => widget.onDismissed?.call(),
child: Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
elevation: widget.isDragging ? 8 : 2,
child: ListTile(
leading: Checkbox(
value: widget.task.isCompleted,
onChanged: (_) => widget.onToggle?.call(!widget.task.isCompleted),
),
title: Text(
widget.task.title,
style: TextStyle(
decoration: widget.task.isCompleted
? TextDecoration.lineThrough
: null,
),
),
subtitle: Text(
widget.task.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Icon(
Icons.drag_handle,
color: Colors.grey[400],
),
),
),
),
),
);
}
}
4. Главный экран (lib/screens/drag_drop_screen.dart)
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "../models/task_model.dart";
import "../providers/task_providers.dart";
import "../widgets/draggable_task_item.dart";
class DragDropScreen extends ConsumerStatefulWidget {
const DragDropScreen({Key? key}) : super(key: key);
@override
ConsumerState<DragDropScreen> createState() => _DragDropScreenState();
}
class _DragDropScreenState extends ConsumerState<DragDropScreen> {
int? _draggingIndex;
final TextEditingController _titleController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
void _onReorder(int oldIndex, int newIndex) {
// Сохраняем состояние перед изменением
final currentTasks = ref.read(tasksProvider);
ref.read(taskHistoryProvider.notifier).addSnapshot(currentTasks);
// Выполняем перемещение
ref.read(tasksProvider.notifier).reorderTasks(oldIndex, newIndex);
// Haptic feedback
HapticFeedback.mediumImpact();
}
void _onRemoveTask(int index) {
final currentTasks = ref.read(tasksProvider);
ref.read(taskHistoryProvider.notifier).addSnapshot(currentTasks);
ref.read(tasksProvider.notifier).removeTask(index);
HapticFeedback.mediumImpact();
}
void _onToggleTask(int index) {
ref.read(tasksProvider.notifier).toggleTask(index);
HapticFeedback.lightImpact();
}
void _undo() {
final history = ref.read(taskHistoryProvider);
if (history.isNotEmpty) {
final lastSnapshot = history.last;
ref.read(tasksProvider.notifier).state = lastSnapshot;
// Удаляем последний снимок из истории
ref.read(taskHistoryProvider.notifier).state =
history.sublist(0, history.length - 1);
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Отмена последнего действия"),
duration: Duration(seconds: 2),
),
);
}
}
void _showAddTaskDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Новая задача"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(hintText: "Название"),
),
const SizedBox(height: 12),
TextField(
controller: _descriptionController,
decoration: const InputDecoration(hintText: "Описание"),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Отмена"),
),
ElevatedButton(
onPressed: () {
if (_titleController.text.isNotEmpty) {
final task = Task.create(
_titleController.text,
_descriptionController.text,
);
ref.read(tasksProvider.notifier).addTask(task);
_titleController.clear();
_descriptionController.clear();
Navigator.pop(context);
}
},
child: const Text("Добавить"),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final tasks = ref.watch(tasksProvider);
final history = ref.watch(taskHistoryProvider);
return Scaffold(
appBar: AppBar(
title: const Text("Drag and Drop Список"),
elevation: 0,
backgroundColor: Colors.blue,
actions: [
if (history.isNotEmpty)
IconButton(
icon: const Icon(Icons.undo),
onPressed: _undo,
),
],
),
body: ReorderableListView(
onReorder: _onReorder,
children: List.generate(
tasks.length,
(index) {
final task = tasks[index];
return DraggableTaskItem(
key: Key(task.id),
task: task,
index: index,
isDragging: _draggingIndex == index,
onDismissed: () => _onRemoveTask(index),
onToggle: (_) => _onToggleTask(index),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTaskDialog,
child: const Icon(Icons.add),
),
);
}
}
5. Версия с группировкой (lib/screens/grouped_drag_drop_screen.dart)
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class GroupedDragDropScreen extends ConsumerWidget {
const GroupedDragDropScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text("Группированный Drag and Drop"),
elevation: 0,
),
body: Column(
children: [
// Колонка "Не начято"
_buildTaskGroup(
title: "Не начято",
color: Colors.grey,
tasks: const [],
),
Divider(),
// Колонка "В процессе"
_buildTaskGroup(
title: "В процессе",
color: Colors.orange,
tasks: const [],
),
Divider(),
// Колонка "Завершено"
_buildTaskGroup(
title: "Завершено",
color: Colors.green,
tasks: const [],
),
],
),
);
}
Widget _buildTaskGroup({
required String title,
required Color color,
required List<String> tasks,
}) {
return Expanded(
child: Container(
color: Colors.grey[100],
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 8),
Expanded(
child: DragTarget<String>(
onAccept: (data) {
// Логика перемещения между группами
},
builder: (context, candidateData, rejectedData) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: candidateData.isNotEmpty ? color : Colors.grey,
width: candidateData.isNotEmpty ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: tasks.isEmpty
? Center(
child: Text(
"Перетащите сюда",
style: TextStyle(color: Colors.grey[400]),
),
)
: ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return Draggable<String>(
data: tasks[index],
feedback: Card(
elevation: 8,
child: Container(
width: 200,
padding: const EdgeInsets.all(8),
child: Text(tasks[index]),
),
),
child: Card(
child: ListTile(
title: Text(tasks[index]),
),
),
);
},
),
);
},
),
),
],
),
),
);
}
}
6. main.dart
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "screens/drag_drop_screen.dart";
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: "Drag Drop App",
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: const DragDropScreen(),
),
);
}
}
7. pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.4.0
freezed_annotation: ^2.4.1
uuid: ^4.0.0
json_serializable: ^6.7.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.6
freezed: ^2.4.1
json_serializable: ^6.7.1
Ключевые особенности
- ReorderableListView: Встроенная поддержка drag and drop
- Анимация: ScaleAnimation при перетаскивании
- Haptic feedback: Вибрация при действиях
- Визуальная обратная связь: Изменение opacity и elevation
- Dismissible: Удаление свайпом в конец
- Undo: Восстановление последних 10 действий
- Сохранение порядка: История изменений
- Группировка: Альтернативная версия с Kanban-доской
- DragTarget: Для перемещения между группами
- Добавление задач: Диалог для создания новых элементов
Это production-ready решение для управления списками!