
Chào mừng các "đệ tử" đến với bài giảng hôm nay của lão Creyt! Hôm nay, chúng ta sẽ "mổ xẻ" một khái niệm nghe có vẻ lạ mà lại quen vô cùng trong thế giới UI/UX: FlowMenu. Nghe tên thì hoành tráng, nhưng thực chất nó là một ý tưởng thiết kế và một kỹ thuật triển khai tinh tế trong Flutter, chứ không phải là một widget "mì ăn liền" như PopupMenuButton đâu nhé.
FlowMenu Là Gì? Để Làm Gì?
Cứ hình dung thế này, cái điện thoại của bạn là một căn phòng nhỏ. Mọi thứ bạn cần dùng mà cứ bày la liệt ra sàn nhà thì vừa chật, vừa rối mắt, phải không? FlowMenu chính là "cái tủ thần kỳ" của Doraemon, nơi bạn có thể cất gọn những món đồ (các hành động, chức năng) quan trọng, liên quan mật thiết với nhau. Khi cần, chỉ cần "mở tủ", chúng sẽ "bung lụa" ra một cách duyên dáng, có thể là hình quạt, hình tròn, hay một đường thẳng tắp, rồi lại thu gọn lại khi không dùng đến.
Mục đích cốt lõi của FlowMenu:
- Tiết kiệm không gian: Thay vì rải rác 3-4 nút hành động quan trọng chiếm chỗ, ta gói gọn chúng vào một nút duy nhất.
- Tăng tính thẩm mỹ: Các hiệu ứng chuyển động mượt mà, "bung nở" của FlowMenu tạo cảm giác hiện đại, chuyên nghiệp cho ứng dụng.
- Cải thiện trải nghiệm người dùng (UX): Gom nhóm các hành động liên quan giúp người dùng dễ dàng tìm thấy và thực hiện các tác vụ theo ngữ cảnh, giảm thiểu sự lộn xộn.
Nói tóm lại, FlowMenu là cách chúng ta biến cái "đống lộn xộn" thành một "vũ điệu" UI uyển chuyển, hiệu quả.
"Dòng Chảy" Của FlowMenu Trong Flutter: Widget Flow
Trong Flutter, để tạo ra "dòng chảy" (flow) của các widget con một cách tùy biến, chúng ta có một "ông trùm" chuyên trị việc này: Widget Flow. Đừng nhầm lẫn nó với Column hay Row nhé. Flow giống như một sân khấu riêng, nơi bạn là đạo diễn, toàn quyền quyết định vị trí (position) và kích thước (size) của từng "diễn viên" (widget con) theo từng "khung hình" (animation tick).
Điểm khác biệt lớn nhất của Flow so với Stack hay các layout widget khác là nó không tự động tính toán vị trí cho con. Thay vào đó, nó ủy quyền hoàn toàn việc này cho một FlowDelegate. Cái FlowDelegate này chính là "kịch bản" của bạn, nơi bạn viết ra cách mỗi widget con sẽ di chuyển, xuất hiện ở đâu khi menu mở ra hay đóng lại.
Các bước triển khai cơ bản:
AnimationController: "Nhạc trưởng" điều khiển tốc độ và trạng thái (mở/đóng) của animation.Tween: Định nghĩa khoảng giá trị mà animation sẽ chạy, ví dụ từ 0 đến 1.FlowWidget: "Sân khấu" chứa các nút hành động con và nút chính.FlowDelegatetùy chỉnh (CustomFlowDelegate): "Kịch bản" để tính toán vị trí của từng widget con dựa trên giá trị animation hiện tại.

