Một hàng đã trượt khỏi màn hình nhưng vẫn nằm trong danh sách dữ liệu; ở lần build sau, ứng dụng có thể gặp lỗi vì Dismissible đã bị dismiss vẫn còn trong cây. Một lỗi khác là xóa theo index cũ, làm mất nhầm hàng sau khi danh sách thay đổi.
Bài này dùng Flutter Material và Dismissible, đối chiếu tài liệu ngày 31/10/2026. Mẫu chỉ quản lý danh sách trong RAM. Chưa chạy Flutter build, widget test hoặc thao tác vuốt trên thiết bị; phần kiểm tra dưới đây là các bước cần thực hiện trong dự án của bạn.
1. Hai callback có hai trách nhiệm
confirmDismiss trả quyết định cho phép thao tác tiếp tục. Kết quả false hoặc null đưa hàng về vị trí ban đầu. onDismissed chạy sau khi hàng đã hoàn tất phần dismiss và là nơi ví dụ cập nhật danh sách.
Không xóa dữ liệu ngay lúc mở dialog, vì người dùng còn có thể chọn Hủy hoặc đóng dialog. Cũng không chỉ trả true rồi bỏ quên cập nhật collection. Trong một danh sách, mỗi Dismissible phải có key phân biệt với những hàng khác.
2. Mẫu chạy trong ứng dụng Material
Dùng đoạn dưới trong lib/main.dart của một project Flutter có sẵn. Mỗi task có ID cố định và duy nhất. Nội dung chỉ là việc học mẫu, không gửi yêu cầu xóa lên server.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: TaskPage()));
class TaskItem {
const TaskItem(this.id, this.title);
final String id;
final String title;
}
class TaskPage extends StatefulWidget {
const TaskPage({super.key});
@override
State<TaskPage> createState() => _TaskPageState();
}
class _TaskPageState extends State<TaskPage> {
final tasks = <TaskItem>[
const TaskItem('read', 'Đọc tài liệu'),
const TaskItem('write', 'Viết ví dụ'),
const TaskItem('review', 'Rà kết quả'),
];
Future<bool> confirm(TaskItem task) async {
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Xóa công việc?'),
content: Text(task.title),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: const Text('Hủy'),
),
TextButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: const Text('Xóa'),
),
],
),
);
return mounted && result == true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Công việc local')),
body: tasks.isEmpty
? const Center(child: Text('Đã hết công việc'))
: ListView(
children: [
for (final task in tasks)
Dismissible(
key: ValueKey(task.id),
direction: DismissDirection.endToStart,
confirmDismiss: (_) => confirm(task),
onDismissed: (_) {
if (!mounted) return;
setState(() {
tasks.removeWhere((item) => item.id == task.id);
});
},
background: Container(
color: Colors.red,
alignment: AlignmentDirectional.centerEnd,
padding: const EdgeInsets.all(16),
child: const Icon(Icons.delete, color: Colors.white),
),
child: ListTile(title: Text(task.title)),
),
],
),
);
}
}
task được giữ theo từng vòng lặp và callback xóa dựa trên ID đó. Không suy ra ID từ vị trí hiển thị tại thời điểm callback kết thúc. Điều kiện mounted sau dialog tránh cho phép thao tác tiếp tục khi page không còn trong cây.
3. Kiểm tra cả Hủy, đóng ngoài và Xóa
Vuốt hàng giữa rồi chọn Hủy: danh sách vẫn có đủ ba mục. Thử vuốt lại và chạm ngoài dialog: kết quả null cũng phải giữ dữ liệu. Sau đó chọn Xóa và kiểm tra chỉ task có ID tương ứng biến mất.
Xóa hết từng mục để quan sát empty state. Thử danh sách đã được sắp xếp theo thứ tự khác và kiểm tra quyết định vẫn dựa trên ID. Với giao diện RTL, endToStart đi theo hướng đọc; không mô tả nó là luôn vuốt sang trái trên mọi locale.
Mẫu đã được rà theo hợp đồng callback trong tài liệu, nhưng các bước gesture và animation này chưa được chạy tự động. Khi đưa vào dự án, lưu phiên bản flutter --version, nền tảng và các bước tái hiện nếu có lỗi.
Lỗi thường gặp và giới hạn
Nếu hàng đã dismiss vẫn tồn tại, kiểm tra collection được dùng để build có thực sự được cập nhật trong onDismissed. Nếu xóa nhầm, tìm index đã bị giữ trong callback hoặc ID bị trùng. Dùng key theo index khiến định danh thay đổi khi hàng trước bị xóa.
Không dùng UniqueKey() mới ở mỗi build để che lỗi định danh. Hãy sửa mô hình dữ liệu để mỗi mục có ID ổn định. Key hỗ trợ framework nhận diện widget; ID vẫn cần được dùng đúng trong thao tác dữ liệu.
Ví dụ chưa xử lý xóa qua mạng, undo hoặc đồng bộ đa thiết bị. Với dữ liệu từ server, cần thiết kế thời điểm chờ kết quả, báo lỗi và khả năng khôi phục trước khi cho hàng biến mất vĩnh viễn. Đừng thêm một request bất đồng bộ vào onDismissed rồi mặc định animation hoàn tất là dữ liệu đã được lưu.


