
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ộtPersistentBottomSheetController. - 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).

3. Mẹo (Best Practices) từ Creyt
- Luôn dùng
Builder: Nhớ kỹ bài học vềBuildContextvàScaffold.of(context).Builderlà người bạn thân thiết nhất khi cần lấy context 'con' củaScaffoldđể gọi các phương thức nhưshowBottomSheethayshowSnackBar. - Quản lý
_bottomSheetController: Đừng để nó 'lơ lửng' sau khi sheet đóng. Luôn đặt nó vềnullkhi sheet không còn hiển thị (hoặc sau khi gọiclose()) để tránh lỗi và cho phép sheet được mở lại. - 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,DraggableScrollableSheetlà một lựa chọn tuyệt vời. Nó không dùngPersistentBottomSheetControllertrực tiếp nhưng là một biến thể nâng cao của Persistent Sheet. - UX là vua: Hỏi bản thân: Liệu đây có phải là
PersistentBottomSheethayModalBottomSheet? 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é!