Mở Một Lần Một Thôi! Flutter ExpansionPanelListRadio
Flutter

Mở Một Lần Một Thôi! Flutter ExpansionPanelListRadio

Author

Admin System

@root

Ngày xuất bản

18 Mar, 2026

Lượt xem

15 Lượt

"ExpansionPanelListRadio"

ExpansionPanelListRadio: Ông Chủ Của Sự Ngăn Nắp

Chào các bạn, lại là Creyt đây! Hôm nay chúng ta sẽ "giải mã" một widget mà thoạt nghe có vẻ phức tạp nhưng thực ra lại là "trợ thủ đắc lực" cho sự gọn gàng và tập trung trong giao diện người dùng của chúng ta: ExpansionPanelListRadio.

Bạn cứ hình dung thế này: trong thế giới lập trình, đôi khi chúng ta cần hiển thị một danh sách các lựa chọn hoặc thông tin chi tiết, nhưng nếu cứ "phanh phui" tất cả ra cùng lúc thì màn hình của bạn sẽ trông như một bãi chiến trường vậy. ExpansionPanelListRadio sinh ra để giải quyết vấn đề đó. Nó giống như một cái tủ quần áo thần kỳ của Doraemon, bạn có nhiều ngăn kéo (các panel), nhưng tại một thời điểm, chỉ được phép mở một ngăn duy nhất để lấy đồ thôi. Rất tiện lợi, phải không?

Về cơ bản, nó là gì và để làm gì?

ExpansionPanelListRadio là một widget trong Flutter cho phép bạn tạo một danh sách các bảng điều khiển (panels) có thể mở rộng. Điều đặc biệt ở đây, như cái tên "Radio" đã gợi ý, là nó sẽ tự động đảm bảo rằng chỉ một panel duy nhất có thể được mở rộng tại bất kỳ thời điểm nào. Khi bạn mở một panel khác, panel đang mở trước đó sẽ tự động đóng lại.

Nó cực kỳ hữu ích trong các tình huống sau:

  • Các câu hỏi thường gặp (FAQ): Người dùng chỉ cần mở câu trả lời cho câu hỏi họ quan tâm, tránh việc phải cuộn qua một danh sách dài các câu trả lời.
  • Lựa chọn cấu hình sản phẩm: Ví dụ, khi bạn chọn "Màu sắc", panel chọn màu sẽ mở ra, và khi bạn chọn "Kích cỡ", panel màu sẽ đóng lại và panel kích cỡ mở ra.
  • Hướng dẫn từng bước: Chỉ hiển thị chi tiết cho bước hiện tại.
  • Các bộ lọc (filters) trong ứng dụng thương mại điện tử: Khi bạn chọn một danh mục lọc, các tùy chọn chi tiết của danh mục đó hiện ra, và khi bạn chọn danh mục khác, danh mục cũ sẽ ẩn đi.
Illustration

Code Ví Dụ Minh Họa: "Tủ Đồ Thông Minh"

Để ExpansionPanelListRadio hoạt động, chúng ta cần một StatefulWidget để quản lý trạng thái của panel đang mở. Hãy xem ví dụ dưới đây:

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: 'ExpansionPanelListRadio Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  // Biến để lưu trữ giá trị của panel đang mở.
  // Null nghĩa là không có panel nào mở.
  // Đây là "chìa khóa" để ExpansionPanelListRadio biết panel nào đang active.
  Object? _currentOpenPanelValue;

  final List<Item> _data = generateItems(5); // Tạo 5 item mẫu

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Tủ Đồ Thông Minh Của Creyt'),
      ),
      body: SingleChildScrollView(
        child: ExpansionPanelList.radio(
          // Giá trị của panel được mở ban đầu.
          // Nếu không set, mặc định sẽ không có panel nào mở.
          initialOpenPanelValue: _currentOpenPanelValue,
          
          // Callback khi trạng thái mở/đóng của panel thay đổi.
          // `value` là giá trị của panel vừa được mở/đóng.
          // `isExpanded` là trạng thái mới của panel đó.
          onExpansionChanged: (Object value, bool isExpanded) {
            setState(() {
              // Nếu panel được mở, lưu giá trị của nó.
              // Nếu panel đóng (do người dùng click lại hoặc mở panel khác),
              // thì _currentOpenPanelValue sẽ được set thành null hoặc giá trị của panel mới.
              _currentOpenPanelValue = isExpanded ? value : null;
            });
            print('Panel with value $value is now expanded: $isExpanded');
          },
          
          // Danh sách các ExpansionPanelRadio con.
          children: _data.map<ExpansionPanelRadio>((Item item) {
            return ExpansionPanelRadio(
              value: item.id, // Giá trị duy nhất cho mỗi panel. RẤT QUAN TRỌNG!
              headerBuilder: (BuildContext context, bool isExpanded) {
                return ListTile(
                  title: Text(item.headerValue),
                  leading: Icon(isExpanded ? Icons.folder_open : Icons.folder),
                );
              },
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(item.expandedValue),
                    const SizedBox(height: 10),
                    ElevatedButton(
                      onPressed: () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Bạn vừa chọn: ${item.headerValue}')),
                        );
                      },
                      child: const Text('Chọn mục này'),
                    ),
                  ],
                ),
              ),
              // Cho phép người dùng nhấn vào header để mở/đóng panel.
              canTapOnHeader: true,
            );
          }).toList(),
        ),
      ),
    );
  }
}

