PopupMenuEntry: Mở Kho Báu Tùy Biến Cho Menu Flutter Của Bạn!
Flutter

PopupMenuEntry: Mở Kho Báu Tùy Biến Cho Menu Flutter Của Bạn!

Author

Admin System

@root

Ngày xuất bản

20 Mar, 2026

Lượt xem

3 Lượt

"PopupMenuEntry"

Chào các bạn developer tương lai, hay nói đúng hơn là các 'phù thủy code' thế hệ Z! Hôm nay, anh Creyt sẽ cùng các bạn 'mổ xẻ' một khái niệm nghe thì có vẻ hơi 'academic' nhưng lại cực kỳ 'cool' và 'hack' được nhiều thứ trong Flutter: PopupMenuEntry.

Các bạn cứ hình dung thế này, trong thế giới game online, mỗi khi bạn mở một cái "loot box" (hộp quà may mắn), bạn sẽ nhận được một danh sách các "item" đúng không? Có thể là một thanh kiếm, một lọ máu, hay thậm chí là một bộ giáp huyền thoại. Trong Flutter, cái "loot box" chính là PopupMenuButton của chúng ta, và mỗi "item" mà bạn thấy trong đó – từ dòng chữ đơn giản đến những tùy chọn phức tạp hơn – tất cả đều là con cháu của một 'ông tổ' vĩ đại tên là PopupMenuEntry.

PopupMenuEntry là gì và để làm gì?

Vậy PopupMenuEntry sinh ra để làm gì? Đơn giản là để bạn định nghĩa từng thành phần một bên trong cái menu popup đó. Nó giống như bạn có một cái khuôn để đúc ra các loại bánh khác nhau vậy. Flutter đã cung cấp sẵn cho bạn một vài loại bánh cơ bản rồi, như PopupMenuItem (bánh đơn giản, có chữ có icon) hay PopupMenuDivider (bánh ngăn cách).

Nhưng nếu bạn muốn một cái bánh 'độc lạ Bình Dương', ví dụ như một cái bánh có nút gạt 'on/off' hay một thanh trượt để điều chỉnh âm lượng ngay trong menu thì sao? Đó chính là lúc 'ông tổ' PopupMenuEntry tỏa sáng! Nó là một abstract class (lớp trừu tượng), nghĩa là nó chỉ là một bản thiết kế, một bộ quy tắc mà các 'con cháu' của nó phải tuân theo. PopupMenuItem là một trong những 'con cháu' phổ biến nhất của nó.

Với PopupMenuEntry, bạn có thể tạo ra bất kỳ widget nào mà bạn muốn xuất hiện trong menu popup, biến menu của bạn không chỉ là danh sách các lựa chọn tĩnh mà còn là một khu vực tương tác mini.

Code Ví Dụ Minh Họa

Trước tiên, chúng ta hãy xem một PopupMenuButton cơ bản với các PopupMenuItemPopupMenuDivider:

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: 'Flutter PopupMenuEntry Demo',
      theme: ThemeData(useMaterial3: true),
      home: const MyHomePage(),
    );
  }
}

