
Chào các lập trình viên tương lai! Hôm nay, Giảng viên Creyt sẽ dẫn các bạn đi khám phá một "công cụ" cực kỳ lợi hại trong kho vũ khí UI/UX của Flutter: Dismissible (hay chính xác hơn là Dismissible widget, vì DismissiblePane là một concept mở rộng từ nó, nhưng ý tưởng cốt lõi thì như nhau). Đừng lo, tôi sẽ giải thích cặn kẽ như khi bạn đang nhâm nhi ly cà phê sáng, dễ hiểu đến mức bà bạn cũng gật gù!
1. Dismissible là gì và để làm gì? (Vuốt phát là bay!)
Bạn đã bao giờ dùng ứng dụng email hay ứng dụng quản lý công việc chưa? Cái cảm giác vuốt một email sang trái để xóa, hay vuốt sang phải để đánh dấu đã đọc ấy, đó chính là Dismissible đang "làm trò" đó.
Hãy tưởng tượng thế này: bạn có một chồng giấy tờ lộn xộn trên bàn, mỗi tờ là một nhiệm vụ cần làm. Với Dismissible, bạn như có một "cái thùng rác ma thuật" ngay bên cạnh. Chỉ cần vuốt nhẹ tờ giấy nào không cần nữa, "phụt!" nó biến mất. Hoặc vuốt sang hướng khác, "tách!" nó được chuyển vào mục lưu trữ.
Nói một cách hàn lâm hơn, Dismissible trong Flutter là một widget cho phép bạn loại bỏ (dismiss) một widget con bằng cách vuốt nó sang một hướng cụ thể. Nó cực kỳ hữu ích để:
- Xóa/Lưu trữ các mục trong danh sách: Điển hình nhất là các mục trong
ListView(email, tin nhắn, ghi chú, sản phẩm trong giỏ hàng). - Cung cấp tương tác trực quan: Nâng cao trải nghiệm người dùng bằng cách cho phép họ thao tác trực tiếp trên các phần tử UI.
- Giảm bớt nút bấm: Thay vì phải nhấn vào một icon xóa, vuốt luôn tiện lợi hơn nhiều.

