
PopupMenuItem: Bí kíp tạo menu ngữ cảnh "phụt" ra trong Flutter!
Chào các chiến hữu Gen Z! Hôm nay, anh Creyt sẽ "khui" một trong những widget mà anh gọi là "ngăn kéo bí mật" của Flutter: PopupMenuItem. Nghe cái tên đã thấy "pop-up" rồi đúng không? Chính xác!
PopupMenuItem là gì? Nó để làm gì?
Thực ra, PopupMenuItem không đứng một mình, nó là "đứa con" của PopupMenuButton. Tưởng tượng thế này: Bạn đang lướt Instagram, thấy một cái ảnh hay ho của crush. Bạn muốn lưu lại, chia sẻ, hay thậm chí... report (à mà thôi, đừng report crush nhé!). Bạn nhấn vào dấu ba chấm ở góc trên cái ảnh đó, "phụt" một cái menu nhỏ nhỏ hiện ra với các tùy chọn. Đó chính là PopupMenuItem đang làm nhiệm vụ của mình đấy!
Nói một cách "học thuật" hơn mà vẫn dễ hiểu: PopupMenuItem là một widget dùng để biểu diễn một mục (item) trong một menu ngữ cảnh (contextual menu), thường được kích hoạt bởi PopupMenuButton. Mục đích chính của nó là:
- Tiết kiệm không gian màn hình: Thay vì nhét tất cả các hành động lên giao diện, chúng ta có thể giấu bớt những hành động ít dùng hơn hoặc chỉ liên quan đến một đối tượng cụ thể vào trong menu này.
- Cung cấp hành động ngữ cảnh: Khi người dùng tương tác với một đối tượng (ví dụ: một bài viết, một item trong danh sách), menu này sẽ cung cấp các hành động cụ thể liên quan đến đối tượng đó.
- Tăng trải nghiệm người dùng: Giúp giao diện gọn gàng, sạch sẽ hơn, và người dùng dễ dàng tìm thấy các tùy chọn khi cần.
Anh Creyt hay ví nó như cái "dao đa năng" của UI vậy. Bình thường nó nằm gọn gàng trong túi, không chiếm diện tích. Nhưng khi bạn cần mở bia, cắt dây, hay thậm chí là... dũa móng tay, nó sẽ "phụt" ra đầy đủ công cụ. Đỉnh của chóp!
Code Ví Dụ Minh Họa Rõ Ràng
Giờ thì, lý thuyết suông làm gì, code thôi các bạn ơi! Anh em mình sẽ tạo một màn hình đơn giản với một AppBar và một PopupMenuButton trên đó, sau đó thử nghiệm các loại PopupMenuItem khác nhau.
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: 'Anh Creyt dạy PopupMenuItem',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String _selectedOption = 'Chưa chọn gì';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Menu Ngữ Cảnh của Anh Creyt'),
actions: [
// Đây là PopupMenuButton, thằng cha ôm các PopupMenuItem
PopupMenuButton<String>(
// onSelected: Hàm được gọi khi một PopupMenuItem được chọn
onSelected: (String result) {
setState(() {
_selectedOption = result;
});
// Hiển thị một SnackBar thông báo lựa chọn của người dùng
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Bạn vừa chọn: $result')),
);
},
// itemBuilder: Hàm trả về danh sách các PopupMenuEntry (bao gồm PopupMenuItem)
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'Lựa chọn 1',
child: Text('Lựa chọn số 1'),
),
const PopupMenuItem<String>(
value: 'Chia sẻ',
child: Row(
children: [
Icon(Icons.share, color: Colors.blue),
SizedBox(width: 8),
Text('Chia sẻ ngay và luôn'),
],
),
),
const PopupMenuItem<String>(
value: 'Xóa',
child: Text('Xóa mục này', style: TextStyle(color: Colors.red)),
),
const PopupMenuDivider(), // Dùng để tạo đường phân cách, giúp menu dễ nhìn hơn
const PopupMenuItem<String>(
value: 'Vô hiệu hóa',
enabled: false, // Thử vô hiệu hóa một tùy chọn xem sao
child: Text('Tùy chọn này bị vô hiệu hóa'),
),
// Một ví dụ với CheckedPopupMenuItem
CheckedPopupMenuItem<String>(
value: 'Đã đọc',
checked: true, // Đánh dấu là đã chọn
child: Text('Đánh dấu là đã đọc'),
),
],
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Lựa chọn gần nhất của bạn:',
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
_selectedOption,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 30),
// Một ví dụ PopupMenuButton ở giữa màn hình (trong body)
// Dùng child để hiển thị widget kích hoạt menu
PopupMenuButton<String>(
onSelected: (String result) {
setState(() {
_selectedOption = result;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Bạn chọn từ Body: $result')),
);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'Body Option A',
child: Text('Tùy chọn A (Body)'),
),
const PopupMenuItem<String>(
value: 'Body Option B',
child: Text('Tùy chọn B (Body)'),
),
],
child: ElevatedButton(
onPressed: null, // Đặt null để ElevatedButton không tự xử lý click mà PopupMenuButton sẽ làm
child: const Text('Nhấn để xem menu ngữ cảnh ở Body'),
),
),
],
),
),
);
}
}
Trong ví dụ trên, chúng ta dùng PopupMenuButton để chứa các PopupMenuItem. Khi bạn click vào icon ba chấm (hoặc nút "Nhấn để xem menu ngữ cảnh"), một menu sẽ hiện ra. Khi bạn chọn một item, hàm onSelected của PopupMenuButton sẽ được gọi, và chúng ta cập nhật UI để hiển thị lựa chọn của bạn.