enum MenuOption { edit, delete, share, settings }

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _selectedOption = 'Chưa chọn gì';
  bool _isProModeEnabled = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Menu Popup của Creyt'),
        actions: [
          PopupMenuButton<MenuOption>(
            onSelected: (MenuOption result) {
              setState(() {
                _selectedOption = 'Bạn đã chọn: ${result.name}';
              });
              if (result == MenuOption.settings) {
                // Xử lý tùy chọn cài đặt đặc biệt
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Mở cài đặt...')), 
                );
              }
            },
            itemBuilder: (BuildContext context) => <PopupMenuEntry<MenuOption>>[
              const PopupMenuItem<MenuOption>(
                value: MenuOption.edit,
                child: Text('Chỉnh sửa'),
              ),
              const PopupMenuItem<MenuOption>(
                value: MenuOption.delete,
                child: Text('Xóa'),
              ),
              const PopupMenuDivider(), // Dùng để phân chia các nhóm tùy chọn
              const PopupMenuItem<MenuOption>(
                value: MenuOption.share,
                child: Text('Chia sẻ'),
              ),
              const PopupMenuDivider(),
              // Đây là nơi chúng ta sẽ nhúng CustomInteractiveEntry!
              CustomInteractiveEntry(
                initialValue: _isProModeEnabled,
                onChanged: (bool newValue) {
                  setState(() {
                    _isProModeEnabled = newValue;
                  });
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Chế độ Pro: ${_isProModeEnabled ? 'BẬT' : 'TẮT'}')),
                  );
                  // Lưu ý: PopupMenuButton thường không tự đóng khi một widget con tương tác.
                  // Nếu bạn muốn đóng, bạn cần Navigator.pop(context) thủ công.
                },
              ),
            ],
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              _selectedOption,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 20),
            Text(
              'Chế độ Pro hiện đang: ${_isProModeEnabled ? 'BẬT' : 'TẮT'}',
              style: Theme.of(context).textTheme.titleMedium,
            ),
          ],
        ),
      ),
    );
  }
}

// Đây là 'con cháu' tùy biến của PopupMenuEntry mà anh Creyt đã nhắc tới!
class CustomInteractiveEntry extends StatefulWidget implements PopupMenuEntry<MenuOption> {
  const CustomInteractiveEntry({
    super.key,
    required this.initialValue,
    required this.onChanged,
  });

  final bool initialValue;
  final ValueChanged<bool> onChanged;

  @override
  State<CustomInteractiveEntry> createState() => _CustomInteractiveEntryState();

  // Chiều cao của item trong menu. kMinInteractiveDimension là chiều cao tiêu chuẩn cho các widget tương tác.
  @override
  double get height => kMinInteractiveDimension;

  // Phương thức này cho biết liệu item này có đại diện cho một giá trị cụ thể trong menu không.
  // Trong trường hợp này, nó là một widget tương tác, không đại diện cho một lựa chọn 'value' nào,
  // nên chúng ta trả về false.
  @override
  bool represents(MenuOption? value) => false;
}

class _CustomInteractiveEntryState extends State<CustomInteractiveEntry> {
  late bool _currentValue;

  @override
  void initState() {
    super.initState();
    _currentValue = widget.initialValue;
  }

  @override
  Widget build(BuildContext context) {
    // Đây là widget thực tế sẽ được hiển thị trong menu popup.
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          const Text('Bật chế độ Pro'),
          Switch(
            value: _currentValue,
            onChanged: (newValue) {
              setState(() {
                _currentValue = newValue;
              });
              widget.onChanged(newValue);
              // Thông thường, bạn không muốn đóng menu khi chỉ thay đổi một switch.
              // Nếu muốn đóng, bạn có thể gọi Navigator.pop(context) ở đây.
            },
          ),
        ],
      ),
    );
  }
}

Trong ví dụ trên, chúng ta đã tạo một CustomInteractiveEntry là một StatefulWidgetimplements PopupMenuEntry<MenuOption>. Điều này cho phép chúng ta nhúng một Switch ngay bên trong menu popup, mang lại trải nghiệm tương tác trực tiếp mà không cần phải rời khỏi menu.

Illustration

Mẹo (Best Practices) từ anh Creyt