Code Ví Dụ Minh Hoạ: Một FlowMenu Hình Quạt (Radial FlowMenu)
Để dễ hình dung, chúng ta sẽ xây dựng một FlowMenu đơn giản với nút chính ở góc dưới bên phải, khi nhấn vào sẽ "bung" ra ba nút con theo hình quạt. Chuẩn bị giấy bút (à quên, bàn phím) nào!
import 'package:flutter/material.dart';
import 'dart:math' as math;
// --- Custom Flow Delegate cho FlowMenu hình quạt ---
class RadialFlowDelegate extends FlowDelegate {
final Animation<double> animation;
RadialFlowDelegate({required this.animation}) : super(repaint: animation);
@override
void paintChildren(FlowPaintingContext context) {
final double xStart = context.size.width - 50.0; // Vị trí nút chính (x)
final double yStart = context.size.height - 50.0; // Vị trí nút chính (y)
for (int i = 0; i < context.childCount; i++) {
// Góc bắt đầu (ví dụ: 180 độ = pi radian) và phân bố đều
// Nút chính (child 0) luôn ở vị trí gốc
if (i == 0) {
context.paintChild(i, transform: Matrix4.translationValues(xStart, yStart, 0.0));
} else {
final double radius = 100.0 * animation.value; // Bán kính bung ra
final double angle = ((i - 1) * math.pi / 4) + math.pi; // Góc phân bố (từ 180 độ về phía trên trái)
final double x = xStart + (radius * math.cos(angle));
final double y = yStart + (radius * math.sin(angle));
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
}
}
}
@override
bool shouldRepaint(covariant RadialFlowDelegate oldDelegate) {
return animation != oldDelegate.animation;
}
@override
Size getSize(BoxConstraints constraints) {
return constraints.biggest; // Chiếm toàn bộ không gian có thể
}
}
// --- Widget chính của FlowMenu ---
class FlowMenuExample extends StatefulWidget {
const FlowMenuExample({super.key});
@override
State<FlowMenuExample> createState() => _FlowMenuExampleState();
}
class _FlowMenuExampleState extends State<FlowMenuExample> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleMenu() {
if (_controller.isDismissed) {
_controller.forward(); // Mở menu
} else {
_controller.reverse(); // Đóng menu
}
}
Widget _buildFab(IconData icon, VoidCallback onPressed) {
return FloatingActionButton(
heroTag: null, // Tránh lỗi heroTag trùng lặp nếu có nhiều FAB
mini: true,
onPressed: onPressed,
child: Icon(icon),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('FlowMenu của Thầy Creyt')),
body: Flow(
delegate: RadialFlowDelegate(animation: _controller),
children: <Widget>[
// Child 0: Nút chính để mở/đóng menu
_buildFab(
Icons.menu,
_toggleMenu,
),
// Child 1: Nút hành động 1
_buildFab(
Icons.add,
() => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Thêm mới!'))
),
),
// Child 2: Nút hành động 2
_buildFab(
Icons.edit,
() => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chỉnh sửa!'))
),
),
// Child 3: Nút hành động 3
_buildFab(
Icons.share,
() => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chia sẻ!'))
),
),
],
),
);
}
}
// --- Cách chạy ví dụ này trong main.dart ---
// void main() {
// runApp(const MyApp());
// }
// class MyApp extends StatelessWidget {
// const MyApp({super.key});
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// title: 'FlowMenu Demo',
// theme: ThemeData(primarySwatch: Colors.blue),
// home: const FlowMenuExample(),
// );
// }
// }
Giải thích sơ bộ về đoạn code:
RadialFlowDelegate: Đây là "linh hồn" của FlowMenu. Trong phương thứcpaintChildren, chúng ta tính toán vị tríxvàycho từng nút con dựa trên giá trịanimation.valuevà gócangle. Nút chính (child 0) thì giữ nguyên, các nút con còn lại (child 1, 2, 3...) sẽ "bung" ra từ nút chính theo hình quạt.animation.valuetừ 0 đến 1 sẽ điều khiển bán kínhradiustừ 0 đến 100.FlowMenuExample: LàStatefulWidgetđể quản lýAnimationController. Khi_toggleMenuđược gọi,_controllersẽ chạyforward()hoặcreverse()để mở/đóng menu.FlowWidget: NhậnRadialFlowDelegatecủa chúng ta và danh sách các widget con. Điều kỳ diệu sẽ xảy ra ở đây!
Mẹo & Best Practices (Mẹo của lão Creyt)
- Hiệu suất là Vàng:
Flowwidget được thiết kế khá tối ưu cho các layout động, phức tạp. Nó tránh việc tái xây dựng toàn bộ cây widget khi animation chạy, chỉ tập trung vào việc sơn lại (repaint) các con. Tuy nhiên, đừng quá lạm dụng với hàng trăm nút con, điều gì quá cũng không tốt! - Trải nghiệm Người dùng là Thượng đế:
- Phản hồi rõ ràng: Khi người dùng chạm vào nút chính, hãy đảm bảo có hiệu ứng để họ biết menu sắp mở ra hoặc đóng lại.
- Dễ dàng đóng: Ngoài việc chạm vào nút chính, có thể cân nhắc thêm chức năng đóng menu khi chạm ra ngoài khu vực menu (sử dụng
GestureDetectorbao quanhFlow). - Số lượng vừa phải: FlowMenu đẹp nhất khi chứa 3-5 hành động quan trọng. Nhiều quá sẽ làm rối mắt và khó chọn.
- Accessibility (Khả năng tiếp cận): Đừng quên người dùng khiếm thị hoặc dùng bàn phím. Đảm bảo các nút con có
semanticsLabelrõ ràng và có thể focus được qua phím Tab (nếu là ứng dụng desktop/web). Flutter đã hỗ trợ khá tốt cho điều này, nhưng bạn cần kiểm tra lại. - Tùy biến không giới hạn:
FlowDelegatelà "sân chơi" của bạn. Muốn menu bung ra hình xoắn ốc? Hình zigzag? Hay theo một đường cong Bézier? Cứ thoải mái "vẽ" trongpaintChildren! - Ngữ cảnh là Chìa khóa: Chỉ sử dụng FlowMenu khi các hành động thực sự liên quan đến nhau và có thể nhóm lại một cách logic. Đừng biến nó thành "cái kho chứa đồ lặt vặt"!
Ứng dụng Thực tế: "Những Ông Lớn" Đã Dùng FlowMenu?
Tuy không phải là một widget có tên gọi "FlowMenu" cụ thể, nhưng ý tưởng và cơ chế hoạt động của nó đã và đang được rất nhiều ứng dụng lớn áp dụng dưới các hình thức khác nhau, thường được gọi là "Speed Dial" hoặc "Radial Menu":
- Ứng dụng Chỉnh sửa Ảnh (ví dụ: Adobe Lightroom Mobile, Snapseed): Thường có các menu tròn hoặc bán nguyệt để chọn nhanh các công cụ (cắt, xoay, bộ lọc, v.v.). Khi bạn chọn một công cụ, các biểu tượng khác sẽ ẩn đi.
- Ứng dụng Ghi chú (ví dụ: Google Keep, Evernote): Nút "+" thường bung ra các tùy chọn như "Thêm ghi chú", "Thêm danh sách", "Thêm ảnh", "Thêm bản vẽ".
- Ứng dụng Mạng xã hội/Chat (ví dụ: Facebook Messenger, Telegram): Nút đính kèm trong khung chat thường bung ra các tùy chọn như "Ảnh", "Video", "Tệp", "Vị trí", "Liên hệ".
- Ứng dụng Quản lý Tác vụ/Dự án: Nhiều ứng dụng sử dụng kiểu menu này để thêm nhanh các loại tác vụ khác nhau (task, event, note).
FlowMenu không chỉ là một kỹ thuật lập trình, nó là một tư duy thiết kế để làm cho ứng dụng của bạn không chỉ hoạt động tốt mà còn "đẹp mắt" và "thông minh" hơn. Hãy thử nghiệm và sáng tạo với nó, các đệ tử 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é!