PersistentBottomSheetController: Remote điều khiển 'bảng thông báo' dưới chân màn hình
Flutter

PersistentBottomSheetController: Remote điều khiển 'bảng thông báo' dưới chân màn hình

Author

Admin System

@root

Ngày xuất bản

20 Mar, 2026

Lượt xem

2 Lượt

"PersistentBottomSheetController"

Chào các homie, anh Creyt lại lên sóng đây! Hôm nay, chúng ta sẽ đào sâu vào một nhân vật khá 'lầm lì' nhưng cực kỳ quyền năng trong vũ trụ Flutter: PersistentBottomSheetController. Nghe tên có vẻ dài dòng, nhưng thực ra nó là cái remote điều khiển 'cái bảng thông báo' hay 'cái khay' nằm chễm chệ dưới chân màn hình của mấy đứa đó. Cùng anh khám phá nhé!

1. PersistentBottomSheetController là cái quái gì và để làm gì?

Thôi bỏ mấy cái tên hàn lâm đi. Tưởng tượng thế này: Màn hình điện thoại của mấy đứa là một cái bàn ăn sang chảnh.

  • ModalBottomSheet (cái mà mấy đứa hay dùng showModalBottomSheet ấy) giống như một anh phục vụ bưng ra một cái menu đặc biệt. Anh ta đứng chặn trước mặt, bắt mấy đứa phải chọn món hoặc từ chối xong xuôi thì mới được tiếp tục ăn món chính. Nó chặn hết tương tác với phần còn lại của màn hình.

  • Còn PersistentBottomSheet thì khác. Nó giống như một cái bảng nhỏ, có thể thu vào kéo ra, gắn cố định ở mép bàn của mấy đứa (ví dụ: cái bảng hiển thị khuyến mãi hôm nay, hoặc nút gọi phục vụ nhanh). Nó luôn ở đó, không chặn mấy đứa ăn món chính, nhưng mấy đứa có thể tương tác với nó bất cứ lúc nào muốn. Nó là một phần của cái bàn, chứ không phải một vật thể 'lơ lửng' che phủ.

Thế còn PersistentBottomSheetController? À, nó chính là cái remote điều khiển cho cái bảng nhỏ đó! Thay vì phải tự tay kéo ra đẩy vào, mấy đứa có thể 'bấm nút' trên remote để cái bảng tự động hiện lên, tự động ẩn đi, hoặc làm bất cứ trò gì mà mấy đứa đã lập trình cho nó. Nó cung cấp cho mấy đứa một 'tay nắm' để tương tác với cái PersistentBottomSheet sau khi nó đã được tạo ra.

Tóm lại: Nó cho phép mấy đứa điều khiển một bottom sheet không che phủ toàn màn hình một cách lập trình, giúp UI của mấy đứa linh hoạt và mượt mà hơn.

2. Code Ví Dụ Minh Họa Rõ Ràng

Để sử dụng PersistentBottomSheetController, chúng ta cần một Scaffold và một Builder widget. Tại sao ư? Vì Scaffold.of(context) cần một BuildContext mà tổ tiên của nó phải là Scaffold. Nếu mấy đứa gọi Scaffold.of(context) ngay trong build method của StatefulWidget chứa Scaffold, context đó sẽ không 'nhìn thấy' Scaffold của chính nó đâu. Cái này gọi là 'context tree' trong Flutter đó mấy đứa. Dùng Builder là cách để có một context 'con cháu' của Scaffold, đảm bảo Scaffold.of hoạt động trơn tru.

import 'package:flutter/material.dart';

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

  @override
  State<PersistentBottomSheetDemo> createState() => _PersistentBottomSheetDemoState();
}

