OverlayEntry: Khi UI của bạn cần 'nhảy dù' khỏi cây widget!
Flutter

OverlayEntry: Khi UI của bạn cần 'nhảy dù' khỏi cây widget!

Author

Admin System

@root

Ngày xuất bản

20 Mar, 2026

Lượt xem

2 Lượt

"OverlayEntry"

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 showDialog mặ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.

Illustration

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:

  1. _overlayEntry_layerLink: _overlayEntry là biến để giữ tham chiếu đến OverlayEntry của chúng ta, để sau này còn remove nó đi. _layerLink là một đối tượng 'siêu năng lực' giúp chúng ta định vị OverlayEntry một cách tương đối so với một widget khác (ở đây là ElevatedButton).
  2. _showOverlay(BuildContext context):
    • Overlay.of(context): Đây là cách chúng ta 'với tay' tới cái Overlay widget 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ác ElevatedButton đ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'. Positioned giúp đặt widget ở vị trí cụ thể. CompositedTransformFollower là 'bạn thân' của CompositedTransformTarget (đặ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 _overlayEntry sẽ hiện ra.
  3. _hideOverlay(): Khi không cần nữa, chúng ta gọi _overlayEntry!.remove(). Nhớ là phải remove nó đi, không thì nó cứ nằm đó mãi, vừa tốn bộ nhớ vừa gây lỗi.
  4. onLongPressonTapCancel: Chúng ta dùng onLongPress để hiện tooltip và onTapCancel để ẩn nó đi khi người dùng nhấc ngón tay ra khỏi nút.
  5. dispose(): Cực kỳ quan trọng! Đảm bảo _hideOverlay() được gọi khi HomeScreen bị 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! OverlayEntry không tự động biến mất. Nếu tụi bây insert mà không remove, 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ý State cẩn thận: Widget trong OverlayEntry cũ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ỳ StatefulWidget nào khác. Đôi khi dùng StatefulBuilder bên trong OverlayEntry cũng là một cách hay.
  • Vị trí là tất cả: Dùng Positioned, Align hoặc CompositedTransformTarget/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: BuildContext dùng để tạo OverlayEntry phải là BuildContext của một widget nằm trong Navigator (thường là Scaffold hoặc một widget con của nó). Đừng dùng BuildContext của MaterialApp hay WidgetsApp trực tiếp nhé.
  • Animation 'thần thánh': OverlayEntry rấ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 trong FadeTransition, 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 AlertDialog truyề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 insertremove, tự tay quản lý animation.

Khi nào không nên dùng OverlayEntry?

  • Khi một AlertDialog, SnackBar, BottomSheet hoặc PopupMenuButton mặ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ả. OverlayEntry mạ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, Row hay Stack và 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é!

#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!