
Chào các đồng chí lập trình viên, anh Creyt lại lên sóng đây! Hôm nay, chúng ta sẽ cùng nhau 'mổ xẻ' một gã khá 'lạnh lùng' nhưng lại cực kỳ quyền năng trong thế giới Flutter: FlowDelegate. Nghe tên có vẻ khô khan, nhưng tin anh đi, nó chính là tay biên đạo múa bậc thầy cho các widget của bạn đấy!
FlowDelegate là gì và tại sao chúng ta cần đến nó?
Cứ hình dung thế này, trong Flutter, chúng ta thường dùng Row, Column, Stack, Wrap để sắp xếp các widget. Chúng nó giống như những 'đội trưởng' chỉ đạo đội hình vậy: ông A đứng đây, bà B đứng cạnh ông A, đứa C nằm chồng lên ông A... Rất tiện lợi, đúng không? Nhưng đời không như mơ, đôi khi bạn cần một màn trình diễn phức tạp hơn, nơi các widget không chỉ đứng yên một chỗ mà còn phải 'nhảy nhót', 'xoay vòng', hay 'tụm năm tụm ba' theo một quy luật rất riêng, và quan trọng nhất là phải mượt mà như bơ dù có hàng trăm 'vũ công' trên sân khấu.
Đó chính là lúc Flow và người cộng sự đắc lực của nó, FlowDelegate, bước ra ánh sáng. Flow là một widget cấp thấp, sinh ra để xử lý các bố cục phức tạp, đặc biệt là khi các phần tử con cần được sắp xếp theo một logic tùy chỉnh cao độ và có thể thay đổi vị trí một cách linh hoạt mà không cần phải 'đập đi xây lại' toàn bộ cây widget.
Còn FlowDelegate ư? Nó chính là bản thiết kế chi tiết của màn trình diễn đó. Nó định nghĩa chính xác cách thức các widget con của Flow được vẽ lên màn hình, từ vị trí, góc xoay cho đến kích thước. FlowDelegate cho phép bạn kiểm soát từng pixel mà không phải trả giá bằng hiệu năng, bởi vì nó chỉ tập trung vào việc vẽ lại vị trí của các widget con, chứ không phải xây lại chúng.
Cơ chế hoạt động: Khi bạn là "Tổng Đạo Diễn"
Khi sử dụng Flow, bạn sẽ cần cung cấp một FlowDelegate tùy chỉnh. Về cơ bản, bạn sẽ phải 'chấp bút' cho ba phương thức chính trong FlowDelegate:
-
paintChildren(FlowPaintingContext context): Đây là trái tim củaFlowDelegate. Nó giống như bạn đang đứng trên sân khấu và chỉ đạo từng vũ công một: "Anh A, ra giữa sân khấu, xoay 45 độ. Chị B, lùi về phía sau một chút, cao hơn anh A 10 pixel." Bạn sẽ dùngcontext.paintChild(index)và áp dụng cácMatrix4để di chuyển, xoay, scale từng widget con. Đây là nơi bạn định nghĩa toàn bộ logic bố cục. -
getSize(BoxConstraints constraints): Phương thức này trả về kích thước tổng thể củaFlowwidget. Nó giống như bạn nói với nhà sản xuất: "Sân khấu của tôi cần rộng chừng này, cao chừng kia để chứa hết các vũ công." Nó sẽ choFlowbiết nó nên chiếm bao nhiêu không gian trên màn hình. -
shouldRepaint(covariant FlowDelegate oldDelegate): Đây là "người gác cổng hiệu năng". Nó quyết định liệuFlowcó cần phải vẽ lại các widget con của nó hay không khiFlowDelegatethay đổi. Nếu bạn thay đổi một thuộc tính nào đó trongFlowDelegate(ví dụ: góc xoay, khoảng cách), phương thức này sẽ kiểm tra xem sự thay đổi đó có đủ lớn để yêu cầu vẽ lại không. Trả vềtruenếu cần vẽ lại,falsenếu không. Đây là chìa khóa để giữ cho ứng dụng của bạn mượt mà.

