Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
PageStorageBucket: Túi Thần Ký Ức Cuộn Trang - Giữ Vững Phong Độ!
20/03/2026

PageStorageBucket: Túi Thần Ký Ức Cuộn Trang - Giữ Vững Phong Độ!

PageStorageBucket: Cái Túi Thần Kỳ Lưu Giữ Ký Ức Cuộn Trang Chào các chiến thần Gen Z! Hôm nay, anh Creyt sẽ cùng các em 'phẫu thuật' một khái niệm nghe hơi… 'sách vở' nhưng lại cực kỳ 'thực chiến' trong Flutter: PageStorageBucket và PageStorageKey. Nghe tên có vẻ phức tạp, nhưng tin anh đi, nó chính là 'người hùng thầm lặng' giúp trải nghiệm app của các em 'mượt như lụa' đó! 1. PageStorageBucket là gì và để làm gì? (Hay: Tại sao cái list của mình cứ 'mất trí' hoài vậy?) Các em có bao giờ lướt TikTok, cuộn đến mỏi tay, thấy một cái video hay ho rồi bấm vào xem profile của đứa đăng không? Sau đó, bấm nút back quay lại feed, phù, cái feed vẫn y nguyên ở vị trí em vừa cuộn tới, chứ không phải 'nhảy' về đầu trang đúng không? Đó chính là 'phép thuật' của việc lưu giữ trạng thái cuộn (scroll position) đó. Trong Flutter, các widget có khả năng cuộn như ListView, GridView, CustomScrollView... khi chúng ta rời khỏi màn hình (ví dụ: navigate sang màn hình khác) rồi quay lại, theo 'mặc định' thì chúng sẽ... 'mất trí nhớ'. Tức là, chúng sẽ reset về vị trí cuộn ban đầu (thường là đầu trang). Tưởng tượng đang cuộn một danh sách sản phẩm dài dằng dặc, thấy cái ưng ý, bấm vào xem chi tiết, rồi quay lại thì nó lại 'nhảy' lên đầu. Bực mình không? Bực mình chứ! Đây chính là lúc PageStorageBucket 'lên sàn'. Các em cứ hình dung nó như một cái 'tủ hồ sơ' thông minh, hoặc chuẩn hơn là một cái 'túi thần kỳ' có khả năng 'ghi nhớ' vị trí cuộn của từng widget scrollable. Khi một widget scrollable được gắn vào một PageStorageBucket, nó sẽ tự động lưu lại vị trí cuộn của mình vào cái túi đó trước khi bị 'biến mất' khỏi màn hình. Và khi nó 'quay trở lại', cái túi sẽ 'nhắc nhở' nó về vị trí cũ. Tuyệt vời chưa! Còn PageStorageKey là gì? Đơn giản thôi. Nếu PageStorageBucket là cái tủ hồ sơ, thì mỗi cái PageStorageKey chính là cái 'nhãn' hay 'mã số' duy nhất mà các em dán lên từng 'hồ sơ' (tức là từng widget scrollable). Nhờ có cái nhãn này, cái tủ mới biết 'ký ức cuộn' này là của 'ai', để sau này trả lại đúng chỗ. Không có PageStorageKey, cái tủ sẽ không biết phải lưu hay lấy ký ức cho widget nào đâu nha! 2. Code Ví Dụ Minh Họa: 'Hồi Ức' Cho ListView Để các em dễ hình dung, anh Creyt sẽ dựng một ví dụ đơn giản: Một app có 2 màn hình. Màn hình đầu tiên là một ListView dài, màn hình thứ hai là một màn hình chi tiết. Chúng ta sẽ xem khi có và không có PageStorageKey, trải nghiệm sẽ khác nhau như thế nào. Bước 1: Chuẩn bị app cơ bản import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { // MaterialApp tự động cung cấp một PageStorageBucket mặc định rồi đó các em. // Nên thường chúng ta không cần bọc thêm PageStorageBucket bên ngoài nữa. return MaterialApp( title: 'Flutter PageStorageBucket 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> { // Tạo một PageStorageKey duy nhất cho ListView này. // Đây là 'cái nhãn' để PageStorageBucket nhận diện và lưu trữ vị trí cuộn. static const PageStorageKey _scrollKey = PageStorageKey('myScrollableList'); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Màn hình chính - List dài dằng dặc'), ), body: ListView.builder( // Đây là chỗ mấu chốt: gắn PageStorageKey vào ListView! // Hãy thử comment dòng này và chạy lại để xem sự khác biệt nhé! key: _scrollKey, itemCount: 100, // Một list dài 100 items cho đã tay cuộn. itemBuilder: (context, index) { return Card( margin: const EdgeInsets.all(8.0), child: ListTile( title: Text('Item số $index'), subtitle: Text('Đây là chi tiết của item $index'), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(itemIndex: index), ), ); }, ), ); }, ), ); } } class DetailScreen extends StatelessWidget { final int itemIndex; const DetailScreen({super.key, required this.itemIndex}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Màn hình chi tiết'), ), body: Center( child: Text( 'Bạn đang xem chi tiết Item số $itemIndex', style: const TextStyle(fontSize: 24), ), ), ); } } Giải thích ví dụ: MyApp: Là widget gốc, MaterialApp tự động tạo ra một PageStorageBucket ở cấp độ cao nhất. Điều này có nghĩa là mọi widget con bên dưới nó đều có thể truy cập và sử dụng PageStorageBucket này. Thường thì các em không cần tự tạo thêm PageStorageBucket đâu. HomeScreen: Chứa ListView.builder. Đây là nơi chúng ta cần lưu giữ vị trí cuộn. _scrollKey = PageStorageKey('myScrollableList'): Đây là 'chìa khóa' quan trọng nhất. Anh Creyt đã tạo một PageStorageKey với một giá trị chuỗi duy nhất ('myScrollableList'). Giá trị chuỗi này có thể là bất cứ thứ gì miễn là nó duy nhất trong phạm vi các widget scrollable mà em muốn lưu trạng thái cuộn. key: _scrollKey: Chúng ta gán _scrollKey này vào thuộc tính key của ListView.builder. Chính nhờ dòng này mà ListView của chúng ta 'có trí nhớ'. Khi em cuộn xuống, bấm vào một item, chuyển sang DetailScreen, rồi pop (quay lại) HomeScreen, ListView sẽ tự động cuộn về đúng vị trí mà em đã rời đi. Thử nghiệm: Chạy lần 1 (có key: _scrollKey): Cuộn xuống giữa list, bấm vào một item, quay lại. Thấy list vẫn ở vị trí cũ. Tuyệt vời! Chạy lần 2 (comment dòng key: _scrollKey): Cuộn xuống giữa list, bấm vào một item, quay lại. Thấy list 'nhảy' về đầu trang. Bực mình không? Đó là sự khác biệt đó! 3. Mẹo Vặt & Best Practices Từ Anh Creyt (Để không bị 'lú' giữa đường) PageStorageKey là 'linh hồn': Luôn nhớ gán một PageStorageKey cho các widget scrollable mà em muốn lưu trữ vị trí cuộn. Không có nó là 'mất trí' ngay! Đảm bảo Key là duy nhất: Mỗi PageStorageKey nên là duy nhất trong phạm vi mà nó hoạt động. Nếu có hai ListView cùng một PageStorageKey trong cùng một PageStorageBucket, chúng sẽ 'đánh nhau' để giành quyền lưu trữ, và kết quả là không ai nhớ đúng cả. Không phải 'thần dược' cho mọi loại state: PageStorageBucket được thiết kế đặc biệt để lưu vị trí cuộn. Đừng cố gắng dùng nó để lưu các loại state phức tạp khác của widget (như dữ liệu đã nhập vào form, trạng thái bật/tắt của switch...). Đối với các loại state đó, em cần dùng các giải pháp quản lý state khác như Provider, Bloc, Riverpod... Vị trí của PageStorageBucket: Như đã nói, MaterialApp mặc định đã cung cấp một PageStorageBucket rồi. Nhưng nếu em có một cấu trúc widget phức tạp hơn và muốn các Bucket riêng biệt cho các phần khác nhau của ứng dụng, em hoàn toàn có thể bọc một phần widget tree bằng PageStorageBucket mới. Tuy nhiên, trong hầu hết các trường hợp, Bucket mặc định là đủ. 4. Ứng Dụng Thực Tế (Ở Đâu Rồi?) Nói đâu xa, các em đang dùng PageStorageBucket (hoặc các cơ chế tương tự trong các framework khác) hàng ngày mà không hay biết đó: Mạng xã hội: Instagram, Facebook, TikTok... Khi cuộn feed, xem profile, rồi quay lại, feed vẫn ở đúng chỗ. Ứng dụng đọc tin tức: Các app như VnExpress, Zing News... cuộn danh sách bài viết, bấm vào đọc một bài, rồi quay lại, danh sách vẫn giữ nguyên vị trí. Thương mại điện tử: Shopee, Lazada, Tiki... cuộn danh sách sản phẩm, xem chi tiết, rồi quay lại, danh sách vẫn 'yên vị'. 5. Thử Nghiệm Đã Từng và Hướng Dẫn Nên Dùng Cho Case Nào Anh Creyt đã từng 'đau đầu' với việc các ListView cứ 'mất trí nhớ' khi làm các app có nhiều tab, mỗi tab là một danh sách. Ban đầu không biết PageStorageBucket, cứ nghĩ phải tự lưu scrollOffset vào Provider hay Bloc, rất lằng nhằng và tốn công. Đến khi phát hiện ra PageStorageKey, mọi thứ như 'mở cờ trong bụng'! Nên dùng khi nào? Khi em có các widget scrollable (như ListView, GridView, CustomScrollView, PageView...) mà người dùng mong muốn trạng thái cuộn được giữ lại khi họ điều hướng tạm thời ra khỏi màn hình đó và quay lại. Đặc biệt hữu ích trong các ứng dụng có cấu trúc BottomNavigationBar hoặc TabBarView nơi các tab chứa các danh sách cuộn. Không nên dùng khi nào? Khi nội dung của danh sách thay đổi quá thường xuyên hoặc quá nhanh đến mức việc giữ lại vị trí cuộn không còn ý nghĩa (ví dụ: một danh sách chat real-time mà tin nhắn mới luôn đẩy lên đầu). Đối với các danh sách quá ngắn, việc reset về đầu trang không gây khó chịu cho người dùng. Vậy đó, PageStorageBucket và PageStorageKey không phải là thứ gì đó 'cao siêu' khó hiểu. Nó chỉ là một 'công cụ' nhỏ nhưng cực kỳ hiệu quả để làm cho app Flutter của các em 'có tâm hồn' hơn, 'nhân văn' hơn, và mang lại trải nghiệm người dùng 'đỉnh của chóp'. Thực hành ngay đi nhé các chiến thần! 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é!

40 Đọc tiếp
PageMetrics Flutter: GPS cho trang của bạn, Gen Z ơi!
20/03/2026

PageMetrics Flutter: GPS cho trang của bạn, Gen Z ơi!

Chào các Gen Z mê code, anh Creyt đây! Hôm nay chúng ta sẽ cùng “mổ xẻ” một khái niệm tuy nhỏ mà có võ, giúp các em “nắm thóp” được mọi chuyển động của các trang trong app Flutter của mình: đó là PageMetrics. Nghe thì có vẻ hàn lâm, nhưng thật ra nó lại là một “GPS” siêu xịn sò cho mấy cái PageView của tụi mình đấy! 1. PageMetrics là gì mà “thần thánh” vậy? Tưởng tượng thế này: các em đang lướt TikTok, lướt Instagram Story hoặc xem một cuốn catalogue sản phẩm online. Mấy cái đó đều có dạng “trang” mà mình vuốt qua vuốt lại đúng không? PageView trong Flutter chính là cái hộp thần kỳ để chứa mấy cái trang đó. Thế thì, PageMetrics chính là bộ cảm biến siêu thông minh được gắn vào cái hộp PageView ấy. Nó không chỉ báo cho em biết “đang ở trang số mấy” mà còn chi tiết hơn nhiều: “trang đó đang hiển thị bao nhiêu phần trăm?”, “đã vuốt được bao nhiêu pixel rồi?”, “trang kế tiếp đã lấp ló được bao nhiêu?”. Nói chung, nó là bảng điều khiển toàn diện cho mọi chuyển động của các trang trong PageView của em. Nó sinh ra là để làm gì ư? Đơn giản thôi: để em có thể tạo ra những hiệu ứng UI “mượt như nhung”, những thanh chỉ số trang (page indicator) thông minh, hay thậm chí là những màn hình onboarding “đỉnh của chóp” mà nội dung thay đổi theo từng milimet chuyển động của ngón tay người dùng. Nó biến một PageView tĩnh thành một vũ đài sống động! Về mặt kỹ thuật, PageMetrics là một subclass của ScrollMetrics. ScrollMetrics thì rộng hơn, nó mô tả trạng thái của bất kỳ thành phần nào có thể cuộn (scroll) được. Còn PageMetrics thì chuyên biệt hóa cho PageView, nơi mà khái niệm "trang" là cốt lõi. Các thuộc tính quan trọng nhất của PageMetrics mà anh em mình cần nhớ như in: page (double): Đây là số trang hiện tại. Nhưng đừng nghĩ nó chỉ là số nguyên nhé! Khi em vuốt giữa trang 1 và trang 2, nó có thể là 0.5, 0.7, 1.2, 1.9... Chính cái giá trị double này mới là "vàng" để tạo hiệu ứng động đó. pixels (double): Tổng số pixel đã cuộn từ đầu PageView. Giống như tổng quãng đường đã đi vậy. viewportFraction (double): Phần trăm chiều rộng (hoặc chiều cao nếu cuộn dọc) của viewport mà một trang chiếm. Mặc định là 1.0 (toàn bộ viewport là một trang). Nếu em muốn tạo hiệu ứng mà trang bên cạnh lấp ló một chút, em sẽ chỉnh cái này. viewportDimension (double): Kích thước (chiều rộng hoặc chiều cao) của vùng hiển thị (viewport) của PageView. 2. Code Ví Dụ: PageMetrics “lên sóng” Để thấy rõ PageMetrics hoạt động thế nào, chúng ta sẽ làm một ví dụ đơn giản: một PageView với 3 trang, và một cái Text hiển thị số trang hiện tại (dạng double) khi chúng ta vuốt. 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: 'PageMetrics Demo by Creyt', theme: ThemeData( primarySwatch: Colors.blue, ), home: const PageMetricsScreen(), ); } } class PageMetricsScreen extends StatefulWidget { const PageMetricsScreen({super.key}); @override State<PageMetricsScreen> createState() => _PageMetricsScreenState(); } class _PageMetricsScreenState extends State<PageMetricsScreen> { double _currentPage = 0.0; // Biến để lưu trữ số trang hiện tại @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('PageMetrics Demo'), ), body: Column( children: [ // Hiển thị số trang hiện tại Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Trang hiện tại: ${_currentPage.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), ), Expanded( child: NotificationListener<ScrollNotification>( onNotification: (ScrollNotification notification) { // Kiểm tra nếu đây là Notification từ PageView và có PageMetrics if (notification.metrics is PageMetrics) { final pageMetrics = notification.metrics as PageMetrics; // Cập nhật trạng thái khi trang thay đổi if (_currentPage != pageMetrics.page) { setState(() { _currentPage = pageMetrics.page!; // page có thể null nếu chưa khởi tạo }); } } // Quan trọng: Trả về false để notification tiếp tục được lan truyền // hoặc true để dừng lại ở đây (tùy trường hợp) return false; }, child: PageView( children: <Widget>[ _buildPage(Colors.red, 'Trang 1'), _buildPage(Colors.green, 'Trang 2'), _buildPage(Colors.blue, 'Trang 3'), ], ), ), ), ], ), ); } Widget _buildPage(Color color, String text) { return Container( color: color, child: Center( child: Text( text, style: const TextStyle(color: Colors.white, fontSize: 48), ), ), ); } } Trong ví dụ trên: Chúng ta dùng NotificationListener<ScrollNotification> để "nghe lén" mọi sự kiện cuộn xảy ra trong PageView của chúng ta. Khi có một ScrollNotification bắn ra, chúng ta kiểm tra xem notification.metrics có phải là PageMetrics hay không. Nếu đúng, chúng ta ép kiểu và lấy ra đối tượng PageMetrics đó. Từ pageMetrics, chúng ta truy cập thuộc tính page để biết số trang hiện tại (kể cả phần thập phân khi đang vuốt). Cuối cùng, dùng setState để cập nhật UI, hiển thị số trang lên màn hình. 3. Mẹo (Best Practices) từ “lão làng” Creyt Để dùng PageMetrics một cách hiệu quả và không bị “lag” app, anh Creyt có vài tips nhỏ cho các em đây: Đừng setState quá đà: ScrollNotification bắn ra liên tục khi em vuốt. Nếu mỗi lần nó bắn ra mà em lại setState thì app có thể bị giật. Hãy chỉ setState khi giá trị page thực sự thay đổi một cách đáng kể (ví dụ, khi nó vượt qua một ngưỡng nào đó, hoặc khi phần nguyên của page thay đổi). Trong ví dụ trên, anh đã thêm điều kiện if (_currentPage != pageMetrics.page) để tránh setState không cần thiết. Sử dụng Debounce hoặc Throttle: Đối với các hiệu ứng phức tạp hơn, nơi mà mỗi lần ScrollNotification bắn ra đều tốn tài nguyên, hãy cân nhắc dùng kỹ thuật debounce hoặc throttle. Tức là, thay vì xử lý ngay lập tức, em đợi một chút hoặc chỉ xử lý sau mỗi khoảng thời gian nhất định. Hiểu rõ PageController vs PageMetrics: PageController dùng để điều khiển PageView (chuyển trang, nhảy trang, lấy thông tin trang hiện tại). PageMetrics dùng để đọc thông tin chi tiết về trạng thái cuộn của PageView khi nó đang hoạt động, đặc biệt là khi người dùng đang thao tác. Thường thì em sẽ dùng PageMetrics qua NotificationListener để phản ứng với hành động của người dùng, còn PageController để điều khiển hoặc lấy thông tin tại một thời điểm cụ thể. Trả về false cho onNotification: Trong hầu hết các trường hợp, em nên trả về false từ onNotification để các NotificationListener khác (nếu có) hoặc các widget cha vẫn có thể nhận được notification. Trả về true sẽ "nuốt" notification và ngăn nó lan truyền. 4. Ứng dụng thực tế: PageMetrics “bật mode” siêu sao PageMetrics không chỉ là lý thuyết suông, nó là nền tảng cho rất nhiều tính năng "xịn xò" mà em thấy hàng ngày: Page Indicators (chấm tròn chỉ trang): Đây là ứng dụng kinh điển nhất. Khi em vuốt qua các trang onboarding, các chấm tròn bên dưới sẽ sáng lên hoặc di chuyển mượt mà theo độ lệch của trang. Chính PageMetrics.page (với phần thập phân) giúp các chấm tròn này chuyển động "ăn khớp" với ngón tay của người dùng. Parallax Scrolling Effects: Khi em vuốt một trang, các lớp nội dung khác nhau di chuyển với tốc độ khác nhau, tạo cảm giác chiều sâu. PageMetrics cung cấp thông tin độ lệch chính xác để tính toán tốc độ di chuyển của từng lớp. Onboarding Screens động: Nội dung text, hình ảnh có thể thay đổi độ mờ (opacity), vị trí, hoặc kích thước một cách mượt mà khi người dùng vuốt giữa các trang. Gallery/Carousel ảnh thông minh: Khi đến trang cuối, có thể tự động tải thêm ảnh mới hoặc gợi ý hành động tiếp theo. Các app như Instagram Stories, Facebook Stories, các ứng dụng đọc báo có carousel ảnh, hay các màn hình giới thiệu sản phẩm của Shopee/Lazada đều ít nhiều dùng đến cơ chế tương tự PageMetrics để tạo ra trải nghiệm mượt mà đó. 5. Thử nghiệm và hướng dẫn nên dùng cho case nào Anh Creyt đã từng "vật lộn" với PageMetrics (hay các khái niệm tương tự trong các framework khác) rất nhiều lần để tạo ra những hiệu ứng UI độc đáo. Khi nào nên dùng PageMetrics qua NotificationListener? Khi em muốn phản ứng với hành động vuốt của người dùng theo thời gian thực: Ví dụ, em muốn một thanh tiến trình (progress bar) di chuyển liên tục khi người dùng vuốt giữa các trang, không chỉ khi trang đã dừng hẳn. Khi em cần thông tin độ lệch chính xác (double page value): Để tạo các hiệu ứng chuyển động mượt mà, liên tục mà PageController.page chỉ cung cấp khi trang đã dừng lại hoặc đang chuyển động một cách rõ ràng. Khi em muốn tạo hiệu ứng dựa trên sự "hiện diện" của trang: Ví dụ, một hình ảnh sẽ scale to dần khi nó bắt đầu xuất hiện trong viewport và scale nhỏ lại khi nó khuất dần. Khi nào nên dùng PageController? Khi em muốn điều khiển PageView: Nhảy đến một trang cụ thể (jumpToPage), chuyển động mượt mà đến một trang (animateToPage). Khi em chỉ cần biết số trang hiện tại đã được chọn (số nguyên) sau khi quá trình cuộn đã dừng lại: pageController.page sẽ cung cấp giá trị này. Khi em muốn lắng nghe sự kiện khi trang đã chuyển đổi hoàn toàn: Dùng addListener trên PageController và kiểm tra pageController.page. Kinh nghiệm của anh Creyt: Anh từng xây dựng một component carousel ảnh với hiệu ứng parallax và "zoom-in" nhẹ nhàng cho ảnh chính, trong khi ảnh phụ ở hai bên hơi mờ và nhỏ hơn. Toàn bộ hiệu ứng đó được tính toán dựa trên giá trị page (double) từ PageMetrics để điều chỉnh opacity, scale và transform của từng ảnh. Nó đòi hỏi một chút toán học về interpolation (nội suy) nhưng kết quả thì "đáng đồng tiền bát gạo" lắm, nhìn app "pro" hẳn ra. Tóm lại, PageMetrics là chìa khóa để mở ra thế giới của những UI động, mượt mà trong Flutter PageView. Nắm vững nó, các em sẽ có thêm một "siêu năng lực" để biến những ý tưởng UI phức tạp thành hiện thực! Cứ thử nghiệm đi, đừng ngại sai, đó là cách tốt nhất để học hỏi đấy các Gen Z của anh! 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é!

42 Đọc tiếp
OverlayState Flutter: Phép thuật 'trên trời' của Gen Z
20/03/2026

OverlayState Flutter: Phép thuật 'trên trời' của Gen Z

Chào các "dev-er" tương lai của vũ trụ số! Giảng viên Creyt đây, hôm nay chúng ta sẽ cùng khám phá một khái niệm nghe thì có vẻ cao siêu nhưng thực chất lại cực kỳ thú vị và quyền năng trong Flutter: OverlayState. Nghe cái tên đã thấy mùi "trên trời" rồi phải không? OverlayState là gì mà Gen Z phải biết? Các bạn cứ hình dung thế này: Ứng dụng Flutter của chúng ta như một sân khấu kịch hoành tráng. Mỗi Widget là một diễn viên, một đạo cụ trên sân khấu đó, tất cả đều tuân thủ kịch bản, vị trí của mình trong "cây widget" (widget tree). Nhưng đôi khi, đạo diễn (chính là bạn đó) muốn có một hiệu ứng đặc biệt, một ánh đèn spotlight rọi từ trên cao xuống, một dòng chữ chạy ngang màn hình, hay một bong bóng thoại bất ngờ xuất hiện trên tất cả các diễn viên và đạo cụ khác mà không làm xáo trộn bố cục sân khấu chính. Đó chính là lúc OverlayState ra tay! Nó như một lớp kính trong suốt phủ lên toàn bộ sân khấu của bạn. Bạn có thể "dán" bất kỳ widget nào lên tấm kính này, và chúng sẽ xuất hiện trên mọi thứ khác, bất kể chúng đang ở đâu trong cái cây widget rậm rạp kia. "À à, vậy là mình có thể làm mấy cái pop-up, tooltip xịn sò mà không sợ bị đè bởi các widget khác đúng không thầy?" - Chính xác! Nói một cách hàn lâm hơn, OverlayState là một State quản lý một stack các OverlayEntry. Mỗi OverlayEntry chính là "tấm vé VIP" cho widget của bạn được xuất hiện trên lớp kính trong suốt kia. Khi bạn "insert" một OverlayEntry, nó sẽ được thêm vào stack đó và hiển thị. Khi bạn "remove", nó biến mất. Dùng để làm gì? OverlayState là "vũ khí bí mật" cho những trường hợp bạn cần một widget: Nổi trên mọi thứ: Không bị giới hạn bởi parent widget hay clip của các widget khác. Xuất hiện ở vị trí tùy ý: Bạn có thể định vị nó theo màn hình, không theo vị trí tương đối của cha mẹ. Tương tác độc lập: Nó có thể nhận sự kiện chạm mà không ảnh hưởng đến các widget bên dưới. Code Ví Dụ Minh Hoạ: "Toast" thông báo siêu tốc Để các bạn dễ hình dung, chúng ta sẽ tạo một cái "toast" thông báo nhỏ nhắn, xinh xắn, bay ra giữa màn hình rồi tự động biến mất – y hệt như mấy cái notification trên Instagram hay TikTok vậy. 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: 'OverlayState Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { OverlayEntry? _overlayEntry; // Biến để giữ tham chiếu đến OverlayEntry void _showOverlay(BuildContext context) { // Bước 1: Đảm bảo không có overlay cũ nào đang hiển thị _overlayEntry?.remove(); // Bước 2: Tạo một OverlayEntry mới _overlayEntry = OverlayEntry( builder: (context) => Positioned( // Định vị widget của bạn trên màn hình top: 100.0, // Cách mép trên 100px left: MediaQuery.of(context).size.width * 0.1, // Cách mép trái 10% width: MediaQuery.of(context).size.width * 0.8, // Chiếm 80% chiều rộng child: Material( // Material giúp widget có elevation và design đẹp hơn color: Colors.transparent, // Nền trong suốt để chỉ hiển thị nội dung child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: Colors.black87, // Nền đen mờ borderRadius: BorderRadius.circular(8.0), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 10.0, offset: Offset(0, 4), ), ], ), child: const Text( 'Bạn vừa kích hoạt Overlay! Nó nằm trên mọi thứ đấy!', textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 16.0), ), ), ), ), ); // Bước 3: Thêm OverlayEntry vào OverlayState của ứng dụng // Overlay.of(context) sẽ tìm OverlayState gần nhất trong cây widget Overlay.of(context).insert(_overlayEntry!); // Bước 4: Tự động remove overlay sau 3 giây (tùy chỉnh thời gian) Future.delayed(const Duration(seconds: 3), () { _overlayEntry?.remove(); // Xóa overlay khỏi màn hình _overlayEntry = null; // Đặt lại về null để sẵn sàng cho lần hiển thị tiếp theo }); } @override void dispose() { // Đảm bảo overlay được remove khi widget cha bị dispose _overlayEntry?.remove(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flutter OverlayState Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Đây là nội dung chính của ứng dụng.', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), ElevatedButton( onPressed: () => _showOverlay(context), child: const Text('Hiện thông báo Overlay'), ), const SizedBox(height: 20), // Widget này sẽ bị Overlay che khi nó xuất hiện Container( height: 100, width: 200, color: Colors.green, child: const Center( child: Text('Widget này sẽ bị Overlay che', style: TextStyle(color: Colors.white)), ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { _showOverlay(context); }, child: const Icon(Icons.add), ), ); } } Giải thích Code: OverlayEntry? _overlayEntry;: Đây là biến để chúng ta giữ "tấm vé VIP" cho widget của mình. Quan trọng là phải giữ nó để sau này còn biết đường mà "thu vé" lại (remove). _showOverlay(BuildContext context): Hàm này là nơi "phép thuật" xảy ra. Nó nhận context để có thể tìm được OverlayState của ứng dụng. _overlayEntry?.remove();: Luôn kiểm tra và remove overlay cũ nếu có, tránh tình trạng "chồng chéo" các lớp kính lên nhau. OverlayEntry(...): Chúng ta tạo một OverlayEntry mới. Bên trong nó là một builder function, nơi bạn định nghĩa widget mà mình muốn hiển thị "trên trời". Positioned(...): Thường thì các widget trong OverlayEntry sẽ được bọc bởi Positioned để bạn có thể định vị chính xác chúng trên màn hình (dùng top, left, right, bottom, width, height). Material(...): Nên bọc widget của bạn trong Material để nó thừa hưởng các thuộc tính Material Design như elevation (tạo bóng đổ) hay splash effect nếu có tương tác. Overlay.of(context).insert(_overlayEntry!);: Đây là câu lệnh mấu chốt! Nó lấy OverlayState gần nhất trong cây widget (thường là của MaterialApp hoặc WidgetsApp) và "insert" tấm vé VIP của bạn vào. Thế là widget của bạn bay lên! Future.delayed(...): Để tạo hiệu ứng "toast" tự biến mất, chúng ta dùng Future.delayed để sau một khoảng thời gian nhất định, gọi _overlayEntry?.remove(); để "gỡ" widget xuống. dispose(): Đừng quên remove _overlayEntry trong dispose() của State để tránh rò rỉ bộ nhớ khi widget bị hủy. Đây là một best practice cực kỳ quan trọng! Mẹo hay từ Creyt (Best Practices) Context là chìa khóa: Để truy cập Overlay.of(context), context của bạn phải nằm bên dưới một Overlay trong cây widget. MaterialApp hay WidgetsApp tự động cung cấp Overlay cho bạn, nên thường dùng context từ Scaffold hoặc bất kỳ widget con nào của nó là được. Quản lý vòng đời (Lifecycle): Luôn nhớ remove() OverlayEntry khi không còn cần nữa. Nếu không, widget đó sẽ mãi mãi hiển thị (hoặc chiếm bộ nhớ) ngay cả khi bạn đã chuyển sang màn hình khác. Cứ nghĩ nó như việc bạn bật đèn thì phải biết tắt đèn vậy. Đặc biệt trong dispose()! Hiệu suất: OverlayState mạnh mẽ, nhưng không phải lúc nào cũng là giải pháp tối ưu. Đối với các UI đơn giản chỉ cần xếp chồng trong một khu vực cụ thể, hãy dùng Stack và Positioned thay vì Overlay. Overlay dành cho những thứ cần nổi toàn cục. Khả năng truy cập (Accessibility): Khi sử dụng overlay, hãy cân nhắc cách người dùng khuyết tật (ví dụ, dùng trình đọc màn hình) sẽ tương tác với nội dung của bạn. Đảm bảo trải nghiệm vẫn mượt mà và dễ hiểu. Animation: Để các overlay xuất hiện và biến mất mượt mà hơn, hãy kết hợp chúng với các widget animation như FadeTransition, SlideTransition hoặc AnimatedOpacity. Nó sẽ biến một cái "pop-up" thô cứng thành một "hiệu ứng" có hồn ngay! Ứng dụng thực tế các website/ứng dụng đã dùng OverlayState (hoặc các cơ chế tương tự trong các framework khác) được sử dụng rất nhiều: Tooltips (Gợi ý công cụ): Khi bạn hover chuột hoặc nhấn giữ một icon, một dòng chữ nhỏ hiện ra giải thích chức năng. Flutter có Tooltip widget, nhưng nếu bạn muốn custom "hết nấc" thì OverlayState là lựa chọn. Context Menus (Menu ngữ cảnh): Nhấn giữ một item trên màn hình, một menu nhỏ hiện ra ngay tại vị trí bạn nhấn. PopupMenuButton của Flutter đã dùng cơ chế tương tự. Custom Notifications/Snackbars: Các thông báo tùy chỉnh không theo chuẩn Material Design, bay từ trên xuống hoặc từ dưới lên, như ví dụ "toast" của chúng ta. Drag-and-Drop Feedback: Khi bạn kéo một item, một bản sao mờ của item đó bay theo con trỏ chuột để hiển thị bạn đang kéo gì và đi đâu. In-app Guides/Onboarding: Các mũi tên, pop-up hướng dẫn người dùng lần đầu sử dụng ứng dụng, chỉ ra các nút bấm quan trọng. Thử nghiệm đã từng và Hướng dẫn nên dùng cho case nào Với kinh nghiệm "chinh chiến" qua bao dự án, Creyt đã thấy OverlayState được dùng trong nhiều tình huống cực kỳ sáng tạo. Hồi xưa, có lần tôi cần xây dựng một hệ thống "baloon tip" (bong bóng gợi ý) cực kỳ phức tạp, có mũi tên chỉ vào đủ mọi hướng, animation bay ra bay vào đủ kiểu. Dùng OverlayState là giải pháp duy nhất để nó không bị các widget khác cắt xén hay đè lên. Nên dùng khi: Bạn cần một widget xuất hiện trên mọi thứ trong route hiện tại, không bị giới hạn bởi bất kỳ parent nào. Bạn muốn định vị widget đó theo tọa độ tuyệt đối của màn hình, không phải tương đối trong một container. Bạn đang xây dựng các thành phần UI rất đặc thù như tooltip custom, context menu custom, floating notification độc đáo, hoặc onboarding flow có các phần tử nổi. Không nên dùng khi: Bạn chỉ cần xếp chồng các widget trong một khu vực nhỏ của màn hình (hãy dùng Stack). Bạn muốn hiển thị một dialog đơn giản, showDialog() của Flutter đã làm rất tốt việc này (và nó cũng dùng Overlay bên trong, nhưng đã được trừu tượng hóa cho bạn rồi). Bạn đang cố gắng thay thế toàn bộ hệ thống navigation hoặc Scaffold bằng OverlayState. Đừng làm phức tạp hóa vấn đề! OverlayState là một công cụ mạnh mẽ, nhưng như mọi công cụ quyền năng khác, nó cần được sử dụng đúng lúc, đúng chỗ. Đừng biến nó thành "mớ bòng bong" chỉ vì bạn thấy nó "ngầu". Hãy dùng nó một cách thông minh, và bạn sẽ thấy ứng dụng của mình "bay" cao hơn đấy! Chúc các bạn code vui vẻ và tạo ra những hiệu ứng "trên trời" thậ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é!

