
OverlayEntry: 'Tấm Kính Ma Thuật' Phủ Lên Màn Hình
Chào các Gen Z mê code! Hôm nay, anh Creyt sẽ bật mí cho tụi bây một 'bí kíp' trong Flutter mà khi hiểu rồi, tụi bây sẽ thấy nó bá đạo vãi chưởng: OverlayEntry. Nghe thì học thuật, nhưng thực ra nó là một 'công cụ' giúp UI của tụi bây trở nên linh hoạt và 'nghịch ngợm' hơn rất nhiều.
1. OverlayEntry là gì và để làm gì?
Để dễ hình dung, tụi bây cứ tưởng tượng màn hình điện thoại của mình là một chồng giấy vẽ. Mỗi tờ giấy là một widget, và bình thường, tụi bây vẽ lên từng tờ, tờ nào ở trên thì che tờ dưới. Tất cả đều nằm trong một 'khung' cố định, gọi là cây widget (widget tree).
Nhưng đôi khi, tụi bây muốn vẽ một cái gì đó không thuộc về bất kỳ tờ giấy nào, mà nó lại nằm lơ lửng trên cùng của cả chồng giấy đó, như một cái tấm kính trong suốt tụi bây đặt lên trên cùng vậy. Tấm kính này không làm xê dịch hay thay đổi các tờ giấy bên dưới, nhưng nó hiện ra lồ lộ cho tụi bây thấy. Khi nào không cần nữa thì gỡ tấm kính ra.
Đó chính là OverlayEntry!
Nói một cách 'chuẩn chỉnh' hơn, OverlayEntry là một cánh cửa cho phép tụi bây chèn một widget vào Overlay widget – một widget đặc biệt nằm trên cùng của Navigator (cái quản lý các màn hình của app). Điều này có nghĩa là widget của tụi bây sẽ được hiển thị trên tất cả các widget khác trong màn hình hiện tại, không bị giới hạn bởi clip (cắt xén) hay overflow (tràn) của các widget cha.
Để làm gì ư? Đơn giản là để tạo ra những UI 'đột biến':
- Tooltips 'bay lượn': Mấy cái chú thích nhỏ hiện ra khi tụi bây chạm/giữ vào một icon nào đó.
- Context Menus 'ma thuật': Mấy cái menu nhỏ hiện ra khi tụi bây nhấn giữ, mà nó có thể hiện ra ở bất cứ đâu trên màn hình, không bị 'nhốt' trong một khung nào cả.
- Custom Popups/Modals 'độc lạ': Thay vì dùng
showDialogmặc định, tụi bây có thể tự tay tạo ra một cái popup siêu cá tính, có animation riêng, vị trí riêng. - Onboarding Hints/Spotlights: Mấy cái hướng dẫn 'nhấp nháy' để chỉ tụi bây cách dùng app lần đầu tiên.
2. Code Ví Dụ Minh Hoạ Rõ Ràng
Giờ thì 'xắn tay áo' vào code thôi! Anh Creyt sẽ chỉ tụi bây cách tạo một cái tooltip đơn giản dùng OverlayEntry.

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: 'OverlayEntry Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
OverlayEntry? _overlayEntry; // Biến để giữ OverlayEntry
final LayerLink _layerLink = LayerLink(); // Để định vị overlay widget
void _showOverlay(BuildContext context) {
// Nếu overlay đã tồn tại, không làm gì cả hoặc remove cái cũ đi
if (_overlayEntry != null) return;
// Lấy OverlayState từ context
final OverlayState overlayState = Overlay.of(context);
// Lấy vị trí và kích thước của widget gốc (nút button)
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final Size size = renderBox.size; // Kích thước của button
final Offset offset = renderBox.localToGlobal(Offset.zero); // Vị trí global của button
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
// Vị trí của OverlayEntry, đặt bên dưới nút button một chút
left: offset.dx,
top: offset.dy + size.height + 8.0, // Đặt dưới button 8 pixel
width: size.width, // Chiều rộng bằng button
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 8.0), // Vị trí tương đối với button
child: Material(
elevation: 4.0,
borderRadius: BorderRadius.circular(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Đây là một tooltip tùy chỉnh!',
style: TextStyle(color: Colors.black, fontSize: 14),
),
),
),
),
),
);
// Chèn OverlayEntry vào OverlayState
overlayState.insert(_overlayEntry!);
}
void _hideOverlay() {
if (_overlayEntry != null) {
_overlayEntry!.remove(); // Xóa OverlayEntry khỏi màn hình
_overlayEntry = null; // Đặt lại biến
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('OverlayEntry Magic')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CompositedTransformTarget(
link: _layerLink,
child: ElevatedButton(
onPressed: () {},
onLongPress: () => _showOverlay(context),
onTapCancel: _hideOverlay, // Ẩn khi người dùng bỏ tay ra
child: const Text('Nhấn giữ để xem tooltip'),
),
),
const SizedBox(height: 100),
const Text('Nội dung khác của màn hình'),
],
),
),
);
}
@override
void dispose() {
_hideOverlay(); // Đảm bảo overlay được xóa khi widget bị dispose
super.dispose();
}
}
Giải thích code:
_overlayEntryvà_layerLink:_overlayEntrylà biến để giữ tham chiếu đếnOverlayEntrycủa chúng ta, để sau này cònremovenó đi._layerLinklà một đối tượng 'siêu năng lực' giúp chúng ta định vịOverlayEntrymột cách tương đối so với một widget khác (ở đây làElevatedButton)._showOverlay(BuildContext context):Overlay.of(context): Đây là cách chúng ta 'với tay' tới cáiOverlaywidget nằm trên cùng của cây widget. Nó giống như xin phép 'thần đèn' để được đặt tấm kính ma thuật lên vậy.renderBox,size,offset: Đoạn này hơi 'khó nhằn' một tí nhưng quan trọng. Nó giúp chúng ta biết chính xácElevatedButtonđang nằm ở đâu trên màn hình và to bao nhiêu, để mình đặt cái tooltip cho đúng vị trí 'hợp lý' (ví dụ: ngay dưới nút).OverlayEntry(builder: (context) => Positioned(...)): Đây là nơi chúng ta định nghĩa widget sẽ hiển thị trên 'tấm kính'.Positionedgiúp đặt widget ở vị trí cụ thể.CompositedTransformFollowerlà 'bạn thân' củaCompositedTransformTarget(đặt ởElevatedButton), nó sẽ giúp cái tooltip 'đi theo' nút bấm nếu nút đó di chuyển.overlayState.insert(_overlayEntry!): 'Thần đèn' đã cho phép, giờ thì 'đặt tấm kính' lên thôi! Widget trong_overlayEntrysẽ hiện ra.
_hideOverlay(): Khi không cần nữa, chúng ta gọi_overlayEntry!.remove(). Nhớ là phảiremovenó đi, không thì nó cứ nằm đó mãi, vừa tốn bộ nhớ vừa gây lỗi.onLongPressvàonTapCancel: Chúng ta dùngonLongPressđể hiện tooltip vàonTapCancelđể ẩn nó đi khi người dùng nhấc ngón tay ra khỏi nút.dispose(): Cực kỳ quan trọng! Đảm bảo_hideOverlay()được gọi khiHomeScreenbị dispose để tránh rò rỉ bộ nhớ. Đừng quên cái này nha, không là app của tụi bây sẽ 'khóc thét' đó.
3. Một Vài Mẹo (Best Practices) Từ Anh Creyt
- Luôn luôn
remove(): Đây là quy tắc vàng!OverlayEntrykhông tự động biến mất. Nếu tụi bâyinsertmà khôngremove, nó sẽ nằm đó mãi mãi, gây rò rỉ bộ nhớ và có thể chặn các tương tác UI khác. Tưởng tượng một cái popup cứ lơ lửng dù app đã chuyển màn hình, phiền phức vãi. - Quản lý
Statecẩn thận: Widget trongOverlayEntrycũng là một widget bình thường. Nếu nó cóstate, tụi bây phải quản lý nó như bất kỳStatefulWidgetnào khác. Đôi khi dùngStatefulBuilderbên trongOverlayEntrycũng là một cách hay. - Vị trí là tất cả: Dùng
Positioned,AlignhoặcCompositedTransformTarget/Followerđể định vị widget của tụi bây một cách chính xác. Đừng để nó hiện ra 'vô duyên' giữa màn hình. - Thận trọng với
BuildContext:BuildContextdùng để tạoOverlayEntryphải làBuildContextcủa một widget nằm trongNavigator(thường làScaffoldhoặc một widget con của nó). Đừng dùngBuildContextcủaMaterialApphayWidgetsApptrực tiếp nhé. - Animation 'thần thánh':
OverlayEntryrất hợp để làm mấy cái animation 'mượt mà'. Tụi bây có thể bọc widget của mình trongFadeTransition,ScaleTransitionđể nó hiện ra/biến mất trông 'có gu' hơn.
4. Ví Dụ Thực Tế Các Ứng Dụng Đã Ứng Dụng
OverlayEntry không phải là cái gì đó 'xa xỉ', mà nó được dùng rất nhiều trong các app hàng ngày, đôi khi tụi bây không để ý thôi:
- Facebook/Instagram: Mấy cái pop-up thông báo nhỏ khi tụi bây comment, like bài viết, hoặc mấy cái menu tùy chọn hiện ra khi nhấn giữ một post. Chúng thường không nằm trong luồng UI chính mà 'nổi' lên trên.
- Google Maps/Apple Maps: Khi tụi bây chạm vào một điểm trên bản đồ, một cái card thông tin nhỏ sẽ hiện ra, nó 'nổi' lên trên bản đồ mà không làm thay đổi layout của bản đồ.
- Các ứng dụng chỉnh sửa ảnh/video: Mấy cái thanh công cụ phụ, bảng màu, hoặc các tùy chọn nhỏ hiện ra khi tụi bây chọn một công cụ nào đó, chúng thường được tạo bằng cách tương tự để không làm rối giao diện chính.
- App có onboarding tour: Mấy cái 'spotlight' chỉ dẫn người dùng lần đầu, làm nổi bật từng phần của giao diện. Đó chính là
OverlayEntryđó!
5. Thử Nghiệm Của Anh Creyt và Hướng Dẫn Nên Dùng Cho Case Nào
Anh Creyt đã từng 'đau đầu' với việc làm sao để một cái tooltip hiện ra ngay cạnh con chuột (trên desktop) mà không bị giới hạn bởi cái Card mẹ của nó. Dùng OverlayEntry là giải pháp 'chân ái'. Hoặc mấy cái pop-up thông báo lỗi mà nó bay lơ lửng giữa màn hình, không cần phải chèn vào bất kỳ Column hay Row nào cả.
Khi nào nên dùng OverlayEntry?
- Khi tụi bây cần một UI element 'nổi' lên trên tất cả: Như tooltips, context menus, custom dropdowns, hoặc các loại pop-up không theo kiểu
AlertDialogtruyền thống. - Khi UI element đó cần vị trí linh hoạt: Nó không cần phải là con của một widget cụ thể nào mà có thể xuất hiện ở bất cứ đâu trên màn hình, thậm chí 'đi theo' một widget khác.
- Khi tụi bây muốn kiểm soát hoàn toàn vòng đời của UI element đó: Tự tay
insertvàremove, tự tay quản lý animation.
Khi nào không nên dùng OverlayEntry?
- Khi một
AlertDialog,SnackBar,BottomSheethoặcPopupMenuButtonmặc định đủ dùng: Đừng 'làm quá' nếu Flutter đã cung cấp sẵn giải pháp đơn giản và hiệu quả.OverlayEntrymạnh nhưng cũng phức tạp hơn để quản lý. - Khi UI element là một phần cố định của layout: Nếu nó luôn nằm trong một
Column,RowhayStackvà không cần 'nổi' lên trên các widget khác, thì cứ dùng widget bình thường thôi.
Tóm lại, OverlayEntry là một 'vũ khí' lợi hại trong kho tàng Flutter của tụi bây. Hãy dùng nó một cách thông minh và có trách nhiệm, và tụi bây sẽ tạo ra những trải nghiệm người dùng 'đỉnh của chóp'!
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é!