class _PersistentBottomSheetDemoState extends State<PersistentBottomSheetDemo> {
  // Khai báo một biến để giữ reference đến controller của bottom sheet.
  PersistentBottomSheetController? _bottomSheetController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Persistent Bottom Sheet Demo'),
      ),
      body: Center(
        child: Builder( // Rất quan trọng! Builder giúp lấy đúng context con của Scaffold.
          builder: (BuildContext innerContext) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    // Nếu sheet chưa được mở, thì mở nó ra.
                    if (_bottomSheetController == null) {
                      _bottomSheetController = Scaffold.of(innerContext).showBottomSheet(
                        (BuildContext context) {
                          return Container(
                            height: 200,
                            color: Colors.blueAccent.shade100,
                            child: Center(
                              child: Column(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: <Widget>[
                                  const Text(
                                    'Đây là Persistent Bottom Sheet của bạn!',
                                    style: TextStyle(fontSize: 18),
                                  ),
                                  const SizedBox(height: 20),
                                  ElevatedButton(
                                    onPressed: () {
                                      // Đóng sheet bằng controller
                                      _bottomSheetController?.close();
                                      _bottomSheetController = null; // Đặt lại về null sau khi đóng
                                    },
                                    child: const Text('Đóng Sheet'),
                                  ),
                                ],
                              ),
                            ),
                          );
                        },
                        // elevation: 10, // Có thể thêm elevation để tạo bóng đổ
                        // backgroundColor: Colors.transparent, // Hoặc làm trong suốt
                      );

                      // Có thể lắng nghe trạng thái đóng của sheet
                      _bottomSheetController?.closed.whenComplete(() {
                        // Khi sheet đóng, đặt controller về null để có thể mở lại.
                        if (mounted) {
                          setState(() {
                            _bottomSheetController = null;
                          });
                        }
                        print('Persistent Bottom Sheet đã đóng rồi!');
                      });
                    } else {
                      // Nếu sheet đang mở, in ra thông báo hoặc làm gì đó khác.
                      print('Persistent Bottom Sheet đã mở rồi!');
                    }
                  },
                  child: const Text('Mở Persistent Bottom Sheet'),
                ),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _bottomSheetController != null
                      ? () {
                          // Đóng sheet trực tiếp nếu controller đang active
                          _bottomSheetController?.close();
                          // Đặt lại về null ngay lập tức để nút 'Mở' có thể được nhấn lại.
                          setState(() {
                            _bottomSheetController = null;
                          });
                        }
                      : null, // Disable nút nếu sheet chưa mở
                  child: const Text('Đóng Persistent Bottom Sheet (từ ngoài)'),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: PersistentBottomSheetDemo()));
}

Trong ví dụ trên:

  • Chúng ta dùng Scaffold.of(innerContext).showBottomSheet để hiển thị bottom sheet. Hàm này trả về một PersistentBottomSheetController.
  • Chúng ta lưu controller này vào biến _bottomSheetController để có thể điều khiển nó sau này.
  • Khi muốn đóng sheet, chỉ cần gọi _bottomSheetController?.close(). Dễ như ăn kẹo!
  • _bottomSheetController?.closed.whenComplete(() { ... }); cho phép mấy đứa thực thi một hành động nào đó khi sheet được đóng (ví dụ: reset trạng thái, giải phóng tài nguyên).
Illustration

3. Mẹo (Best Practices) từ Creyt

  1. Luôn dùng Builder: Nhớ kỹ bài học về BuildContextScaffold.of(context). Builder là người bạn thân thiết nhất khi cần lấy context 'con' của Scaffold để gọi các phương thức như showBottomSheet hay showSnackBar.
  2. Quản lý _bottomSheetController: Đừng để nó 'lơ lửng' sau khi sheet đóng. Luôn đặt nó về null khi sheet không còn hiển thị (hoặc sau khi gọi close()) để tránh lỗi và cho phép sheet được mở lại.
  3. Xem xét DraggableScrollableSheet: Nếu mấy đứa muốn một bottom sheet có thể kéo lên xuống, thay đổi kích thước linh hoạt hơn và 'ôm' nội dung bên trong, DraggableScrollableSheet là một lựa chọn tuyệt vời. Nó không dùng PersistentBottomSheetController trực tiếp nhưng là một biến thể nâng cao của Persistent Sheet.
  4. UX là vua: Hỏi bản thân: Liệu đây có phải là PersistentBottomSheet hay ModalBottomSheet? Persistent phù hợp khi nội dung thứ cấp không cần chặn tương tác chính, và người dùng có thể muốn tham chiếu nó thường xuyên. Modal thì dành cho các tác vụ cần sự tập trung tuyệt đối.