41 Đọc tiếp
OverlayEntry: Khi UI của bạn cần 'nhảy dù' khỏi cây widget!
20/03/2026

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

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. 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: _overlayEntry và _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). _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. _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. onLongPress và onTapCancel: 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. 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 insert và remove, 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é!

43 Đọc tiếp
Overlay trong Flutter: Khi Widget Muốn 'Bay Lượn' Ngoài Quy Đạo!
20/03/2026

Overlay trong Flutter: Khi Widget Muốn 'Bay Lượn' Ngoài Quy Đạo!

Overlay trong Flutter: Khi Widget Muốn 'Bay Lượn' Ngoài Quy Đạo! Chào các bạn GenZ, anh Creyt đây! Hôm nay chúng ta sẽ cùng "mổ xẻ" một khái niệm mà nhiều bạn hay nhầm lẫn hoặc chưa khai thác hết tiềm năng của nó trong Flutter: Overlay. Nghe tên thì có vẻ "deep" nhưng thực ra nó cực kỳ gần gũi và hữu ích, đặc biệt khi các em muốn tạo ra những trải nghiệm UI "đỉnh của chóp" mà không bị ràng buộc bởi bố cục thông thường. 1. Overlay là gì và để làm gì? – "Kỹ thuật tàng hình" cho Widget! Các em cứ hình dung thế này: Khi các em đang xem một bộ phim bom tấn, bỗng dưng có một cái thông báo "Tin nóng!" hiện ra ngay giữa màn hình, hoặc một cái nút "Like" bay lơ lửng theo ngón tay các em khi chạm vào. Những thứ "bay lượn" độc lập, không nằm trong luồng giao diện chính đó chính là ứng dụng của Overlay. Trong Flutter, Overlay là một cơ chế cho phép chúng ta "chèn" các widget (gọi là OverlayEntry) lên trên các widget khác, thậm chí là lên trên toàn bộ ứng dụng, mà không bị ảnh hưởng bởi cấu trúc cây widget thông thường. Nó giống như việc các em có một tấm kính trong suốt, rồi vẽ vời đủ thứ lên đó, và tấm kính đó được đặt ngay trước mắt người xem, che phủ toàn bộ khung cảnh phía sau. Để làm gì ư? Đơn giản là để tạo ra những hiệu ứng UI mà Stack, Positioned hay Align không thể làm được một cách dễ dàng. Khi các em cần một widget "nổi" lên trên tất cả, ví dụ: Một cái loading spinner toàn màn hình. Một tooltip "xịn xò" hiện ra khi người dùng giữ tay vào một icon. Một menu ngữ cảnh (context menu) xuất hiện ngay tại vị trí con trỏ chuột. Hay thậm chí là một "hint" hướng dẫn người dùng lần đầu sử dụng app. 2. Code Ví Dụ Minh Hoạ: "Bật mí" cách dùng Overlay Để hiểu rõ hơn, chúng ta hãy cùng xem cách "triệu hồi" một OverlayEntry đơn giản nhé. Anh sẽ làm một ví dụ kinh điển: tạo một tooltip đơn giản khi nhấn nút. Đầu tiên, chúng ta cần một OverlayEntry để chứa widget mà mình muốn "bay lượn". Sau đó, chúng ta sẽ insert nó vào OverlayState và remove nó khi không cần nữa. 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: 'Flutter Overlay Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { OverlayEntry? _overlayEntry; // Biến để giữ OverlayEntry void _showOverlay(BuildContext context) { // Nếu có overlay cũ đang hiển thị, thì xóa nó đi trước _overlayEntry?.remove(); // Lấy RenderBox của widget mà chúng ta muốn overlay xuất hiện gần đó final RenderBox renderBox = context.findRenderObject() as RenderBox; final Size size = renderBox.size; final Offset offset = renderBox.localToGlobal(Offset.zero); _overlayEntry = OverlayEntry( builder: (context) => Positioned( left: offset.dx + size.width / 2 - 50, // Căn giữa tooltip theo chiều ngang top: offset.dy - 40, // Đặt tooltip phía trên nút một chút child: Material( elevation: 4.0, borderRadius: BorderRadius.circular(8.0), child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8.0), ), child: const Text( 'Đây là Tooltip!', style: TextStyle(color: Colors.white, fontSize: 14.0), ), ), ), ), ); // Chèn OverlayEntry vào OverlayState Overlay.of(context).insert(_overlayEntry!); // Tự động ẩn overlay sau 2 giây Future.delayed(const Duration(seconds: 2), () { _overlayEntry?.remove(); _overlayEntry = null; // Đặt lại null sau khi xóa }); } @override void dispose() { _overlayEntry?.remove(); // Đảm bảo overlay được xóa khi widget bị dispose super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Overlay Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Nhấn nút bên dưới để xem Overlay:', ), const SizedBox(height: 20), ElevatedButton( onPressed: () => _showOverlay(context), // Truyền context của nút child: const Text('Hiện Tooltip'), ), ], ), ), ); } } Giải thích nhanh: Chúng ta dùng OverlayEntry? _overlayEntry; để giữ tham chiếu đến OverlayEntry của mình. Hàm _showOverlay sẽ tạo OverlayEntry với widget Positioned bên trong để định vị tooltip. Overlay.of(context).insert(_overlayEntry!); là phép thuật chèn widget vào lớp Overlay. _overlayEntry?.remove(); là cách chúng ta "thu hồi" widget về. Quan trọng là phải gọi nó khi không cần nữa để tránh rò rỉ bộ nhớ. Anh dùng RenderBox để lấy vị trí và kích thước của nút, từ đó tính toán vị trí cho tooltip một cách chính xác. 3. Mẹo (Best Practices) từ Creyt để "cầm cưa" Overlay hiệu quả! Quản lý lifecycle là chìa khóa: Luôn nhớ _overlayEntry?.remove() khi widget OverlayEntry không còn cần thiết nữa (ví dụ: khi màn hình bị đóng, hoặc sau một thời gian nhất định). Nếu không, nó sẽ "bay lơ lửng" mãi mãi trong bộ nhớ và gây ra lỗi hiển thị hoặc rò rỉ bộ nhớ. Coi chừng đấy, nó như "hồn ma" của widget vậy! Context đúng chỗ, đúng lúc: Khi gọi Overlay.of(context), hãy đảm bảo context bạn truyền vào là của một widget nằm trong cây widget có Overlay (thường là MaterialApp hoặc WidgetsApp đã cung cấp sẵn Overlay). Trong ví dụ trên, anh dùng context của ElevatedButton để định vị tooltip, nhưng Overlay.of(context) vẫn sẽ tìm đến OverlayState gần nhất trong cây. Không lạm dụng: Overlay mạnh mẽ nhưng không phải là giải pháp cho mọi vấn đề. Nếu chỉ cần xếp chồng các widget trong một khu vực nhất định, hãy ưu tiên dùng Stack, Positioned. Overlay nên dùng cho những tình huống thực sự cần "thoát ly" khỏi bố cục cha mẹ. Kết hợp với Animation: Để các OverlayEntry xuất hiện và biến mất mượt mà hơn, hãy kết hợp chúng với các widget animation như FadeTransition, SlideTransition, hoặc AnimatedOpacity. Nó sẽ biến trải nghiệm người dùng từ "ổn" thành "wow"! Accessibility (Khả năng tiếp cận): Đảm bảo rằng các overlay của bạn không làm gián đoạn trải nghiệm của người dùng có nhu cầu đặc biệt (ví dụ: người dùng trình đọc màn hình). Cân nhắc cách họ sẽ tương tác và đóng các overlay này. 4. Ví dụ thực tế: "Ứng dụng của Overlay ở khắp mọi nơi!" Các em có thể không nhận ra, nhưng Overlay đã và đang được sử dụng rất nhiều trong các ứng dụng mà các em dùng hàng ngày: Facebook/Instagram: Khi các em thấy một thông báo "Đã thích bài viết" nhỏ gọn hiện lên và tự động biến mất, hoặc khi một loading spinner toàn màn hình xuất hiện khi chuyển trang. Google Maps/Apple Maps: Các tooltip thông tin địa điểm xuất hiện khi các em chạm vào một điểm trên bản đồ. Các ứng dụng chỉnh sửa ảnh/video: Các menu ngữ cảnh nhỏ gọn hiện ra khi các em giữ tay vào một đối tượng, cho phép chỉnh sửa nhanh. Ứng dụng học tập/Onboarding: Các "tour" hướng dẫn người dùng mới, với các mũi tên và chú thích "bay" trên các nút chức năng. 5. Thử nghiệm của Creyt và lời khuyên chân thành Anh nhớ hồi mới "vọc" Flutter, anh từng "đau đầu" với việc làm sao để một cái loading spinner nó "phủ" lên toàn bộ màn hình, dù màn hình đó có scrollable hay không, có nhiều lớp widget phức tạp đến mấy. Dùng Stack thì nó cứ bị giới hạn trong phạm vi của Stack đó, không "thoát" ra được. Rồi anh tìm đến Overlay, và "à ố" ra rằng nó chính là "chân ái" cho những trường hợp cần một widget "đè" lên mọi thứ khác mà không bị ràng buộc bởi cây widget cha mẹ. Nó cho phép anh tạo ra một "lớp" riêng biệt, độc lập hoàn toàn với nội dung bên dưới. Nên dùng Overlay cho case nào ư? Khi bạn cần một widget "bay lơ lửng" trên mọi thứ: Ví dụ như loading indicator toàn màn hình, custom toast message, floating context menu. Khi Stack hay showDialog/showModalBottomSheet không đủ linh hoạt: showDialog và showModalBottomSheet cũng tạo ra các overlay, nhưng chúng có những hành vi mặc định (ví dụ: modal barrier, hiệu ứng đóng mở) mà đôi khi bạn muốn tùy chỉnh hoàn toàn. OverlayEntry cho bạn quyền kiểm soát tối đa. Khi bạn cần điều khiển sự xuất hiện/biến mất của widget một cách độc lập: Không phụ thuộc vào việc rebuild của các widget cha. Nhớ nhé, Overlay là một công cụ mạnh mẽ, nhưng như mọi công cụ mạnh mẽ khác, hãy dùng nó một cách có trách nhiệm và hiểu rõ cơ chế của nó. Chúc các em "code" ra những UI "chất lừ" với Overlay! Hẹn gặp lại trong bài học tiếp theo! 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é!

45 Đọc tiếp
OpacityTransition Flutter: Tàng Hình & Hiện Hình UI mượt mà cùng Creyt
20/03/2026

OpacityTransition Flutter: Tàng Hình & Hiện Hình UI mượt mà cùng Creyt

Chào các em, lại là anh Creyt đây! Hôm nay, chúng ta sẽ "mổ xẻ" một khái niệm mà nghe thì có vẻ cao siêu, nhưng thực ra lại là "gia vị" cực kỳ quan trọng để món UI của mấy đứa thêm phần "ngon nghẻ": đó chính là OpacityTransition trong Flutter. Nghe cái tên đã thấy hơi "nghệ" rồi đúng không? 1. OpacityTransition là gì và để làm gì? Thôi, không vòng vo tam quốc nữa. OpacityTransition – đúng như cái tên của nó – là một widget trong Flutter giúp chúng ta điều khiển độ trong suốt (opacity) của một widget con theo thời gian một cách mượt mà. Các em cứ hình dung thế này: mấy đứa có nhớ mấy cảnh trong phim siêu anh hùng không? Khi Iron Man bay đi hay Spider-Man ẩn mình, họ không "bốp" cái biến mất luôn đúng không? Mà là từ từ, mờ dần rồi biến mất, hoặc từ từ hiện ra. OpacityTransition chính là "bộ đồ tàng hình" đó của UI của chúng ta. Nó giúp một widget con từ trạng thái hiện rõ 100% (opacity 1.0) chuyển dần sang tàng hình hoàn toàn (opacity 0.0), hoặc ngược lại. Để làm gì á? Đơn giản thôi: để UI của mấy đứa không còn "cục mịch", "giật cục" nữa. Thay vì một cái popup "nhảy xổ" vào mặt người dùng, nó sẽ từ từ "lướt" vào, tạo cảm giác tinh tế, chuyên nghiệp và "uy tín" hơn hẳn. Nó là một trong những công cụ cơ bản để tạo ra các hiệu ứng chuyển động (animation) mượt mà, giúp tăng trải nghiệm người dùng lên một tầm cao mới. Nói tóm lại, nó giúp app của bạn trông "xịn xịn", "mượt mượt" và "có gu" hơn rất nhiều. 2. Code Ví Dụ Minh Hoạ: "Cái Nút Tàng Hình" Để minh họa, anh Creyt sẽ làm một ví dụ đơn giản: một cái nút bấm, khi nhấn vào thì nó sẽ từ từ "tàng hình" rồi lại từ từ "hiện hình". Nghe có vẻ dễ, nhưng là cả một nghệ thuật đó nha! Đầu tiên, chúng ta cần một StatefulWidget vì animation là về thay đổi trạng thái theo thời gian mà. Kế đến, phải có AnimationController để điều khiển "nhịp điệu" của hiệu ứng, và một Animation<double> để giữ cái giá trị độ trong suốt thực tế. import 'package:flutter/material.dart'; class OpacityTransitionDemo extends StatefulWidget { const OpacityTransitionDemo({Key? key}) : super(key: key); @override State<OpacityTransitionDemo> createState() => _OpacityTransitionDemoState(); } class _OpacityTransitionDemoState extends State<OpacityTransitionDemo> with SingleTickerProviderStateMixin { // <--- Nhớ cái này nha, quan trọng lắm! late AnimationController _controller; late Animation<double> _opacityAnimation; bool _isVisible = true; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, // <--- Nhớ 'this' vì chúng ta dùng SingleTickerProviderStateMixin duration: const Duration(milliseconds: 800), // Thời gian chuyển đổi ); // Dùng CurvedAnimation để hiệu ứng mượt mà hơn, không bị "cứng" _opacityAnimation = CurvedAnimation( parent: _controller, curve: Curves.easeInCubic, // Thử các loại curve khác nhau xem sao nha! ); } @override void dispose() { _controller.dispose(); // <--- Cực kỳ quan trọng, tránh rò rỉ bộ nhớ super.dispose(); } void _toggleVisibility() { setState(() { _isVisible = !_isVisible; if (_isVisible) { _controller.forward(from: 0.0); // Chạy từ đầu để hiện ra } else { _controller.reverse(from: 1.0); // Chạy ngược lại để ẩn đi } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('OpacityTransition của anh Creyt'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Đây rồi, nhân vật chính của chúng ta! OpacityTransition( opacity: _opacityAnimation, // Truyền cái animation vào đây child: Container( width: 200, height: 200, color: Colors.deepPurple, child: const Center( child: Text( 'Anh Creyt đây!', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), const SizedBox(height: 30), ElevatedButton( onPressed: _toggleVisibility, child: Text(_isVisible ? 'Tàng Hình!' : 'Hiện Hình!'), ), ], ), ), ); } } Trong ví dụ trên: _controller: Là "nhạc trưởng" điều khiển tốc độ và hướng của animation. _opacityAnimation: Là "nốt nhạc" mà OpacityTransition sẽ lắng nghe để biết độ trong suốt cần là bao nhiêu. Anh dùng CurvedAnimation để làm hiệu ứng nó "mượt" hơn, chứ không phải "đi thẳng" như robot đâu nha. OpacityTransition: Widget này nhận một Animation<double> (chính là _opacityAnimation của chúng ta) để điều khiển opacity của child của nó. 3. Mẹo Vặt & Best Practices từ Creyt Là một lập trình viên "lão làng", anh Creyt có vài "chiêu" muốn truyền lại cho mấy đứa để dùng OpacityTransition cho nó "chuẩn bài": Đừng quên dispose()! Đây là lỗi kinh điển mà nhiều đứa mới học hay mắc phải. AnimationController tiêu tốn tài nguyên, nên khi widget không còn được sử dụng nữa, phải dispose() nó đi để tránh rò rỉ bộ nhớ. Cứ coi như dọn dẹp nhà cửa sau khi tiệc tùng vậy đó. vsync là bạn thân: Luôn nhớ mixin SingleTickerProviderStateMixin (hoặc TickerProviderStateMixin nếu có nhiều controller) và truyền this vào vsync của AnimationController. Nó giúp Flutter biết cách đồng bộ animation với tốc độ khung hình của màn hình, tránh tình trạng "giật lag". Chọn Curve phù hợp: Curves.linear là thẳng tắp, nhưng Curves.easeIn, Curves.easeOut, Curves.easeInOut, Curves.bounceIn... sẽ tạo ra các hiệu ứng "có hồn" hơn rất nhiều. Hãy thử nghiệm để tìm ra "chất riêng" cho app của mình. Thời gian là vàng: Đừng để duration quá nhanh (người dùng không kịp thấy gì) hoặc quá chậm (gây khó chịu). Một khoảng từ 300ms đến 800ms thường là "chuẩn đẹp" cho hầu hết các hiệu ứng fade. Kết hợp với các widget khác: OpacityTransition rất mạnh khi đứng một mình, nhưng còn mạnh hơn khi kết hợp với các widget animation khác. Ví dụ, nếu em muốn chuyển đổi giữa hai widget với hiệu ứng fade, hãy cân nhắc dùng AnimatedCrossFade – nó xử lý cho em cả hai chiều luôn, tiện lợi hơn nhiều. 4. Ví Dụ Thực Tế Nơi OpacityTransition "Toả Sáng" OpacityTransition không phải là thứ gì đó quá xa vời đâu, nó hiện diện khắp mọi nơi trong các ứng dụng mà mấy đứa dùng hàng ngày đó: Hiệu ứng Loading: Khi em tải dữ liệu từ server, một cái spinner loading hiện ra. Khi dữ liệu về xong, spinner đó sẽ mờ dần rồi biến mất. Đó chính là OpacityTransition đó. Thông báo (Toast/Snackbar): Các thông báo nhỏ hiện lên ở cuối màn hình, rồi sau vài giây lại mờ dần và biến mất. Popup/Dialog: Một số ứng dụng dùng hiệu ứng fade-in khi hiển thị một popup hoặc dialog, trông rất "xịn". Chuyển đổi trạng thái UI: Ví dụ, khi một nút bị disable, nó có thể mờ đi một chút để người dùng dễ nhận biết hơn. Hover effect trên Web (khi dùng Flutter Web): Khi di chuột qua một phần tử, nó có thể hơi mờ đi hoặc hiện rõ hơn. 5. Khi nào nên và không nên dùng OpacityTransition? Nên dùng khi: Bạn muốn một phần tử UI xuất hiện hoặc biến mất một cách tinh tế, nhẹ nhàng. Bạn muốn tạo hiệu ứng phản hồi (feedback) cho người dùng (ví dụ: một item trong danh sách được chọn, nó hơi mờ đi một chút). Bạn cần làm cho UI của mình trông "mượt mà" và "chuyên nghiệp" hơn mà không cần quá nhiều phức tạp. Các hiệu ứng loading, thông báo, chuyển đổi trạng thái đơn giản. Không nên dùng khi: Bạn cần hiệu ứng chuyển động phức tạp hơn như di chuyển (move), thay đổi kích thước (scale), xoay (rotate). Lúc đó, hãy tìm đến các "anh em" khác của nó như AnimatedPositioned, ScaleTransition, RotationTransition, hoặc Hero animation. Bạn cần chuyển đổi giữa hai widget hoàn toàn khác nhau mà cả hai đều có hiệu ứng fade. Như anh Creyt đã nói ở trên, AnimatedCrossFade sẽ là lựa chọn tối ưu hơn. Khi hiệu năng là tối quan trọng và bạn có quá nhiều widget cần OpacityTransition cùng lúc – đôi khi việc thay đổi opacity có thể ảnh hưởng nhẹ đến hiệu năng render nếu không được tối ưu. Tuy nhiên, với Flutter thì việc này thường không đáng lo ngại lắm, trừ khi bạn làm những thứ quá "điên rồ". Tóm lại, OpacityTransition là một công cụ đơn giản nhưng vô cùng hiệu quả để "thổi hồn" vào UI của bạn. Hãy thực hành và thử nghiệm thật nhiều để biến những khái niệm này thành "cơm bữa" của mình nhé các chiến thần code! Hẹn gặp lại trong bài học tiếp theo của anh Creyt! 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é!

48 Đọc tiếp
Flutter Offstage: Giữ Trạng Thái, Ẩn View, Chớp Nhoáng!
20/03/2026

Flutter Offstage: Giữ Trạng Thái, Ẩn View, Chớp Nhoáng!

Chào các bạn Gen Z mê code, anh Creyt đây! Hôm nay chúng ta sẽ cùng nhau "bóc phốt" một khái niệm mà nghe tên thì có vẻ "lén lút" nhưng lại cực kỳ quyền năng trong Flutter: OffstageState. Hay nói đúng hơn, là tác dụng của widget Offstage lên trạng thái của widget con. 1. OffstageState là gì mà "lén lút" dữ vậy? Các em cứ hình dung thế này, trong vũ trụ Flutter của chúng ta, mỗi widget là một "diễn viên" trên sân khấu ứng dụng. Bình thường, khi một diễn viên không cần xuất hiện, chúng ta hay "đuổi" họ vào cánh gà (tức là xóa khỏi cây widget, chẳng hạn dùng if hoặc Visibility với maintainState: false). Khi cần lại, họ phải "trang điểm, thay đồ" lại từ đầu, khá tốn công sức và thời gian. Offstage widget thì khác! Nó giống như một tấm màn nhung huyền bí. Khi em đặt một widget con vào trong Offstage và set thuộc tính offstage: true, thì cái widget con đó vẫn y nguyên ở trên sân khấu, vẫn giữ nguyên "trạng thái" của nó (OffstageState), nhưng bị tấm màn nhung che khuất hoàn toàn. Nó không chiếm không gian, không nhận sự kiện chạm, và không được vẽ ra màn hình. Nhưng nó vẫn "sống", vẫn "thở", vẫn "nhớ" tất cả những gì nó đang có. Nói cách khác, Offstage giúp ta giấu đi một widget mà không cần hủy bỏ nó. Trạng thái nội tại của nó (ví dụ: giá trị của một TextField, trạng thái của một nút bấm, dữ liệu của một StreamBuilder) vẫn được bảo toàn. Cứ như một idol K-Pop đang đứng sau cánh gà, sẵn sàng bước ra trình diễn ngay lập tức, không cần phải chuẩn bị lại từ đầu vậy. 2. Code Ví Dụ: "Idol" ẩn mình và khoe dáng Để các em dễ hình dung, chúng ta sẽ làm một ví dụ đơn giản với một CounterWidget có nút tăng giảm. Chúng ta sẽ dùng Offstage để ẩn/hiện nó và xem trạng thái của nó có được giữ nguyên không nhé. 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: 'Flutter Offstage Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const OffstageDemoScreen(), ); } } class CounterWidget extends StatefulWidget { const CounterWidget({super.key}); @override State<CounterWidget> createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } void _decrementCounter() { setState(() { _counter--; }); } @override Widget build(BuildContext context) { print('CounterWidget rebuilt. Current counter: $_counter'); // Để ý log này return Card( margin: const EdgeInsets.all(16.0), elevation: 4, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ const Text( 'Giá trị Counter:', style: TextStyle(fontSize: 18), ), Text( '$_counter', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _decrementCounter, child: const Icon(Icons.remove), ), const SizedBox(width: 20), ElevatedButton( onPressed: _incrementCounter, child: const Icon(Icons.add), ), ], ), const SizedBox(height: 10), const Text( '(Xem log để thấy khi nào widget được rebuild)', style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic), textAlign: TextAlign.center, ), ], ), ), ); } } class OffstageDemoScreen extends StatefulWidget { const OffstageDemoScreen({super.key}); @override State<OffstageDemoScreen> createState() => _OffstageDemoScreenState(); } class _OffstageDemoScreenState extends State<OffstageDemoScreen> { bool _isOffstage = true; // Ban đầu ẩn CounterWidget @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flutter Offstage Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: () { setState(() { _isOffstage = !_isOffstage; }); }, child: Text(_isOffstage ? 'Hiện Counter' : 'Ẩn Counter'), ), const SizedBox(height: 30), // Đây là nơi Offstage phát huy tác dụng Offstage( offstage: _isOffstage, child: const CounterWidget(), ), const SizedBox(height: 30), const Text( 'Widget bên dưới (luôn hiện)', style: TextStyle(fontSize: 16), ), // Widget này để chứng minh Offstage không ảnh hưởng layout của các widget khác Container( width: 100, height: 100, color: Colors.green, child: const Center( child: Text('Luôn Hiện', style: TextStyle(color: Colors.white)), ), ), ], ), ), ); } } Thử nghiệm: Chạy ứng dụng. Ban đầu, CounterWidget bị ẩn (_isOffstage là true). Nhấn nút "Hiện Counter". CounterWidget sẽ xuất hiện với giá trị 0. Tăng giảm counter vài lần (ví dụ lên 5). Nhấn nút "Ẩn Counter". CounterWidget biến mất. Quan sát log console: không có dòng CounterWidget rebuilt nào xuất hiện khi ẩn/hiện! Nhấn nút "Hiện Counter" lần nữa. CounterWidget xuất hiện trở lại với giá trị 5! Điều này chứng tỏ trạng thái của CounterWidget đã được giữ nguyên, không bị khởi tạo lại. 3. Mẹo (Best Practices) của Creyt để dùng "sân khấu ẩn" hiệu quả Dùng khi cần giữ trạng thái: Đây là lý do chính để dùng Offstage. Nếu em có một widget phức tạp, mất công khởi tạo, và em muốn ẩn/hiện nó mà không mất đi dữ liệu hay trạng thái hiện tại của nó, thì Offstage là lựa chọn số 1. Tối ưu tốc độ chuyển đổi: Việc ẩn/hiện bằng Offstage là cực nhanh vì widget không bị xóa và tạo lại. Nó chỉ đơn giản là ngừng vẽ. Cẩn trọng với hiệu năng: Mặc dù Offstage không vẽ widget con, nhưng nó vẫn giữ widget con trong cây widget (element tree và render tree). Điều này có nghĩa là nếu widget con của em cực kỳ nặng về mặt bộ nhớ hoặc có các luồng dữ liệu (stream, timer) chạy ngầm, thì việc dùng Offstage có thể không giúp tiết kiệm tài nguyên mà chỉ giấu đi thôi. Hãy cân nhắc. Kết hợp với Visibility: Visibility widget cũng có maintainState: true và maintainSize: true/false. Offstage tương đương với Visibility(visible: false, maintainState: true, maintainAnimation: true, maintainSize: false). Nếu em cần kiểm soát chi tiết hơn về việc duy trì kích thước (layout) hay animation, Visibility có thể linh hoạt hơn. Nhưng nếu chỉ đơn giản là "ẩn hoàn toàn nhưng giữ trạng thái", Offstage gọn gàng hơn. 4. Ứng dụng thực tế: Ai đã dùng "sân khấu ẩn" này? Em cứ nhìn vào các ứng dụng lớn, kiểu gì cũng có bóng dáng của Offstage hoặc các cơ chế tương tự: Các ứng dụng có tab phức tạp: Ví dụ như các ứng dụng ngân hàng, mạng xã hội (Facebook, Zalo) với nhiều tab chính. Khi em chuyển tab, các tab khác thường không bị hủy đi mà chỉ được ẩn đi để khi em quay lại, trạng thái của chúng (cuộn đến đâu, dữ liệu gì đang hiển thị) vẫn còn nguyên. Form nhập liệu nhiều bước/phần: Khi em điền một form dài, có thể có các phần tùy chọn. Thay vì xóa đi và xây lại toàn bộ phần đó, người ta dùng Offstage để ẩn nó đi, giữ nguyên dữ liệu đã nhập. Các công cụ chỉnh sửa ảnh/video: Các panel công cụ, bảng thuộc tính thường được ẩn/hiện linh hoạt. Nếu mỗi lần ẩn đi mà mất hết các thiết lập đang chọn thì phiền toái vô cùng. Game UI: Trong game, các menu, HUD (Head-Up Display) thường được load một lần và sau đó chỉ ẩn/hiện khi cần, để đảm bảo hiệu năng và phản hồi nhanh chóng. 5. Thử nghiệm và khi nào nên dùng Offstage? Với kinh nghiệm "chinh chiến" của anh Creyt, anh đã dùng Offstage rất nhiều trong các dự án cần sự mượt mà và giữ trạng thái. Khi nào nên dùng: Toggling nhanh và thường xuyên: Khi em có một phần UI cần ẩn/hiện liên tục (ví dụ: một nút bật/tắt filter, một bảng điều khiển nhỏ). Widget có trạng thái phức tạp hoặc đắt tiền để khởi tạo: Nếu widget của em mất nhiều thời gian để xây dựng hoặc có nhiều logic/dữ liệu cần duy trì (ví dụ: một ListView đã cuộn đến vị trí nhất định, một WebView đã load xong trang), Offstage là vị cứu tinh. Yêu cầu giữ nguyên vị trí trong cây layout (một cách ảo): Mặc dù Offstage không chiếm không gian, nó vẫn giữ widget con trong cây widget để khi hiện ra, nó có thể lấy lại vị trí và context của nó một cách dễ dàng. Khi nào nên tránh (hoặc cân nhắc giải pháp khác): Widget cực kỳ nặng về bộ nhớ: Nếu widget con của em tiêu thụ quá nhiều RAM ngay cả khi không hiển thị, việc dùng Offstage có thể gây lãng phí tài nguyên. Lúc đó, việc hủy bỏ hoàn toàn widget (dùng if hoặc Visibility với maintainState: false) có thể tốt hơn. Khi em thực sự muốn giải phóng tài nguyên: Nếu widget đó không cần thiết trong một thời gian dài và em muốn hệ thống dọn dẹp nó hoàn toàn, đừng dùng Offstage. Khi cần hiệu ứng chuyển động mượt mà: Offstage chỉ là ẩn/hiện "cộp" một cái. Nếu em muốn có các hiệu ứng mờ dần, trượt vào/ra, thì nên kết hợp với AnimatedOpacity, SlideTransition hoặc Visibility với maintainAnimation: true để đạt được hiệu quả mong muốn. Tóm lại, Offstage là một công cụ mạnh mẽ trong hộp đồ nghề của developer Flutter, giúp em quản lý trạng thái và tối ưu trải nghiệm người dùng một cách khéo léo. Hãy dùng nó một cách thông minh, và các em sẽ thấy ứng dụng của mình "mượt như bơ" ngay thôi! Chúc các em 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é!

43 Đọc tiếp
NotificationListenerState: 'Thính Giác' của Flutter
20/03/2026

NotificationListenerState: 'Thính Giác' của Flutter

Chào các chiến hữu Gen Z! Hôm nay, anh Creyt sẽ cùng các em 'mổ xẻ' một khái niệm nghe thì lạ mà quen, đó là NotificationListenerState trong Flutter. Nghe cái tên có vẻ 'hack não' đúng không? Đừng lo, anh sẽ biến nó thành món 'gỏi' dễ nuốt nhất! 1. NotificationListenerState là cái 'mô tê' gì và để làm gì? Để dễ hình dung, các em cứ tưởng tượng thế này: cuộc sống của chúng ta, hay nói đúng hơn là cái app Flutter của các em, là một 'bữa tiệc' sôi động. Các widget con trong app giống như những 'khách mời' đang vui chơi, đôi khi họ 'làm ồn' (cuộn màn hình, thay đổi kích thước, v.v.). Bây giờ, các em là 'chủ bữa tiệc' (widget cha), muốn biết khi nào có 'tiếng động lạ' để có thể phản ứng lại (ví dụ: tắt nhạc, bật đèn). Thay vì phải gắn một cái 'mic' vào từng người khách (widget con) để hỏi 'Bạn đang làm gì đấy?', Flutter cung cấp cho chúng ta một 'tai nghe siêu nhạy' gọi là NotificationListener. Cái NotificationListener này được đặt ở một vị trí chiến lược trong cây widget, nó sẽ 'chộp' lấy những 'tiếng động' (notifications) mà các widget con phát ra và 'truyền' lên trên. Thực ra, NotificationListenerState KHÔNG PHẢI là một class cụ thể mà các em có thể new ra đâu nhé. Nó là cái trạng thái mà một StatefulWidget của chúng ta sẽ thay đổi khi nó nhận được một Notification thông qua thằng NotificationListener. Hiểu đơn giản, khi NotificationListener nghe thấy 'tiếng động', nó sẽ gọi một hàm callback, và trong hàm đó, chúng ta thường dùng setState để cập nhật lại UI hoặc dữ liệu, tức là thay đổi trạng thái của widget cha. Đó chính là ý nghĩa sâu xa của 'State' trong cái tên NotificationListenerState mà các em hay thắc mắc! Tóm lại: NotificationListener giúp widget cha 'nghe lén' các sự kiện từ widget con mà không cần truyền callback ngược dòng phức tạp. Và 'State' là cách widget cha phản ứng lại với những gì nó 'nghe' được. 2. Code Ví Dụ Minh Hoạ: 'Thính Giác' cho Cuộn Trang Ví dụ điển hình nhất mà anh Creyt hay dùng để minh họa chính là việc phát hiện sự kiện cuộn trang (scrolling) để làm một cái gì đó, chẳng hạn như ẩn/hiện một nút 'Back to Top'. import 'package:flutter/material.dart'; class NotificationListenerDemo extends StatefulWidget { const NotificationListenerDemo({super.key}); @override State<NotificationListenerDemo> createState() => _NotificationListenerDemoState(); } class _NotificationListenerDemoState extends State<NotificationListenerDemo> { bool _showFab = false; // Trạng thái của nút 'Back to Top' final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // Đảm bảo controller được gắn vào ListView trước khi sử dụng nếu cần } @override void dispose() { _scrollController.dispose(); super.dispose(); } // Hàm xử lý khi nhận được Notification bool _handleScrollNotification(ScrollNotification notification) { // Kiểm tra nếu là ScrollUpdateNotification, tức là đang cuộn if (notification is ScrollUpdateNotification) { // Nếu cuộn qua một ngưỡng nhất định (ví dụ 200 pixel), // thì hiện nút 'Back to Top', ngược lại thì ẩn đi. if (notification.metrics.pixels > 200 && !_showFab) { setState(() { _showFab = true; }); } else if (notification.metrics.pixels <= 200 && _showFab) { setState(() { _showFab = false; }); } } // Trả về false để Notification tiếp tục được truyền lên các Listener khác trong cây widget. // Trả về true nếu bạn muốn dừng sự kiện tại đây. return false; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('NotificationListener Demo'), backgroundColor: Colors.blueAccent, ), // NotificationListener sẽ 'nghe' các sự kiện ScrollNotification body: NotificationListener<ScrollNotification>( onNotification: _handleScrollNotification, child: ListView.builder( controller: _scrollController, // Gắn ScrollController vào ListView itemCount: 100, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), elevation: 4, child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Item số ${index + 1}', style: const TextStyle(fontSize: 18), ), ), ); }, ), ), floatingActionButton: _showFab ? FloatingActionButton( onPressed: () { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); }, child: const Icon(Icons.arrow_upward), backgroundColor: Colors.green, ) : null, // Ẩn nút nếu _showFab là false ); } } void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter NotificationListener Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const NotificationListenerDemo(), ); } } Giải thích code: Chúng ta có một StatefulWidget (NotificationListenerDemo) để quản lý trạng thái _showFab (hiện/ẩn nút). NotificationListener<ScrollNotification> được đặt bao ngoài ListView.builder. Nó sẽ lắng nghe chỉ các ScrollNotification từ ListView con. Hàm _handleScrollNotification là nơi chúng ta xử lý logic. Khi có ScrollUpdateNotification (nghĩa là người dùng đang cuộn), chúng ta kiểm tra notification.metrics.pixels để biết vị trí cuộn. Nếu cuộn qua 200 pixel, chúng ta setState để _showFab thành true (và ngược lại). floatingActionButton sẽ hiển thị dựa vào giá trị của _showFab. Quan trọng: return false; trong onNotification nghĩa là notification sẽ tiếp tục 'truyền' lên các NotificationListener khác nếu có. Nếu return true;, notification sẽ 'chết' tại đây và không bubble lên nữa. 3. Mẹo (Best Practices) để 'Nuốt Trọn' NotificationListener Chọn đúng 'tần số': Luôn chỉ định loại Notification cụ thể mà bạn muốn lắng nghe (NotificationListener<ScrollNotification>, NotificationListener<SizeChangedLayoutNotification>, v.v.). Đừng để nó 'nghe' linh tinh, tốn tài nguyên. Quyết định 'tiếp sóng' hay 'ngắt sóng': Hàm onNotification trả về true hay false. true có nghĩa là bạn đã xử lý xong và muốn notification dừng lại ở đây (giống như 'ngắt sóng'). false có nghĩa là bạn đã xử lý nhưng vẫn muốn notification tiếp tục 'bubble' lên các NotificationListener cấp cao hơn (giống như 'tiếp sóng'). Hãy suy nghĩ kỹ về luồng sự kiện của bạn. Cẩn thận với hiệu năng: onNotification có thể được gọi rất thường xuyên (ví dụ: khi cuộn). Tránh đặt các tác vụ nặng, tốn thời gian vào đây. Nếu không, app của bạn sẽ 'lag' như 'đồ cổ' vậy. ScrollController vs NotificationListener: Đối với các tác vụ đơn giản liên quan đến cuộn (như lấy vị trí cuộn hiện tại), ScrollController thường đơn giản và hiệu quả hơn. NotificationListener mạnh mẽ hơn khi bạn cần phản ứng với các loại Notification đa dạng hơn hoặc khi bạn cần 'chặn' sự kiện cuộn. 4. Ứng Dụng Thực Tế: 'Thính Giác' trong Thế Giới App Các em có biết những tính năng 'xịn sò' nào đang dùng cơ chế này không? Nhiều lắm đó: Facebook, Instagram (và hầu hết các feed): Tính năng 'kéo để làm mới' (Pull to Refresh) hoặc 'tải thêm khi cuộn đến cuối' (Infinite Scrolling). Đây chính là NotificationListener đang 'nghe' các sự kiện ScrollNotification để kích hoạt tải dữ liệu mới. YouTube, Netflix: Các thanh tiến độ (progress bar) ở cuối màn hình khi bạn cuộn qua danh sách video, hoặc tự động ẩn/hiện thanh điều khiển khi không tương tác. Tất cả đều là nhờ NotificationListener 'nghe' sự thay đổi trong layout hoặc cuộn. Các app thương mại điện tử: Khi bạn cuộn qua danh sách sản phẩm, các hiệu ứng parallax hoặc các nút lọc/sắp xếp tự động ẩn/hiện cũng thường dùng cơ chế này. Mọi app có UI động: Hiding/showing AppBar khi cuộn, các hiệu ứng animation dựa trên vị trí cuộn. 5. Thử Nghiệm và Nên Dùng Cho Case Nào? Anh Creyt đã từng 'đau đầu' với việc truyền dữ liệu ngược dòng trong cây widget, và NotificationListener chính là 'vị cứu tinh'. Anh đã thử nghiệm nó trong nhiều trường hợp: Infinite Scrolling: Đây là 'case' kinh điển nhất. Khi người dùng cuộn đến gần cuối danh sách, NotificationListener sẽ 'báo động' để app tải thêm dữ liệu. Cực kỳ hiệu quả và mượt mà. Hiệu ứng UI dựa trên cuộn: Anh đã dùng để tạo hiệu ứng AppBar co lại hoặc mở rộng, hoặc một FloatingActionButton xuất hiện/biến mất khi cuộn. Nó giúp UI sống động và tương tác hơn rất nhiều. Phát hiện thay đổi kích thước widget: Đôi khi, một widget con thay đổi kích thước và anh muốn widget cha biết để điều chỉnh layout. SizeChangedLayoutNotification là một 'người bạn' đắc lực trong trường hợp này. Custom Pull-to-Refresh: Mặc dù Flutter có RefreshIndicator, nhưng nếu bạn muốn một hiệu ứng 'kéo để làm mới' độc đáo hơn, bạn có thể tự xây dựng bằng cách lắng nghe các ScrollNotification liên quan đến overscroll. Lời khuyên từ Creyt: Hãy dùng NotificationListener khi bạn cần một cơ chế 'nghe lén' các sự kiện từ widget con mà không muốn làm 'nhiễu loạn' bằng cách truyền callbacks qua nhiều tầng widget. Nó giống như một hệ thống 'liên lạc nội bộ' hiệu quả, giúp các widget 'nói chuyện' với nhau một cách 'kín đáo' và có tổ chức. Nhưng nhớ, đừng lạm dụng nó, hãy dùng đúng chỗ, đúng lúc để app của các em luôn 'mượt mà' và 'chất lượng' 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é!

48 Đọc tiếp
NestedScrollViewState: Bậc thầy điều khiển cuộn cuộn trong Flutter!
20/03/2026

NestedScrollViewState: Bậc thầy điều khiển cuộn cuộn trong Flutter!

Yo, fam! Đã bao giờ bạn lướt TikTok, Instagram hay YouTube và thấy mấy cái app đó có cái header (thanh tiêu đề) nó kiểu "biến hình" cực mượt chưa? Lúc thì to đùng, lúc lại co lại tí hon khi bạn cuộn nội dung? Đó không phải là phép thuật đâu nhá, mà là công nghệ! Và trong Flutter, cái "phép thuật" đó phần lớn đến từ một combo siêu đỉnh: NestedScrollView và "linh hồn" của nó, NestedScrollViewState. 1. NestedScrollViewState: "Conductor" của Dàn nhạc Cuộn Cuộn Nói một cách Gen Z cho dễ hình dung: NestedScrollView giống như một cái "hộp cuộn" đa năng, cho phép bạn nhét nhiều cái "hộp cuộn con" khác vào trong, nhưng tất cả chúng lại biết cách làm việc nhóm với nhau. Ví dụ, bạn có một cái header (thanh tiêu đề) có thể co giãn, bên dưới là một danh sách sản phẩm dài dằng dặc cũng cuộn được. NestedScrollView sẽ đảm bảo khi bạn cuộn danh sách sản phẩm lên, cái header kia cũng tự động "nghe lời" mà co lại. Vậy còn NestedScrollViewState? Nó chính là "ông bầu" hay "conductor" tài ba của dàn nhạc cuộn cuộn này. NestedScrollViewState không phải là widget bạn dùng trực tiếp để xây dựng UI, mà nó là cái "trạng thái" nội bộ, cái "bộ não" điều khiển cách mà NestedScrollView hoạt động. Nó nắm giữ thông tin về trạng thái cuộn của cả phần header bên ngoài và phần nội dung bên trong, giúp chúng ta can thiệp, điều khiển hoặc lắng nghe các sự kiện cuộn một cách "chủ động" hơn. Tóm lại: NestedScrollView là cái khung cảnh sân khấu, còn NestedScrollViewState là người đạo diễn đứng sau cánh gà, điều phối mọi chuyển động cuộn một cách mượt mà và ăn khớp. 2. Code Ví Dụ Minh Họa: Màn "Biến Hình" của Header Để dễ hiểu, chúng ta sẽ làm một ví dụ kinh điển: một trang có SliverAppBar co giãn và một TabBarView với các tab chứa danh sách riêng biệt. NestedScrollViewState sẽ giúp chúng ta "nói chuyện" với các ScrollController của cả phần header và phần body. 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: 'NestedScrollView Demo by Creyt', theme: ThemeData( primarySwatch: Colors.blue, ), home: const NestedScrollViewPage(), ); } } class NestedScrollViewPage extends StatefulWidget { const NestedScrollViewPage({super.key}); @override State<NestedScrollViewPage> createState() => _NestedScrollViewPageState(); } class _NestedScrollViewPageState extends State<NestedScrollViewPage> with SingleTickerProviderStateMixin { late TabController _tabController; // Key để truy cập NestedScrollViewState final GlobalKey<NestedScrollViewState> _nestedScrollViewKey = GlobalKey(); @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); // Lắng nghe sự kiện cuộn của NestedScrollView // Đây là cách bạn có thể tương tác với NestedScrollViewState WidgetsBinding.instance.addPostFrameCallback((_) { _nestedScrollViewKey.currentState?.outerController.addListener(() { // Ví dụ: in ra vị trí cuộn của header // print('Outer Scroll Offset: ${_nestedScrollViewKey.currentState?.outerController.offset}'); }); }); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( key: _nestedScrollViewKey, // Gắn key vào NestedScrollView headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ SliverAppBar( title: const Text('Creyt\'s Nested Scroll'), floating: true, // Header sẽ nổi lên khi cuộn xuống một chút pinned: true, // Header sẽ luôn ghim lại ở top khi co lại hết cỡ snap: true, // Kết hợp với floating, giúp header hiện lên nhanh hơn expandedHeight: 200.0, // Chiều cao ban đầu của header flexibleSpace: FlexibleSpaceBar( centerTitle: true, title: innerBoxIsScrolled ? null // Khi cuộn vào, title mặc định của SliverAppBar sẽ hiện : const Text('Chào Mừng Gen Z!', style: TextStyle(color: Colors.white, fontSize: 20)), background: Image.network( 'https://picsum.photos/800/400?random=1', // Ảnh nền đẹp zai fit: BoxFit.cover, ), ), bottom: TabBar( controller: _tabController, tabs: const <Widget>[ Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3'), ], ), ), ]; }, body: TabBarView( controller: _tabController, children: <Widget>[ _buildTabContent('Nội dung Tab 1'), _buildTabContent('Nội dung Tab 2'), _buildTabContent('Nội dung Tab 3'), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Sử dụng NestedScrollViewState để cuộn lên đầu // outerController là ScrollController của phần header _nestedScrollViewKey.currentState?.outerController.animateTo( 0.0, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); }, child: const Icon(Icons.arrow_upward), ), ); } Widget _buildTabContent(String title) { return ListView.builder( // Quan trọng: ListView trong NestedScrollView không cần ScrollController riêng // NestedScrollView sẽ tự động quản lý nó. itemCount: 50, itemBuilder: (BuildContext context, int index) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( padding: const EdgeInsets.all(16.0), child: Text('$title - Item ${index + 1}', style: const TextStyle(fontSize: 16)), ), ); }, ); } } Trong ví dụ trên: Chúng ta dùng GlobalKey<NestedScrollViewState> để có thể "nắm thóp" được NestedScrollViewState từ bên ngoài. Đây là cách để bạn tương tác trực tiếp với nó. _nestedScrollViewKey.currentState?.outerController cho phép bạn truy cập ScrollController của phần header. Từ đó, bạn có thể addListener để theo dõi vị trí cuộn, hoặc animateTo để cuộn lên/xuống theo ý muốn (như nút FloatingActionButton đã làm). 3. Mẹo (Best Practices) từ "Lão Làng" Creyt Đừng lạm dụng! NestedScrollView mạnh thật, nhưng đừng dùng nó cho mọi thứ. Nếu bạn chỉ cần một danh sách cuộn đơn giản, ListView hoặc CustomScrollView (với các sliver đơn giản) là đủ rồi. Dùng đúng chỗ mới là pro. Hiểu "linh hồn" của nó: NestedScrollView có hai phần chính: headerSliverBuilder (cho các phần header co giãn) và body (cho nội dung cuộn bên trong). Hãy tưởng tượng headerSliverBuilder là cái "áo khoác" và body là "người" mặc áo. Khi "người" di chuyển, "áo khoác" cũng phải điều chỉnh theo. ScrollController là chìa khóa: Nếu bạn muốn làm những trò "ảo diệu" như tự động cuộn, lắng nghe sự kiện cuộn, hay đồng bộ cuộn giữa nhiều phần, hãy nắm lấy outerController và innerController thông qua NestedScrollViewState. Nhớ nhé, NestedScrollView sẽ tự động cung cấp ScrollController cho body của nó, bạn không cần tự tạo thêm nữa. GlobalKey là "cầu nối": Khi bạn cần truy cập NestedScrollViewState từ một widget cha hoặc từ một FloatingActionButton như ví dụ, GlobalKey là người bạn thân thiết nhất. Nó cho phép bạn "gọi tên" và "nói chuyện" với state của widget đó. Performance: Tránh xây dựng những Sliver quá phức tạp hoặc danh sách quá dài trong headerSliverBuilder hoặc body mà không dùng builder (như SliverList.builder, ListView.builder). Hiệu suất là vàng, đặc biệt trên mobile. 4. Ứng Dụng Thực Tế: "Thấy quen mà không biết tên" Bạn đã thấy NestedScrollView "tung hoành" ở khắp mọi nơi rồi đấy: Trang cá nhân Instagram/Facebook: Phần thông tin cá nhân (ảnh đại diện, số lượng follower) ở trên sẽ co lại khi bạn cuộn xuống xem feed bài viết. Ứng dụng YouTube: Khi bạn xem video, phần video player ở trên sẽ co nhỏ lại khi bạn cuộn xuống xem bình luận hoặc video gợi ý. Google Play Store/App Store: Trang chi tiết ứng dụng, phần ảnh bìa và thông tin cơ bản sẽ co lại khi bạn cuộn xuống đọc mô tả, đánh giá. Bất kỳ ứng dụng nào có SliverAppBar "ngầu lòi": Đấy, chính nó đấy! 5. Thử Nghiệm và Nên Dùng Cho Case Nào Anh Creyt đã từng "đau đầu" với việc làm sao cho mấy cái header nó "nhảy múa" đúng ý. Hồi xưa, chưa có NestedScrollView, phải tự "chế" bằng tay, tính toán offset các kiểu con đà điểu, cực lắm! Giờ có NestedScrollView rồi thì mọi thứ dễ thở hơn nhiều. Nên dùng NestedScrollView khi: Bạn muốn một SliverAppBar có thể co giãn (expand/collapse) và đồng bộ với việc cuộn của nội dung bên dưới. Bạn có một TabBar nằm trong SliverAppBar và muốn các nội dung của TabBarView cuộn "chung nhịp" với SliverAppBar. Bạn cần một hiệu ứng cuộn "phức tạp" hơn, nơi mà một phần giao diện (header) sẽ thay đổi kích thước hoặc vị trí dựa trên hành vi cuộn của một phần giao diện khác (body). Bạn muốn có quyền kiểm soát ScrollController của cả phần header (outerController) và phần body (innerController) để làm những logic tùy chỉnh. Không nên dùng NestedScrollView khi: Bạn chỉ có một danh sách đơn giản và không cần header co giãn. Bạn cần hai danh sách cuộn độc lập hoàn toàn với nhau (không có sự tương tác giữa chúng). Nhớ nhé, NestedScrollViewState không phải là thứ bạn "nhìn thấy" trực tiếp, mà nó là "bộ não" điều khiển cái trải nghiệm cuộn mượt mà mà bạn "cảm nhận" được. Nắm vững nó, bạn sẽ tạo ra những UI "đỉnh của chóp" trong Flutter! 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é!

42 Đọc tiếp
NavigatorState: Đạo Diễn Sân Khấu Màn Hình Flutter Của Bạn
19/03/2026

NavigatorState: Đạo Diễn Sân Khấu Màn Hình Flutter Của Bạn

Chào các đệ tử Gen Z mê code, hôm nay anh Creyt sẽ giải mã một khái niệm mà nghe tên có vẻ khô khan nhưng lại là 'nội công thâm hậu' giúp các em điều khiển ứng dụng Flutter mượt mà như lướt TikTok: NavigatorState. NavigatorState là gì mà 'hot' vậy Creyt? Tưởng tượng ứng dụng Flutter của các em là một rạp chiếu phim hoành tráng, mỗi màn hình (screen) là một bộ phim đang chiếu. Navigator chính là 'người quản lý rạp' tổng thể, lo việc sắp xếp các bộ phim. Còn NavigatorState chính là 'cái bảng điều khiển' của ông quản lý đó. Nó lưu trữ toàn bộ thông tin về các bộ phim đang được chiếu, thứ tự chiếu, bộ phim nào vừa kết thúc, bộ phim nào sắp bắt đầu. Nói cách khác, nó là trạng thái hiện tại của chồng các Route (màn hình) trong ứng dụng của bạn. Thường thì các em dùng Navigator.of(context).push(...) hay pop(...) đúng không? Ngon lành cành đào. Nhưng lỡ có lúc các em cần đẩy một màn hình mới lên, hay đóng màn hình hiện tại lại, mà lại đang ở 'hậu trường' (ví dụ: trong một service, một BLoC, hoặc một hàm không có BuildContext trực tiếp) thì sao? Lúc đó, NavigatorState được sinh ra để 'cứu bồ' đấy. Nó cho phép em điều khiển Navigator mà không cần phải 'mượn' BuildContext từ một Widget nào đó. Làm sao để 'gọi hồn' NavigatorState từ bất cứ đâu? Để 'gọi hồn' được cái bảng điều khiển này từ bất cứ đâu, chúng ta cần một 'đường dây nóng' đặc biệt: GlobalKey<NavigatorState>. Đây như là 'số điện thoại riêng' của ông quản lý rạp, giúp các em liên lạc trực tiếp mà không cần phải đi qua quầy vé (BuildContext) nữa. Khi các em gán một GlobalKey<NavigatorState> vào thuộc tính navigatorKey của MaterialApp (hoặc CupertinoApp), cái GlobalKey đó sẽ giữ một tham chiếu đến NavigatorState của ứng dụng. Từ đó, bất cứ đâu trong ứng dụng, chỉ cần truy cập globalKeyCuaBan.currentState, các em sẽ có trong tay NavigatorState và có thể 'múa' các hàm như push, pop, pushNamed, popUntil, v.v. Code Ví Dụ Minh Hoạ Rõ Ràng Để các em dễ hình dung, anh Creyt có một ví dụ 'nhẹ nhàng tình cảm' sau: import 'package:flutter/material.dart'; // Bước 1: Khai báo GlobalKey<NavigatorState> ở tầm toàn cục // hoặc ở một lớp quản lý state (ví dụ: trong Provider, Riverpod, BLoC...) final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'NavigatorState Demo', // Bước 2: Gán GlobalKey vào navigatorKey của MaterialApp // Đây là cách để NavigatorState của ứng dụng 'kết nối' với GlobalKey của chúng ta. navigatorKey: navigatorKey, theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), // Định nghĩa các routes để có thể điều hướng bằng tên routes: { '/detail': (context) => const DetailScreen(), }, ); } } class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); // Một hàm giả lập ở 'hậu trường' không có BuildContext. // Ví dụ: đây có thể là một hàm trong service xử lý push notification, // hoặc trong một BLoC/ChangeNotifier sau khi xử lý xong data. void _navigateToDetailFromBackground() { // Bước 3: Sử dụng GlobalKey để truy cập NavigatorState và điều hướng. // Luôn kiểm tra currentState có null không trước khi dùng nhé các em! // Bởi vì GlobalKey có thể chưa được gắn vào Navigator lúc này. navigatorKey.currentState?.pushNamed('/detail'); // Hoặc nếu muốn đẩy một MaterialPageRoute trực tiếp: // navigatorKey.currentState?.push(MaterialPageRoute(builder: (context) => const DetailScreen())); // Anh Creyt hay dùng snackbar để báo hiệu đã thực hiện hành động này từ 'hậu trường' // Tất nhiên, để show snackbar cũng cần context hoặc một GlobalKey cho ScaffoldMessengerState // Nhưng ở đây ta cứ tập trung vào NavigatorState trước đã. print('Đã điều hướng tới Trang Chi Tiết từ background function!'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Trang Chủ - HomeScreen'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Đây là màn hình chính.', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Cách thông thường dùng context: an toàn và phổ biến khi có context Navigator.of(context).pushNamed('/detail'); }, child: const Text('Đi tới Trang Chi Tiết (dùng context)'), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Gọi hàm giả lập ở 'hậu trường' để xem GlobalKey hoạt động _navigateToDetailFromBackground(); }, child: const Text('Đi tới Trang Chi Tiết (dùng GlobalKey)'), ), ], ), ), ); } } class DetailScreen extends StatelessWidget { const DetailScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Trang Chi Tiết - DetailScreen'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Bạn đã đến trang chi tiết!', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Dùng GlobalKey để quay lại từ màn hình này (chỉ demo) // Trong thực tế, dùng Navigator.of(context).pop() sẽ phổ biến và rõ ràng hơn ở đây // vì chúng ta đang có BuildContext sẵn. navigatorKey.currentState?.pop(); }, child: const Text('Quay lại (dùng GlobalKey)'), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Cách thông thường dùng context để pop: rõ ràng và dễ hiểu Navigator.of(context).pop(); }, child: const Text('Quay lại (dùng context)'), ), ], ), ), ); } } Giải thích code: final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();: Chúng ta khai báo một GlobalKey kiểu NavigatorState. Đây là 'chìa khóa vàng' để truy cập NavigatorState. navigatorKey: navigatorKey,: Trong MaterialApp (hoặc CupertinoApp), chúng ta gán GlobalKey này vào thuộc tính navigatorKey. Điều này báo cho Flutter biết rằng NavigatorState của ứng dụng sẽ được liên kết với GlobalKey này. navigatorKey.currentState?.pushNamed('/detail');: Bây giờ, từ bất kỳ đâu (ví dụ: trong hàm _navigateToDetailFromBackground không có BuildContext), chúng ta có thể truy cập navigatorKey.currentState để lấy NavigatorState và gọi các phương thức điều hướng như pushNamed, pop, v.v. Mẹo 'xương máu' (Best Practices) từ anh Creyt Dùng khi thực sự cần: GlobalKey<NavigatorState> là một công cụ mạnh, nhưng cũng như 'dao hai lưỡi'. Chỉ dùng nó khi các em cần điều hướng hoặc hiển thị UI từ một lớp logic không có BuildContext (ví dụ: service, BLoC, ViewModel, hàm xử lý push notification). Ưu tiên Navigator.of(context): Khi các em đang ở trong một Widget và có BuildContext sẵn, hãy ưu tiên dùng Navigator.of(context). Cách này rõ ràng, an toàn và dễ theo dõi hơn nhiều. GlobalKey nên là 'phương án B' khi BuildContext không khả dụng. Kiểm tra null: Luôn kiểm tra navigatorKey.currentState có null không trước khi sử dụng (navigatorKey.currentState?.push(...)). Điều này đảm bảo ứng dụng không bị crash nếu NavigatorState chưa được khởi tạo hoặc đã bị hủy. Đừng lạm dụng: Lạm dụng GlobalKey có thể làm code của các em khó kiểm soát và debug hơn, vì nó tạo ra một 'kết nối toàn cục' (global access) mà không bị giới hạn bởi cây widget. Ứng dụng thực tế: Khi nào thì NavigatorState 'tỏa sáng'? NavigatorState (thông qua GlobalKey) thường được dùng trong các tình huống sau: Xử lý Push Notification: Khi người dùng nhấn vào một thông báo đẩy (push notification) và ứng dụng cần điều hướng đến một màn hình cụ thể, dù ứng dụng đang ở background hay đã bị terminate. Các service xử lý notification thường không có BuildContext. Trong các kiến trúc quản lý trạng thái (BLoC, Provider, Riverpod): Khi các em muốn điều hướng sau một sự kiện (ví dụ: đăng nhập thành công, tải dữ liệu hoàn tất) được xử lý trong một BLoC hoặc ViewModel mà không muốn truyền BuildContext vào đó. Hiển thị Dialog/Snackbar từ Service: Cần hiển thị một SnackBar hoặc AlertDialog từ một lớp logic không phải widget (ví dụ: để báo lỗi API). Mặc dù có GlobalKey<ScaffoldMessengerState> cho việc này, nhưng NavigatorState cũng có thể được dùng để push dialog routes. Kiểm soát luồng ứng dụng tổng thể: Trong một số trường hợp đặc biệt, khi cần can thiệp vào toàn bộ stack navigation của ứng dụng từ một điểm truy cập duy nhất. Thử nghiệm và Nên dùng cho case nào theo Creyt Hồi xưa anh Creyt cũng 'loay hoay' mãi vụ này. Có lần làm con app gọi API xong cần show lỗi, mà cái hàm xử lý API nó nằm tít trong cái service không có BuildContext. Cứ tưởng 'tắc tị', ai dè GlobalKey<NavigatorState> nó 'giải cứu' một bàn thua trông thấy, giúp anh Creyt push một màn hình lỗi hoặc showDialog ngay lập tức. Nên dùng khi: Các em cần điều hướng hoặc hiển thị UI (dialog, snackbar) từ một lớp logic không có BuildContext (ví dụ: Repository, Service, ViewModel, BLoC). Đây là 'lý do sống còn' của GlobalKey<NavigatorState>. Khi các em muốn tạo một 'điểm truy cập toàn cục' để kiểm soát navigation cho những chức năng cốt lõi, ví dụ như xử lý deep link. Không nên lạm dụng khi: Các em đang ở trong một Widget và có BuildContext sẵn rồi. Dùng Navigator.of(context) sẽ rõ ràng và an toàn hơn nhiều, tránh được những 'cú lừa' khó debug khi GlobalKey chưa được gán hoặc bị mất liên kết. Nói chung, NavigatorState với GlobalKey là một 'công cụ quyền năng', nhưng mà 'quyền năng lớn thì trách nhiệm lớn'. Dùng đúng lúc, đúng chỗ thì nó là 'siêu anh hùng', dùng sai thì nó thành 'phản diện' làm code khó hiểu đấy nhé! Vậy là hôm nay chúng ta đã 'mổ xẻ' xong NavigatorState. Nhớ nhé, học là phải thực hành, về nhà 'mần' ngay một con app nhỏ để thử nghiệm đi. Có gì khó khăn cứ 'alô' anh Creyt! 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é!

44 Đọc tiếp
MouseTrackerAnnotation: "Radar" của Chuột, Biến App Flutter Thêm Mượt!
19/03/2026

MouseTrackerAnnotation: "Radar" của Chuột, Biến App Flutter Thêm Mượt!

Này các bạn genZ developer, hôm nay anh Creyt sẽ giải mã một khái niệm nghe hơi “academic” nhưng lại là “công thần” thầm lặng giúp app Flutter của chúng ta mượt mà, “thông minh” hơn rất nhiều khi tương tác với chuột: MouseTrackerAnnotation. 1. MouseTrackerAnnotation là gì mà nghe “ngầu” vậy anh Creyt? Nghe tên thì hơi dài dòng, nhưng hiểu nôm na, MouseTrackerAnnotation không phải là một widget mà các bạn “kéo thả” như Text hay Button. Nó giống như một “radar ngầm” hay một “bộ cảm biến siêu nhạy” trong hệ thống render của Flutter vậy. Nhiệm vụ của nó là "đánh dấu" một khu vực cụ thể trên giao diện người dùng (UI) và bảo cho Flutter biết: "Ê, cái vùng này quan trọng đấy, nếu có con chuột nào lượn lờ qua đây thì nhớ báo động cho tôi biết nhé!". Để làm gì? Đơn giản là để app của bạn có thể phản ứng lại với các hành động của chuột như: rê chuột vào (hover), rời chuột ra (exit), hay thậm chí là di chuyển chuột bên trong vùng đó. Nhờ có nó, chúng ta mới có thể tạo ra những hiệu ứng "ảo diệu" như: đổi màu nút khi rê chuột vào, hiện tooltip (bảng thông tin nhỏ) khi chỉ vào icon, hay đổi con trỏ chuột thành hình bàn tay "click" khi di chuyển qua một liên kết. Cứ hình dung thế này: Giao diện của bạn là một bữa tiệc buffet hoành tráng. Mỗi món ăn ngon (widget) đều có một cái MouseTrackerAnnotation gắn kèm. Khi con chuột của người dùng (khách dự tiệc) đi vào "vùng an toàn" của món nào, cái Annotation đó sẽ "tít tít" báo hiệu cho đầu bếp (app của bạn) biết: "Có khách đang quan tâm món này!" và đầu bếp sẽ ngay lập tức làm một hành động gì đó để "chiều lòng" vị khách đó, như đổi đĩa, thêm gia vị, hay mời chào thân thiện hơn. Đó chính là cách MouseTrackerAnnotation giúp trải nghiệm người dùng trở nên mượt mà và trực quan hơn rất nhiều! Trong thực tế, các bạn sẽ ít khi tương tác trực tiếp với MouseTrackerAnnotation. Thay vào đó, chúng ta sẽ dùng một widget “thân thiện” hơn rất nhiều là MouseRegion. MouseRegion chính là cái “vỏ bọc” hoàn hảo, sử dụng MouseTrackerAnnotation bên dưới để làm việc của nó. 2. Code Ví Dụ Minh Hoạ: MouseRegion – "Cánh tay nối dài" của MouseTrackerAnnotation Anh Creyt sẽ dùng MouseRegion để các bạn thấy rõ sức mạnh của cơ chế này nhé. Chúng ta sẽ tạo một cái Card, khi rê chuột vào thì nó đổi màu và con trỏ chuột cũng thay đổi theo. 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: 'MouseTrackerAnnotation Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MouseTrackerDemoPage(), ); } } class MouseTrackerDemoPage extends StatefulWidget { const MouseTrackerDemoPage({super.key}); @override State<MouseTrackerDemoPage> createState() => _MouseTrackerDemoPageState(); } class _MouseTrackerDemoPageState extends State<MouseTrackerDemoPage> { Color _cardColor = Colors.lightBlueAccent; // Màu mặc định MouseCursor _cardCursor = SystemMouseCursors.basic; // Con trỏ chuột mặc định @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Anh Creyt dạy MouseTracker!'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Di chuột vào ô vuông này xem có gì hay ho nhé!', style: TextStyle(fontSize: 18), ), const SizedBox(height: 30), // Đây chính là nơi MouseTrackerAnnotation "âm thầm" làm việc thông qua MouseRegion! MouseRegion( onEnter: (event) { setState(() { _cardColor = Colors.deepPurpleAccent; // Đổi màu khi chuột vào _cardCursor = SystemMouseCursors.click; // Đổi con trỏ thành icon click }); print('Chuột đã vào vùng VIP!'); }, onExit: (event) { setState(() { _cardColor = Colors.lightBlueAccent; // Trở lại màu cũ khi chuột rời đi _cardCursor = SystemMouseCursors.basic; // Trở lại con trỏ cơ bản }); print('Chuột đã rời vùng VIP!'); }, onHover: (event) { // Bạn có thể làm gì đó khi chuột di chuyển trong vùng. // Ví dụ: hiển thị tọa độ chuột, nhưng cẩn thận đừng spam setState quá nhiều! // print('Chuột đang di chuyển tại: ${event.localPosition}'); }, cursor: _cardCursor, // Gán con trỏ chuột tùy chỉnh child: Container( width: 200, height: 200, decoration: BoxDecoration( color: _cardColor, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: _cardColor.withOpacity(0.5), blurRadius: 10, offset: const Offset(0, 5), ), ], ), alignment: Alignment.center, child: const Text( 'Hover Me!', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 20), const Text( 'Thấy chưa? Chỉ cần "MouseRegion" là đủ để bắt sóng chuột rồi!', style: TextStyle(fontSize: 16, fontStyle: FontStyle.italic), ), ], ), ), ); } } Giải thích nhanh: MouseRegion là widget bao bọc cái Container của chúng ta. onEnter: Hàm này được gọi khi con trỏ chuột vừa mới đi vào phạm vi của MouseRegion. onExit: Hàm này được gọi khi con trỏ chuột vừa mới rời khỏi phạm vi của MouseRegion. onHover: Hàm này được gọi liên tục mỗi khi con trỏ chuột di chuyển bên trong phạm vi của MouseRegion. Cẩn thận khi dùng setState ở đây vì nó có thể gây hiệu năng không tốt nếu bạn không tối ưu. cursor: Thuộc tính này cho phép bạn thay đổi hình dạng con trỏ chuột khi nó nằm trong MouseRegion. Flutter cung cấp sẵn nhiều SystemMouseCursors tiện lợi. 3. Mẹo (Best Practices) để ghi nhớ và dùng thực tế Anh Creyt có vài "chiêu" nhỏ để các bạn nhớ và dùng MouseTrackerAnnotation (thông qua MouseRegion) cho hiệu quả: "Đừng quá tham lam": Chỉ dùng MouseRegion khi thực sự cần hiệu ứng tương tác chuột. Việc có quá nhiều MouseRegion có thể làm tăng chi phí render, đặc biệt trên các UI phức tạp. "Đừng quên người anh em": MouseRegion thường đi đôi với các widget khác như Tooltip để tạo trải nghiệm hoàn chỉnh. Ví dụ, khi rê chuột vào một icon, MouseRegion đổi con trỏ, còn Tooltip hiện mô tả chức năng. "Nghĩ xa hơn bàn phím": Luôn nhớ rằng không phải ai cũng dùng chuột (ví dụ: người dùng bàn phím, người dùng thiết bị cảm ứng). Đảm bảo giao diện của bạn vẫn dễ sử dụng và truy cập (accessible) ngay cả khi không có chuột. "Kiểm tra đa nền tảng": Hành vi của chuột có thể hơi khác nhau giữa các nền tảng (web, desktop). Luôn test kỹ trên môi trường bạn đang nhắm tới. 4. Ứng dụng thực tế: Ai đã dùng "radar" này? "Radar" MouseTrackerAnnotation (thông qua MouseRegion) được ứng dụng rộng rãi trong các nền tảng hỗ trợ chuột: Các trang web và ứng dụng desktop: Đây là nơi nó tỏa sáng nhất. Hầu hết các nút bấm, liên kết, hoặc thẻ thông tin trên website đều đổi màu, đổi hình dạng hoặc hiển thị thêm chi tiết khi bạn rê chuột vào. Giao diện người dùng game (Game UI): Các nút điều khiển, thanh máu, vật phẩm trong game thường được highlight hoặc có hiệu ứng khi người chơi rê chuột qua, tạo cảm giác tương tác sống động hơn. Bảng điều khiển (Dashboards) và ứng dụng phân tích dữ liệu: Khi rê chuột vào các điểm dữ liệu trên biểu đồ, các ứng dụng này thường hiển thị một tooltip với thông tin chi tiết, giúp người dùng dễ dàng khám phá dữ liệu hơn. Editor ảnh/video hoặc CAD software: Các vùng chọn, công cụ vẽ, hoặc thanh công cụ thường có phản hồi trực quan khi chuột di chuyển qua, giúp người dùng định vị và thao tác chính xác hơn. 5. Thử nghiệm và hướng dẫn nên dùng cho case nào Anh Creyt đã từng "đau đầu" với việc tạo ra các hiệu ứng tương tác chuột mượt mà cho các ứng dụng web và desktop bằng Flutter. Và MouseRegion chính là "cứu tinh" đấy! Nên dùng khi nào? Tạo hiệu ứng hover cho các nút bấm, card, hoặc bất kỳ widget nào: Khi bạn muốn một widget "sống động" hơn khi người dùng tương tác bằng chuột, như đổi màu nền, tăng độ nổi bật (elevation), hoặc hiện icon ẩn. Thay đổi con trỏ chuột: Để chỉ ra rằng một vùng nào đó có thể click được (SystemMouseCursors.click), có thể kéo (SystemMouseCursors.grab), hoặc đang trong trạng thái chờ (SystemMouseCursors.wait). Hiển thị tooltip hoặc overlay thông tin: Khi bạn muốn cung cấp thêm thông tin chi tiết mà không làm chật chội giao diện, chỉ hiện ra khi người dùng rê chuột vào. Phát hiện vị trí chuột trong một vùng: Dùng onHover để lấy event.localPosition hoặc event.globalPosition để thực hiện các thao tác vẽ, kéo thả tùy chỉnh trong một khu vực cụ thể. Thử nghiệm với onHover (lưu ý quan trọng!): onHover là một "ông hoàng" của sự kiện, nó sẽ bắn liên tục mỗi khi con chuột nhích một pixel trong vùng của bạn. Nếu bạn đặt setState trong onHover mà không có logic kiểm soát, UI của bạn có thể bị re-render liên tục và gây giật lag. Hãy cẩn thận! Chỉ dùng setState trong onHover khi bạn thực sự cần cập nhật UI dựa trên vị trí chuột (ví dụ: vẽ một đường line theo chuột) và cố gắng tối ưu hóa nó bằng cách debounce hoặc throttle các lần gọi setState nếu cần thiết. Nên cân nhắc khi nào (hoặc không nên dùng)? Ứng dụng mobile native thuần túy: Trên các thiết bị di động không có chuột, MouseRegion sẽ không có tác dụng gì cả. Đừng tốn công sức vào nó nhé! Nhớ nhé các bạn, MouseTrackerAnnotation là nền tảng, còn MouseRegion là công cụ bạn dùng hàng ngày. Nắm vững nó, các bạn sẽ biến ứng dụng Flutter của mình từ "bình thường" thành "siêu mượt" trong mắt người dùng chuột đấy! 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é!

44 Đọc tiếp
MouseTracker Flutter: Bắt trọn khoảnh khắc chuột lướt!
19/03/2026

MouseTracker Flutter: Bắt trọn khoảnh khắc chuột lướt!

Chào các bạn Gen Z mê công nghệ, hôm nay anh Creyt sẽ dẫn các bạn khám phá một 'thám tử' cực xịn trong Flutter, chuyên đi 'rình mò' chuyển động của con chuột: MouseTracker, mà cụ thể hơn là widget MouseRegion! Tưởng tượng thế này: UI của bạn giống như một bữa tiệc. Bình thường, mọi thứ cứ đứng yên đó, người dùng đến thì dùng thôi. Nhưng với MouseRegion, bữa tiệc của bạn sẽ có một 'bảo vệ' hoặc một 'người phục vụ' siêu tinh ý. Cứ thấy 'khách' (con chuột) vừa lướt qua khu vực nào, là lập tức có phản ứng: đèn sáng lên, món ăn được đẩy ra, hoặc thậm chí là nhạc đổi bài. Nghe 'chill' không? Đúng vậy, MouseRegion sinh ra để biến những UI tĩnh như bức tranh thành những tác phẩm tương tác sống động, mang lại trải nghiệm 'mượt mà' và 'có hồn' hơn rất nhiều. Nó là chìa khóa để tạo ra các hiệu ứng hover thần thánh, những menu dropdown tự động hiện ra, hay những nút bấm 'nhấp nháy' mời gọi khi bạn rê chuột qua. MouseRegion hoạt động như thế nào? (The Guts) Vậy làm sao mà 'thám tử' này hoạt động? Đơn giản thôi, Flutter cung cấp cho chúng ta widget MouseRegion. Bạn cứ bọc bất kỳ widget nào muốn 'bắt sóng' chuyển động chuột bằng MouseRegion là xong. Nó sẽ cung cấp cho bạn các 'callback' thần thánh: onEnter: Khi con chuột 'bước vào' khu vực của widget. onExit: Khi con chuột 'bước ra' khỏi khu vực. onHover: Khi con chuột 'đang lượn lờ' bên trong khu vực. (Thằng này là 'thám tử' chính hiệu, báo cáo từng cử động nhỏ nhất). Ngoài ra, nó còn có thuộc tính cursor để bạn thay đổi hình dạng con trỏ chuột khi nó nằm trong vùng đó. Muốn biến thành cái tay, cái kính lúp, hay thậm chí là một icon custom? MouseRegion cân tất! Code Ví Dụ Minh Hoạ: Box biến hình! Nói suông thì các bạn lại bảo anh Creyt 'chém gió'. Giờ thì 'thực chiến' ngay với một ví dụ kinh điển: đổi màu, hiện bóng và thay đổi bo góc khi rê chuột qua một cái hộp. 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: 'MouseTracker Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MouseTrackerDemo(), ); } } class MouseTrackerDemo extends StatefulWidget { const MouseTrackerDemo({super.key}); @override State<MouseTrackerDemo> createState() => _MouseTrackerDemoState(); } class _MouseTrackerDemoState extends State<MouseTrackerDemo> { Color _boxColor = Colors.blueGrey; String _message = 'Rê chuột vào đây xem!'; bool _isHovering = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('MouseRegion Magic'), ), body: Center( child: MouseRegion( onEnter: (event) { setState(() { _boxColor = Colors.lightBlueAccent; _message = 'Chào mừng bạn đã đến!'; _isHovering = true; }); print('Mouse Entered!'); // Để debug }, onExit: (event) { setState(() { _boxColor = Colors.blueGrey; _message = 'Tạm biệt, hẹn gặp lại!'; _isHovering = false; }); print('Mouse Exited!'); // Để debug }, onHover: (event) { // Có thể dùng để hiển thị tọa độ chuột hoặc các hiệu ứng phức tạp hơn // Ví dụ: _message = 'Bạn đang ở X: ${event.localPosition.dx.toInt()}, Y: ${event.localPosition.dy.toInt()}'; // setState(() {}); print('Mouse Hovering at ${event.localPosition}'); }, cursor: SystemMouseCursors.click, // Thay đổi con trỏ thành icon click child: AnimatedContainer( // Dùng AnimatedContainer để hiệu ứng chuyển màu mượt mà duration: const Duration(milliseconds: 200), width: 200, height: 200, decoration: BoxDecoration( color: _boxColor, borderRadius: BorderRadius.circular(_isHovering ? 20 : 8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(_isHovering ? 0.3 : 0.1), blurRadius: _isHovering ? 15 : 5, offset: Offset(0, _isHovering ? 10 : 3), ), ], ), alignment: Alignment.center, child: Text( _message, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ), ); } } Mẹo hay từ anh Creyt (Best Practices) Anh Creyt có vài 'mẹo nhỏ' để các bạn dùng MouseRegion không bị 'phản tác dụng' nè: Đừng lạm dụng quá: Giống như gia vị, vừa đủ thì ngon, cho nhiều quá là hỏng bét. Quá nhiều hiệu ứng hover có thể làm UI của bạn trở nên 'rối rắm' và 'nặng nề'. Chỉ dùng cho những chỗ cần nhấn mạnh hoặc cung cấp thông tin thêm thôi nhé. Phản hồi rõ ràng: Khi có tương tác chuột, hãy đảm bảo người dùng thấy rõ sự thay đổi. Đổi màu, thêm bóng, phóng to nhẹ, hoặc hiện tooltip là những cách hiệu quả. Đừng để họ 'mò mẫm' không biết có gì xảy ra không. Tương thích mọi nền tảng: MouseRegion chủ yếu dành cho Web và Desktop. Với Mobile, nơi người dùng dùng ngón tay, nó sẽ không có tác dụng. Luôn nhớ thiết kế UI của bạn phải 'responsive' với cả chuột và chạm nhé. Kết hợp với Animation: Để hiệu ứng mượt mà, hãy dùng AnimatedContainer, TweenAnimationBuilder hoặc các widget animation khác. Sự chuyển động 'uyển chuyển' sẽ làm tăng trải nghiệm người dùng lên tầm cao mới. Accessibility: Nhớ rằng không phải ai cũng dùng chuột. Người dùng bàn phím hoặc công nghệ hỗ trợ cần có cách khác để truy cập các tính năng mà MouseRegion mang lại. Ví dụ thực tế: Ai đang dùng chiêu này? Thế thì mấy cái website/app 'xịn xò' nào đang dùng chiêu này của anh Creyt? Menu điều hướng (Navigation Menus): Bạn thấy trên các trang web như Google, Facebook, hoặc các trang thương mại điện tử, khi rê chuột qua một mục menu, nó thường đổi màu, gạch chân, hoặc hiện ra một submenu con không? Đó chính là MouseRegion đang làm việc đó. Thẻ sản phẩm/Bài viết (Product/Article Cards): Trên Shopee, Tiki, Medium, khi bạn lướt qua một sản phẩm hay một bài viết, cái thẻ đó thường "nhô" lên một chút, có bóng đổ đẹp hơn, hoặc hiện ra nút "Thêm vào giỏ hàng" hay "Đọc thêm" ẩn? MouseRegion đó! Nút bấm tương tác (Interactive Buttons): Các nút "Like", "Share" hay bất kỳ nút CTA (Call To Action) nào trên các ứng dụng Flutter Desktop/Web của bạn đều có thể dùng MouseRegion để thêm hiệu ứng hover, khiến chúng trở nên "mời gọi" hơn. Tooltip: Khi bạn rê chuột qua một icon nhỏ hoặc một đoạn text không rõ nghĩa, một cái hộp nhỏ chứa thông tin giải thích hiện ra. Chính là MouseRegion kết hợp với Tooltip đấy. Thử nghiệm đã từng và nên dùng cho case nào? Anh Creyt đã từng 'vọc vạch' với MouseRegion khá nhiều rồi, và đây là vài kinh nghiệm xương máu: Nên dùng cho: Flutter Web Apps: Tuyệt vời để tạo ra trải nghiệm web hiện đại, mượt mà, không thua kém gì các trang web dùng HTML/CSS/JS truyền thống. Flutter Desktop Apps (Windows, macOS, Linux): Các ứng dụng desktop rất cần sự phản hồi của chuột để mang lại cảm giác chuyên nghiệp và dễ sử dụng. Từ các nút bấm, thanh cuộn tùy chỉnh, đến các khu vực kéo thả (drag-and-drop), MouseRegion là 'bạn thân'. UI có tính năng kéo thả (Drag-and-Drop): Khi bạn muốn kéo một item từ vị trí này sang vị trí khác, MouseRegion có thể giúp bạn làm nổi bật vùng đích khi chuột kéo item đi qua. Không nên hoặc ít cần dùng cho: Flutter Mobile Apps (iOS, Android): Như đã nói, mobile không có chuột. Dù MouseRegion vẫn tồn tại, nhưng nó sẽ không bao giờ được kích hoạt bởi ngón tay. Đừng phí thời gian tối ưu nó cho mobile nếu không có keyboard/mouse ngoại vi. Các UI tĩnh, không cần tương tác: Nếu chỉ hiển thị thông tin mà không có bất kỳ hành động nào từ người dùng, thì MouseRegion là không cần thiết và chỉ làm tăng thêm gánh nặng cho cây widget. Tóm lại, MouseRegion không chỉ là một widget đơn thuần, nó là một công cụ mạnh mẽ giúp bạn 'thổi hồn' vào UI, biến những tương tác đơn giản thành những trải nghiệm đáng nhớ. Hãy 'quẩy' hết mình với nó, nhưng nhớ 'quẩy' có kiểm soát nhé các bạn developer tương lai! 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é!

37 Đọc tiếp