Code Ví Dụ: Tạo một Radial Menu "siêu ngầu"
Chúng ta hãy cùng tạo một menu hình tròn (Radial Menu) đơn giản. Khi bạn nhấn vào nút trung tâm, các nút chức năng khác sẽ 'bung' ra xung quanh nó như những cánh hoa.
Đầu tiên, chúng ta cần một FlowDelegate để định nghĩa cách các nút con sẽ được sắp xếp:
import 'dart:math' as math;
import 'package:flutter/material.dart';
class RadialMenuDelegate extends FlowDelegate {
final Animation<double> animation;
RadialMenuDelegate({required this.animation}) : super(repaint: animation);
@override
void paintChildren(FlowPaintingContext context) {
// Kích thước của widget con đầu tiên (nút trung tâm)
final double buttonSize = context.getChildSize(0)!.width;
// Bán kính đường tròn các nút con sẽ bung ra
final double radius = buttonSize * 1.5;
// Vẽ nút trung tâm
context.paintChild(0,
transform: Matrix4.translationValues(
(context.size.width - buttonSize) / 2,
(context.size.height - buttonSize) / 2,
0,
));
// Vẽ các nút con còn lại
for (int i = 1; i < context.childCount; i++) {
final double theta = i * (math.pi / (context.childCount - 2)) * animation.value; // Góc xoay
final double x = (context.size.width / 2) - (buttonSize / 2) + (radius * math.cos(theta));
final double y = (context.size.height / 2) - (buttonSize / 2) + (radius * math.sin(theta));
context.paintChild(i,
transform: Matrix4.translationValues(x, y, 0));
}
}
@override
Size getSize(BoxConstraints constraints) {
// Đảm bảo Flow có đủ không gian cho menu bung ra
return Size.square(constraints.maxWidth);
}
@override
bool shouldRepaint(covariant RadialMenuDelegate oldDelegate) {
return animation != oldDelegate.animation;
}
}
Và đây là cách chúng ta sử dụng RadialMenuDelegate với một Flow widget:
import 'package:flutter/material.dart';
// Import RadialMenuDelegate từ file trên
class RadialMenu extends StatefulWidget {
const RadialMenu({super.key});
@override
State<RadialMenu> createState() => _RadialMenuState();
}
class _RadialMenuState extends State<RadialMenu>
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();
} else {
_controller.reverse();
}
}
Widget _buildFab(IconData icon, VoidCallback onPressed) {
return RawMaterialButton(
onPressed: onPressed,
shape: const CircleBorder(),
padding: const EdgeInsets.all(16.0),
fillColor: Colors.blue,
child: Icon(icon, color: Colors.white),
);
}
@override
Widget build(BuildContext context) {
return Flow(
delegate: RadialMenuDelegate(animation: _controller),
children: <Widget>[
// Nút trung tâm
_buildFab(Icons.menu, _toggleMenu),
// Các nút con
_buildFab(Icons.edit, () => print('Edit')),
_buildFab(Icons.share, () => print('Share')),
_buildFab(Icons.add, () => print('Add')),
_buildFab(Icons.delete, () => print('Delete')),
],
);
}
}
// Để chạy thử, bạn có thể đặt RadialMenu vào Scaffold:
/*
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('FlowDelegate Example')),
body: Center(
child: SizedBox(
width: 300,
height: 300,
child: RadialMenu(),
),
),
),
));
}
*/
Trong ví dụ trên, RadialMenuDelegate dùng animation.value để tính toán góc xoay cho từng nút con, tạo hiệu ứng 'bung' ra hoặc 'thu vào' mượt mà. Matrix4.translationValues là công cụ để di chuyển widget con đến vị trí mong muốn.
Mẹo và Best Practices từ "Lão Làng" Creyt
- Khi nào thì dùng, khi nào thì "thôi đi ông"?
- Dùng khi: Bạn cần bố cục tùy chỉnh cao độ, đặc biệt là các bố cục động, hoạt ảnh mà vị trí các phần tử thay đổi liên tục nhưng bản thân các phần tử không thay đổi cấu trúc bên trong.
Flowtối ưu cho hiệu năng trong những trường hợp này vì nó chỉ vẽ lại, không xây lại. Ví dụ: menu hình tròn, tag cloud phức tạp, hiệu ứng xếp chồng card động. - Thôi đi ông khi: Các bố cục đơn giản, tĩnh, hoặc chỉ cần
Row,Column,Stack,Wraplà đủ. Đừng "vác dao mổ trâu đi giết gà" nhé.Flowphức tạp hơn để debug và duy trì.
- Dùng khi: Bạn cần bố cục tùy chỉnh cao độ, đặc biệt là các bố cục động, hoạt ảnh mà vị trí các phần tử thay đổi liên tục nhưng bản thân các phần tử không thay đổi cấu trúc bên trong.
- Hiệu năng là vàng: Nhớ kỹ, ưu điểm lớn nhất của
Flowlà hiệu năng. Nó tránh được việc tái tạo (rebuild) toàn bộ cây widget con khi chỉ vị trí của chúng thay đổi. Hãy tận dụngshouldRepaintmột cách thông minh để chỉ vẽ lại khi thực sự cần thiết. Matrix4là "người bạn thân": Hầu hết các phép biến đổi trongpaintChildrensẽ liên quan đếnMatrix4. Hãy làm quen với các phương thức nhưtranslationValues,rotationZ,scaleđể điều khiển vị trí, xoay, và kích thước của các widget con.- Debugging
Flowcó thể "lú": VìFlowhoạt động ở cấp độ thấp, nó không cung cấp các cơ chế ràng buộc bố cục tự động nhưRowhayColumn. Khi có lỗi về vị trí, bạn sẽ phải tự tính toán và kiểm tra các giá trịx,y,thetacủa mình. Hãy dùngprinthoặc debug mode để xem các giá trị tính toán được. - Caching calculations: Nếu logic tính toán vị trí của bạn phức tạp, hãy cân nhắc cache các giá trị trung gian để tránh tính toán lại không cần thiết trong mỗi frame.
Ứng dụng thực tế: "Cuộc sống là một sân khấu lớn"
FlowDelegate không phải là một ngôi sao thường xuyên xuất hiện trên các ứng dụng phổ thông, nhưng nó là một "ngôi sao thầm lặng" trong các trường hợp đặc biệt cần đến sự tinh tế và hiệu năng:
- Radial Action Buttons: Giống như ví dụ chúng ta vừa làm, nhiều ứng dụng có một nút hành động nổi (FAB) ở góc màn hình, khi nhấn vào, nó bung ra một loạt các tùy chọn nhỏ hơn theo hình quạt. Đây chính là mảnh đất màu mỡ cho
FlowDelegate. - Tag Clouds/Dynamic Tag Layouts: Trong các ứng dụng có nhiều thẻ (tag) cần hiển thị một cách linh hoạt, có thể chồng chéo hoặc sắp xếp ngẫu nhiên nhưng vẫn đảm bảo tính thẩm mỹ và hiệu năng cao.
- Custom Loading Animations: Các hiệu ứng loading phức tạp, nơi các phần tử nhỏ di chuyển theo quỹ đạo đặc biệt (ví dụ: xoay quanh một điểm, sắp xếp lại theo hình dạng động).
- Interactive Galleries/Image Viewers: Trong một số trường hợp đặc biệt, khi bạn muốn tạo hiệu ứng xem ảnh độc đáo, nơi các ảnh con có thể xoay, phóng to, thu nhỏ và di chuyển theo cử chỉ người dùng,
FlowDelegatecó thể là một công cụ mạnh mẽ.
Nhớ nhé, FlowDelegate không phải là công cụ bạn dùng hàng ngày, nhưng khi bạn cần một màn trình diễn bố cục "đỉnh cao", mượt mà và hiệu quả, nó chính là bí kíp cuối cùng trong túi đồ nghề của bạn. Hãy luyện tập và làm chủ nó để nâng tầm kỹ năng Flutter của mình lên một đẳng cấp mới! Chúc các bạn code vui vẻ!
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é!