2. Code Ví Dụ Minh Họa: Danh sách nhiệm vụ "vuốt là bay"
Để các bạn dễ hình dung, chúng ta sẽ xây dựng một danh sách các nhiệm vụ đơn giản. Khi vuốt một nhiệm vụ sang trái, nó sẽ bị xóa. Khi vuốt sang phải, nó sẽ được đánh dấu là "đã hoàn thành".
Các thành phần chính của Dismissible:
key: Cực kỳ quan trọng! Flutter dùngkeyđể nhận diện duy nhất từngDismissiblewidget. Nếu không có key hoặc key không duy nhất, bạn sẽ gặp lỗi hoặc hành vi không mong muốn. Thường dùngUniqueKey()hoặcValueKey()với ID của đối tượng.child: Widget mà bạn muốn cho phép người dùng vuốt đi (ví dụ: mộtListTile).background: Widget sẽ hiển thị phía sauchildkhi bạn vuốt từ trái sang phải (hoặc từ trên xuống dưới, tùydirection).secondaryBackground: Tương tựbackground, nhưng hiển thị khi bạn vuốt từ phải sang trái (hoặc từ dưới lên trên).onDismissed: Hàm callback được gọi khi widget đã được vuốt hoàn toàn và bị loại bỏ. Đây là nơi bạn cập nhật dữ liệu của mình.direction: Chỉ định các hướng vuốt được phép (DismissDirection.horizontal,DismissDirection.startToEnd,DismissDirection.endToStart, v.v.).
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dismissible Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const DismissibleTasksScreen(),
);
}
}
class DismissibleTasksScreen extends StatefulWidget {
const DismissibleTasksScreen({super.key});
@override
State<DismissibleTasksScreen> createState() => _DismissibleTasksScreenState();
}
class _DismissibleTasksScreenState extends State<DismissibleTasksScreen> {
final List<String> _tasks = List.generate(
10,
(index) => 'Nhiệm vụ số ${index + 1}',
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Danh Sách Nhiệm Vụ (Vuốt là bay!)'),
),
body: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) {
final item = _tasks[index];
return Dismissible(
// 1. **Key là bắt buộc và phải duy nhất!**
key: Key(item), // Sử dụng item làm key, đảm bảo duy nhất
direction: DismissDirection.horizontal, // Cho phép vuốt ngang
// 2. Background khi vuốt từ trái sang phải (đánh dấu hoàn thành)
background: Container(
color: Colors.green,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: const Icon(Icons.check, color: Colors.white),
),
// 3. Background khi vuốt từ phải sang trái (xóa)
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: const Icon(Icons.delete, color: Colors.white),
),
// 4. Widget con được vuốt
child: ListTile(
title: Text(item),
subtitle: const Text('Trạng thái: Đang chờ'),
),
// 5. Hàm callback khi widget bị loại bỏ
onDismissed: (direction) {
// Xóa item khỏi danh sách dữ liệu
setState(() {
_tasks.removeAt(index);
});
// Hiển thị Snackbar thông báo hành động
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
direction == DismissDirection.startToEnd
? '$item đã hoàn thành!'
: '$item đã bị xóa.',
),
action: SnackBarAction(
label: 'Hoàn tác',
onPressed: () {
// Thường thì bạn sẽ không hoàn tác xóa ngay lập tức
// mà có thể lưu vào một danh sách 'đã xóa' tạm thời.
// Trong ví dụ này, chúng ta đơn giản là thêm lại.
setState(() {
_tasks.insert(index, item);
});
},
),
),
);
},
);
},
),
);
}
}
Giải thích nhanh:
- Chúng ta có một
List<String> _tasksđể lưu trữ dữ liệu. - Mỗi
ListTileđược bọc trong mộtDismissible. Key(item)đảm bảo mỗiDismissiblecó một khóa duy nhất.backgroundvàsecondaryBackgroundcung cấp phản hồi trực quan khi người dùng vuốt.- Trong
onDismissed, chúng ta xóa mục khỏi_tasksvà dùngsetStateđể cập nhật UI. MộtSnackBarcũng được dùng để thông báo và cung cấp tùy chọn hoàn tác (dù cho xóa thực sự thì việc hoàn tác phức tạp hơn nhiều).
3. Mẹo (Best Practices) từ "lão làng" Creyt
- Key là Vua: Tôi nhắc lại lần nữa,
Keylà linh hồn củaDismissible. Luôn đảm bảo nó là duy nhất cho mỗi item. Nếu không, Flutter sẽ không biết item nào đang bị vuốt, dẫn đến những hành vi khó chịu như item sai bị xóa hoặc lỗi.- Mẹo nhỏ: Nếu dữ liệu của bạn có ID duy nhất (như từ database), hãy dùng
ValueKey(yourItem.id). Nếu không,UniqueKey()là lựa chọn an toàn cho các widget động.
- Mẹo nhỏ: Nếu dữ liệu của bạn có ID duy nhất (như từ database), hãy dùng
- Phản hồi Trực quan là Tiền: Luôn cung cấp
backgroundvàsecondaryBackgroundrõ ràng. Người dùng cần biết hành động của họ sẽ dẫn đến kết quả gì trước khi họ hoàn thành thao tác vuốt. - Xử lý dữ liệu cẩn thận: Hàm
onDismissedlà nơi bạn thực sự xóa hoặc cập nhật dữ liệu. Luôn gọisetStatesau khi thay đổi danh sách dữ liệu để UI được cập nhật. - Hoàn tác (Undo) là ân huệ: Đối với các hành động phá hủy (như xóa), hãy cân nhắc cung cấp một cơ chế hoàn tác ngắn hạn (thường là qua
SnackBar). Điều này giúp người dùng sửa chữa sai lầm và tăng sự tự tin khi sử dụng ứng dụng của bạn. - Xác nhận cho hành động "nghiêm trọng": Nếu việc xóa có thể gây mất mát dữ liệu lớn, hãy cân nhắc hiển thị một
AlertDialogđể xác nhận trước khi thực sự loại bỏ item.Dismissiblecó một callbackconfirmDismisscho mục đích này.
4. Ứng dụng thực tế: Bạn thấy Dismissible ở đâu?
Dismissible không phải là một công nghệ mới lạ mà nó đã trở thành một chuẩn mực trong thiết kế UI/UX hiện đại. Bạn sẽ thấy nó ở khắp mọi nơi:
- Gmail / Outlook / Apple Mail: Vuốt email để lưu trữ, xóa, đánh dấu đã đọc. Đây là ví dụ kinh điển nhất!
- Todoist / Google Tasks: Vuốt nhiệm vụ để đánh dấu hoàn thành hoặc xóa bỏ.
- WhatsApp / Telegram: Vuốt cuộc trò chuyện để lưu trữ hoặc xóa.
- Spotify / Apple Music: Vuốt bài hát trong danh sách phát để xóa khỏi danh sách.
- Ứng dụng mua sắm (Shopping Cart): Vuốt sản phẩm trong giỏ hàng để xóa khỏi giỏ.
Nhìn chung, bất cứ khi nào bạn có một danh sách các mục và muốn người dùng có thể nhanh chóng loại bỏ hoặc thực hiện một hành động nhanh trên từng mục mà không cần phải nhấn vào nhiều nút, Dismissible chính là "người hùng" bạn cần triệu hồi.
Chúc mừng bạn đã nắm vững một trong những kỹ thuật tương tác người dùng hiệu quả nhất trong Flutter! Giờ thì hãy tự tin áp dụng nó vào các dự án của mình nhé. Hẹn gặp lại trong bài học tiếp theo!
Thuộc Series: Flutter
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!