Mẹo (Best Practices) từ anh Creyt
Để dùng PopupMenuItem một cách hiệu quả, anh Creyt có vài "chiêu" muốn truyền lại cho các bạn:
- Giữ cho menu ngắn gọn: Đừng biến nó thành cái "tủ lạnh" chứa đủ thứ đồ mà không ai tìm thấy. Chỉ đặt những hành động thực sự cần thiết và liên quan đến ngữ cảnh.
- Sử dụng icon cho hành động phổ biến: Một cái icon
sharesẽ dễ hiểu hơn nhiều so với một dòng chữ "Chia sẻ bài viết này". - Đừng giấu hành động quan trọng: Những hành động then chốt, người dùng cần truy cập thường xuyên thì nên để lộ ra ngoài (ví dụ: trên
AppBarhoặcFloatingActionButton).PopupMenuItemdành cho các hành động phụ. - Dùng
PopupMenuDividerđể nhóm các mục: Nếu menu của bạn có nhiều mục, hãy dùngPopupMenuDividerđể phân chia các nhóm hành động có liên quan, giúp người dùng dễ quét và hiểu hơn. enabledlà bạn thân: Đôi khi một hành động chỉ có ý nghĩa trong một số điều kiện nhất định. Hãy dùng thuộc tínhenabled: falseđể vô hiệu hóaPopupMenuItemkhi nó không khả dụng, thay vì ẩn nó đi. Điều này giúp người dùng biết rằng hành động đó tồn tại nhưng hiện tại không thể thực hiện.valuevàonSelectedđi đôi với nhau: Luôn gán mộtvalueduy nhất cho mỗiPopupMenuItemđể bạn có thể dễ dàng xác định hành động nào được chọn trong callbackonSelected.
Văn phong học thuật sâu của anh Creyt
Về bản chất, PopupMenuItem là một widget con được thiết kế để hiển thị trong một PopupMenuButton. Nó không đứng một mình mà phải được "nuôi dưỡng" bởi thằng cha PopupMenuButton thông qua thuộc tính itemBuilder thần thánh. itemBuilder này là một hàm (một PopupMenuBuilder) nhận vào BuildContext và trả về một List<PopupMenuEntry<T>>. PopupMenuEntry<T> là một class trừu tượng, và PopupMenuItem<T> cùng với PopupMenuDivider hay CheckedPopupMenuItem<T> là các triển khai cụ thể của nó.
Điều quan trọng cần nắm là generic type T mà bạn truyền vào PopupMenuButton và PopupMenuItem. Type này xác định kiểu dữ liệu của value mà mỗi item sẽ trả về khi được chọn. Khi người dùng chạm vào một PopupMenuItem, PopupMenuButton sẽ gọi callback onSelected của nó, truyền vào giá trị value của item đó. Đây là cơ chế cốt lõi để bạn biết được người dùng muốn làm gì.
Flutter thiết kế rất linh hoạt, bạn có thể đặt bất kỳ widget nào làm child của PopupMenuItem, không nhất thiết phải là Text. Điều này cho phép chúng ta tạo ra các item menu phức tạp với icon, hình ảnh, hoặc thậm chí là các layout tùy chỉnh. Tuyệt vời phải không?
Ví dụ thực tế các ứng dụng/website đã ứng dụng
PopupMenuItem (hoặc các thành phần UI tương tự trong các nền tảng khác) xuất hiện khắp nơi, đến mức bạn dùng mà không để ý:
- Mạng xã hội (Instagram, TikTok, Facebook): Khi bạn nhấn vào dấu ba chấm trên một bài đăng để xem các tùy chọn như "Lưu", "Chia sẻ", "Ẩn bài viết", "Báo cáo", "Xóa", "Chỉnh sửa".
- Ứng dụng quản lý file (Google Drive, Dropbox): Khi bạn nhấn giữ hoặc nhấn vào icon menu bên cạnh một file/thư mục để thực hiện các hành động như "Đổi tên", "Sao chép", "Di chuyển", "Xóa", "Chia sẻ", "Chi tiết".
- Ứng dụng Email (Gmail, Outlook): Khi bạn chọn một email và muốn "Đánh dấu là đã đọc/chưa đọc", "Chuyển vào thư mục", "Xóa", "Phản hồi".
- Trình duyệt web (Chrome, Safari): Menu ngữ cảnh khi bạn click chuột phải vào một đối tượng (ảnh, link) trên trang web.
Đó là những nơi PopupMenuItem phát huy tác dụng tối đa, giúp giao diện trở nên gọn gàng và cung cấp các tùy chọn theo ngữ cảnh.
Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Hồi xưa anh Creyt mới vào nghề, cũng ham hố nhét hết mọi thứ lên màn hình, nhìn nó rối như mớ bòng bong. Đến khi gặp PopupMenuItem này, mới thấy nó như một "phép màu" giúp dọn dẹp cái mớ bòng bong đó, biến giao diện từ "chợ trời" thành "showroom".
Nên dùng PopupMenuItem khi:
- Các hành động phụ, ít được sử dụng thường xuyên: Những chức năng không phải là trọng tâm của màn hình nhưng vẫn cần thiết.
- Các hành động ngữ cảnh: Chỉ có ý nghĩa khi người dùng tương tác với một đối tượng cụ thể (ví dụ: các tùy chọn cho một item trong danh sách).
- Tiết kiệm không gian UI: Đặc biệt quan trọng trên màn hình di động nhỏ hẹp, nơi mỗi pixel đều quý như vàng.
- Menu cài đặt nhanh: Cung cấp một bộ tùy chọn cài đặt nhỏ gọn, nhanh chóng.
Không nên dùng PopupMenuItem khi:
- Hành động chính, thường xuyên: Nếu người dùng phải thực hiện hành động này liên tục, hãy đưa nó ra ngoài (ví dụ: nút "Thêm mới", "Lưu" nên là
FloatingActionButtonhoặc nằm trênAppBar). - Cần sự chú ý ngay lập tức: Các hành động mang tính cảnh báo hoặc yêu cầu người dùng phản hồi ngay lập tức thì nên dùng
AlertDialoghoặcSnackBar. - Quá nhiều tùy chọn: Nếu menu của bạn dài dằng dặc với hàng chục tùy chọn, thì có lẽ bạn nên xem xét một màn hình cài đặt riêng hoặc một cách tổ chức UI khác.
Hãy nghĩ về nó như một ngăn kéo bí mật. Đồ quan trọng nhất, dùng thường xuyên nhất thì để trên mặt bàn. Còn những thứ dùng ít hơn, hoặc chỉ dùng trong ngữ cảnh nhất định, thì cất vào ngăn kéo này. Dùng đúng chỗ, đúng lúc, PopupMenuItem sẽ là trợ thủ đắc lực cho ứng dụng Flutter của bạn!
Chúc các bạn code vui vẻ và áp dụng thành công!
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é!