4. Học thuật sâu của anh Creyt: Cơ chế bên trong

Khi mấy đứa gọi Scaffold.of(context).showBottomSheet(), thực chất là mấy đứa đang yêu cầu ScaffoldState (là State của Scaffold widget) tạo ra một OverlayEntry mới và thêm nó vào Overlay của toàn bộ ứng dụng. PersistentBottomSheetController mà mấy đứa nhận được chính là một 'cái tay cầm' để điều khiển cái OverlayEntry đó. Nó cho phép mấy đứa tương tác với OverlayEntry mà không cần biết chi tiết về cách nó được quản lý trong OverlayState.

closed property của controller là một Future. Nó sẽ hoàn thành (complete) khi bottom sheet được đóng. Đây là một cơ chế callback rất mạnh mẽ, giúp mấy đứa đồng bộ hóa các hành động khác trong ứng dụng với vòng đời của bottom sheet.

5. Ví dụ thực tế các ứng dụng/website đã ứng dụng

  • Google Maps: Khi mấy đứa tìm kiếm một địa điểm, thông tin chi tiết của địa điểm đó thường hiện ra ở một bottom sheet có thể kéo lên xuống. Mấy đứa vẫn có thể nhìn thấy bản đồ phía sau và tương tác với nó ở một mức độ nào đó. Đây chính là một dạng của persistent bottom sheet.
  • Spotify/Apple Music: Thanh 'Now Playing' ở dưới cùng màn hình là một ví dụ điển hình. Nó luôn hiển thị bài hát đang phát, và mấy đứa có thể kéo nó lên để xem chi tiết hoặc điều khiển phát nhạc. Nó 'persistent' và không chặn tương tác với danh sách bài hát chính.
  • Các ứng dụng mua sắm/đặt đồ ăn: Thường có một thanh giỏ hàng nhỏ ở dưới màn hình, hiển thị tổng số món và giá. Khi nhấn vào, nó có thể mở rộng thành một bottom sheet chi tiết hơn.

6. Thử nghiệm đã từng và nên dùng cho case nào?

Anh Creyt đã từng 'đau đầu' với việc làm sao để một mini-player (trình phát nhạc nhỏ) có thể luôn hiện diện và điều khiển được từ mọi màn hình trong ứng dụng mà không cần phải dùng Navigator.push phức tạp. PersistentBottomSheetController chính là vị cứu tinh!

Nên dùng cho các trường hợp:

  • Mini Media Player: Như Spotify, YouTube Music. Người dùng muốn điều khiển phát nhạc/video mà không cần rời khỏi màn hình hiện tại.
  • Bộ lọc/Tùy chọn nhanh: Một bảng điều khiển nhỏ ở dưới để thay đổi bộ lọc hoặc tùy chọn mà không che mất nội dung chính.
  • Thông tin ngữ cảnh: Hiển thị thông tin bổ sung liên quan đến nội dung hiện tại (ví dụ: chi tiết sản phẩm khi cuộn danh sách).
  • Giỏ hàng/Thông báo trạng thái: Một thanh nhỏ hiển thị tổng số mặt hàng trong giỏ hoặc trạng thái của một tác vụ dài hạn.

Nhớ nhé, PersistentBottomSheetController không chỉ là một cái tên dài dòng, nó là chìa khóa để mấy đứa tạo ra những trải nghiệm UI mượt mà, không gián đoạn và cực kỳ trực quan cho người dùng. Cứ thử và cảm nhận sức mạnh của nó đi!

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!