Anh Creyt có vài 'bí kíp' truyền lại cho các bạn đây, nhớ mà xài nhé:

  1. "Đừng biến menu thành mê cung": Giữ cho các lựa chọn đơn giản, dễ hiểu. Nếu menu quá dài hoặc có quá nhiều thứ, hãy nghĩ đến việc dùng BottomSheet hoặc đưa các hành động phức tạp sang một màn hình riêng.
  2. "Phân chia ranh giới rõ ràng": Dùng PopupMenuDivider để nhóm các hành động liên quan lại với nhau. Giống như phân loại đồ đạc trong kho báu vậy, dễ tìm, dễ dùng, không bị loạn.
  3. "Tùy biến là sức mạnh, nhưng phải có chừng mực": Khi bạn cần các UI element độc đáo như Switch, Slider, hoặc thậm chí là một TextField nhỏ ngay trong menu, PopupMenuEntry chính là 'thần đèn' của bạn. Nhưng đừng lạm dụng, một menu quá 'nặng' sẽ gây khó chịu cho người dùng.
  4. "Đừng quên người dùng đặc biệt": Luôn nghĩ đến khả năng tiếp cận (Accessibility). Đảm bảo các child của bạn có tooltip rõ ràng, các semantics phù hợp để người dùng khiếm thị cũng có thể hiểu được và tương tác dễ dàng.
  5. "Khi nào dùng showMenu?": PopupMenuButton là tiện lợi, nhưng khi bạn muốn kiểm soát vị trí hiển thị menu một cách chính xác hơn, hoặc kích hoạt nó từ một sự kiện không phải là nhấn nút (ví dụ: nhấn giữ vào một item trong danh sách), hãy dùng hàm showMenu trực tiếp. Nó giống như bạn tự tay đặt cái 'loot box' ở bất cứ đâu bạn muốn vậy.

Ví Dụ Thực Tế

Các bạn có thấy cái menu 'ba chấm' (kebab menu) thần thánh trong Gmail, Google Drive không? Hay khi bạn nhấn giữ vào một tin nhắn trong Zalo, Messenger để hiện ra các tùy chọn như 'trả lời', 'chuyển tiếp', 'xóa'? Đó chính là những ứng dụng kinh điển của popup menu.

Trong các ứng dụng chỉnh sửa ảnh hoặc video, khi bạn nhấn vào một layer hoặc một đối tượng và hiện ra menu 'tùy chọn' với các nút gạt 'hiện/ẩn', 'khóa layer', hay một thanh trượt để điều chỉnh độ trong suốt ngay trong menu – đó cũng là một biến thể của PopupMenuEntry tùy biến đấy. Nó giúp người dùng thao tác nhanh mà không cần mở một cửa sổ hay màn hình mới.

Thử Nghiệm và Nên Dùng Cho Case Nào?

Anh Creyt đã từng 'test drive' PopupMenuEntry trong nhiều dự án rồi. Nó cực kỳ hữu ích khi:

  • "Không gian hẹp": Khi bạn có một danh sách các hành động phụ mà không muốn chiếm quá nhiều diện tích màn hình chính. Ví dụ, trên một thẻ bài (card) thông tin, bạn chỉ có một icon ba chấm để mở menu các hành động liên quan đến thẻ đó.
  • "Hành động phụ cho từng item": Ví dụ, trên một danh sách các bài viết, mỗi bài viết có một nút 'ba chấm' để 'chỉnh sửa', 'xóa', 'chia sẻ'. Đây là trường hợp phổ biến nhất.
  • "UI tương tác nhanh": Khi bạn cần một vài tùy chỉnh nhanh gọn mà không muốn chuyển sang màn hình mới. Anh từng làm một cái app nghe nhạc, và trong menu popup của bài hát có một cái Switch để bật/tắt lặp lại bài hát, hoặc một Slider nhỏ để điều chỉnh tốc độ phát. Cực kỳ tiện lợi!

Cẩn thận đừng lạm dụng: Tuy nhiên, đừng biến menu popup thành một cái form mini nhé. Nếu bạn cần quá nhiều input hoặc logic phức tạp, hãy đưa nó ra một màn hình riêng hoặc một AlertDialog cho 'sang chảnh' và dễ quản lý hơn. Mục đích của PopupMenuEntry là cung cấp các tùy chọn nhanh, gọn, lẹ thôi. Nó giống như một 'lối tắt' vậy, chứ không phải là 'con đường cao tốc' để đi đến mọi nơi đâu nha!

Hy vọng với những chia sẻ này, các bạn đã 'nắm trọn' được sức mạnh của PopupMenuEntry và biết cách 'hack' nó vào các dự án Flutter của mình rồi. Cứ thực hành và 'cứu thế giới' bằng code của mình nhé!

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é!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!