// Lớp mẫu để chứa dữ liệu cho mỗi panel
class Item {
  Item({
    required this.id,
    required this.headerValue,
    required this.expandedValue,
  });

  Object id; // Dùng Object để linh hoạt, thường là int hoặc String
  String headerValue;
  String expandedValue;
}

// Hàm tạo dữ liệu mẫu
List<Item> generateItems(int numberOfItems) {
  return List<Item>.generate(numberOfItems, (int index) {
    return Item(
      id: index,
      headerValue: 'Ngăn Kéo Số ${index + 1}',
      expandedValue: 'Đây là nội dung chi tiết của Ngăn Kéo Số ${index + 1}. Bạn có thể đặt bất cứ widget nào vào đây.',
    );
  });
}

Trong ví dụ trên:

  • Chúng ta tạo một List<Item> để mô phỏng dữ liệu cho các panel.
  • _currentOpenPanelValue là biến Object? quản lý panel nào đang được mở. Khi onExpansionChanged được gọi, chúng ta cập nhật biến này để ExpansionPanelListRadio biết trạng thái mới.
  • Mỗi ExpansionPanelRadio cần một value duy nhất. Đây là "định danh" để widget biết panel nào đang được thao tác.
  • headerBuilder xây dựng phần tiêu đề của panel, và body là nội dung sẽ hiển thị khi panel được mở.
  • canTapOnHeader: true là một chi tiết nhỏ nhưng quan trọng, giúp người dùng có thể chạm vào tiêu đề để mở/đóng, thay vì chỉ mũi tên.

Mẹo Hay Từ Giảng Viên Creyt (Best Practices)

  1. Luôn dùng StatefulWidget: ExpansionPanelListRadio cần một biến trạng thái để theo dõi panel nào đang mở (initialOpenPanelValue). Nếu bạn dùng StatelessWidget mà không có cơ chế quản lý trạng thái bên ngoài (như Provider, BLoC, Riverpod), nó sẽ không hoạt động như mong đợi.
  2. value phải DUY NHẤT: Đây là "chìa khóa" để ExpansionPanelListRadio xác định các panel. Nếu các value bị trùng lặp, hành vi của widget sẽ không đúng. Tốt nhất là dùng int hoặc String làm ID duy nhất.
  3. Quản lý initialOpenPanelValue: Biến này không chỉ dùng để thiết lập panel mở ban đầu mà còn được ExpansionPanelListRadio sử dụng nội bộ để biết panel nào đang mở. Luôn cập nhật nó trong onExpansionChanged để đồng bộ trạng thái.
  4. canTapOnHeader là bạn của người dùng: Mặc định, chỉ có mũi tên nhỏ ở cuối header mới có thể mở/đóng panel. Bật canTapOnHeader: true sẽ giúp trải nghiệm người dùng mượt mà hơn rất nhiều.
  5. Nội dung body linh hoạt: Bạn có thể đặt bất kỳ widget phức tạp nào vào phần body của ExpansionPanelRadio, từ Column, Row, Form cho đến các ListView lồng nhau. Hãy tận dụng sự linh hoạt này!

Ứng Dụng Thực Tế: "Ai Đã Dùng Nó?"

Bạn có thể thấy ExpansionPanelListRadio (hoặc các biến thể của nó) được sử dụng rộng rãi trong nhiều ứng dụng và trang web hàng ngày:

  • Các ứng dụng ngân hàng/tài chính: Phần FAQ, hoặc mục "Hỗ trợ" nơi bạn có thể mở các câu hỏi về tài khoản, thẻ tín dụng, v.v., nhưng chỉ một câu trả lời hiện ra mỗi lần.
  • Ứng dụng mua sắm (E-commerce): Trong phần bộ lọc sản phẩm, bạn có thể thấy các mục như "Thương hiệu", "Kích cỡ", "Màu sắc". Khi bạn mở "Thương hiệu", các lựa chọn về thương hiệu hiện ra, và nếu bạn mở "Kích cỡ", phần thương hiệu sẽ tự động đóng lại.
  • Ứng dụng học tập/khóa học online: Các mục lục bài giảng hoặc FAQ về khóa học.
  • Các trang cài đặt (Settings): Đôi khi các cài đặt được nhóm lại thành các panel, và bạn chỉ có thể mở một nhóm cài đặt tại một thời điểm để điều chỉnh.

Tóm lại, ExpansionPanelListRadio không chỉ là một widget đẹp mắt mà còn là một công cụ mạnh mẽ để tạo ra giao diện người dùng gọn gàng, có tổ chức và tập trung. Hãy thử nghiệm và biến nó thành "đồ nghề" của bạn 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!