Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
TabPageSelector: Mắt Thần Của Giao Diện, Dẫn Lối GenZ Không Lạc Lối!
21/03/2026

TabPageSelector: Mắt Thần Của Giao Diện, Dẫn Lối GenZ Không Lạc Lối!

Chào các GenZ developer tương lai, và cả những chiến thần code đã lăn lộn trên chiến trường! Anh Creyt lại xuất hiện để khai sáng cho các em một khái niệm tưởng chừng nhỏ bé nhưng lại cực kỳ quan trọng trong việc "dụ dỗ" người dùng ở lại app của mình: TabPageSelector trong Flutter. TabPageSelector: Mắt Thần Của Giao Diện, Dẫn Lối GenZ Không Lạc Lối! Các em cứ hình dung thế này, khi các em lướt TikTok, xem story trên Instagram, hay thậm chí là xem mấy cái quảng cáo "swipe-up" trên app nào đó, có phải đôi khi các em thấy mấy cái chấm tròn nhỏ xíu ở đâu đó trên màn hình không? Mấy cái chấm đó thay đổi màu sắc, to nhỏ tùy theo việc các em đang ở trang nào, slide nào. Chính xác! TabPageSelector trong Flutter chính là "mấy cái chấm thần thánh" đó. Nói một cách hàn lâm hơn nhưng vẫn dễ hiểu, TabPageSelector là một widget "chuyên gia chỉ điểm". Nó không tự mình làm gì cả, không có khả năng điều khiển hay chuyển trang. Nhiệm vụ duy nhất của nó là "nhìn" vào một TabController hoặc PageController (cái này mới là "ông chủ" thực sự điều khiển các trang), và sau đó "báo hiệu" cho người dùng biết hiện tại họ đang đứng ở vị trí nào trong chuỗi các trang đó. Nó giống như cái đèn tín hiệu trên bảng điều khiển xe hơi vậy, chỉ báo hiệu chứ không lái xe. Để làm gì? Hay, "Tại sao mình cần nó, anh Creyt?" Đơn giản là để cải thiện trải nghiệm người dùng (UX) một cách thần sầu. Chỉ dẫn trực quan: Người dùng sẽ biết ngay họ đang ở trang 1 trong 5 trang, hay trang cuối cùng rồi, không còn cảm giác "lạc trôi" giữa biển thông tin. Tăng tương tác: Khi người dùng thấy có nhiều trang, họ có xu hướng vuốt xem hết hơn, đặc biệt là trong các màn hình onboarding (giới thiệu ứng dụng) hay gallery ảnh sản phẩm. Thẩm mỹ: Một hàng chấm nhỏ xinh xắn, được tùy chỉnh màu sắc, kích thước hợp lý sẽ làm giao diện của em trông chuyên nghiệp và "có gu" hơn hẳn. Code Ví Dụ Minh Họa: "Thực chiến" ngay và luôn! Để TabPageSelector hoạt động, em cần một "ông chủ" là TabController (hoặc PageController cho PageView). Ở đây, anh sẽ dùng DefaultTabController để mọi thứ đơn giản như ăn kẹo. 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: 'TabPageSelector Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const TabPageSelectorScreen(), ); } } class TabPageSelectorScreen extends StatefulWidget { const TabPageSelectorScreen({super.key}); @override State<TabPageSelectorScreen> createState() => _TabPageSelectorScreenState(); } class _TabPageSelectorScreenState extends State<TabPageSelectorScreen> with SingleTickerProviderStateMixin { late TabController _tabController; final List<Color> _pageColors = [ Colors.redAccent, Colors.greenAccent, Colors.blueAccent, Colors.purpleAccent, ]; @override void initState() { super.initState(); _tabController = TabController(length: _pageColors.length, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TabPageSelector Của Creyt'), bottom: TabBar( // Dùng TabBar ở đây nếu muốn có tabs truyền thống controller: _tabController, tabs: const [ Tab(icon: Icon(Icons.home)), Tab(icon: Icon(Icons.search)), Tab(icon: Icon(Icons.settings)), Tab(icon: Icon(Icons.person)), ], ), ), body: Column( children: [ Expanded( child: TabBarView( controller: _tabController, children: _pageColors.asMap().entries.map((entry) { int index = entry.key; Color color = entry.value; return Container( color: color, child: Center( child: Text( 'Trang số ${index + 1}', style: const TextStyle(fontSize: 30, color: Colors.white), ), ), ); }).toList(), ), ), Padding( padding: const EdgeInsets.all(16.0), child: TabPageSelector( controller: _tabController, // KẾT NỐI VỚI "ÔNG CHỦ" Ở ĐÂY! selectedColor: Colors.deepOrange, // Màu chấm khi được chọn color: Colors.grey.shade400, // Màu chấm khi không được chọn indicatorSize: 12.0, // Kích thước của mỗi chấm ), ), const SizedBox(height: 20), ], ), ); } } Giải Thích Code: "Mổ xẻ" ra xem nó có gì! _tabController = TabController(...): Đây là trái tim của mọi thứ. Anh khởi tạo một TabController với length bằng số lượng trang (ở đây là 4 màu). vsync: this là cần thiết để animation hoạt động mượt mà, và thường được cung cấp bởi SingleTickerProviderStateMixin mà anh with vào _TabPageSelectorScreenState. TabBarView(...): Đây là nơi chứa các trang thực tế của em. Nó sẽ hiển thị từng Container với màu sắc khác nhau. Quan trọng là nó cũng được gắn với _tabController. TabPageSelector(...): Đây là ngôi sao của chúng ta! controller: _tabController: Đây là lúc TabPageSelector "bắt tay" với TabController để biết được trạng thái hiện tại. Nó sẽ "theo dõi" ông chủ của nó. selectedColor, color: Tùy chỉnh màu sắc cho chấm đang được chọn và các chấm còn lại. Giúp app của em "đẹp trai" hơn. indicatorSize: Kích thước của mỗi chấm. Điều chỉnh cho phù hợp với thiết kế của em. Khi em vuốt qua lại giữa các trang trong TabBarView, em sẽ thấy TabPageSelector tự động đổi màu chấm tương ứng, báo hiệu em đang ở trang nào. Tuyệt vời chưa? Mẹo Hay Từ Creyt (Best Practices): "Để Code Không Chỉ Chạy Mà Còn Bay!" Luôn đi kèm với "ông chủ": TabPageSelector vô dụng nếu không có TabController hoặc PageController. Hãy đảm bảo chúng được kết nối đúng cách. Tùy biến hết cỡ: Đừng ngại thay đổi selectedColor, color, indicatorSize. Đây là những props nhỏ nhưng tạo ra sự khác biệt lớn về mặt thẩm mỹ và nhận diện thương hiệu. Không dùng cho TabBar truyền thống: Nếu em muốn người dùng nhấn vào các chấm để chuyển tab, thì đó không phải là việc của TabPageSelector. Lúc đó, em cần dùng TabBar widget (như anh có đặt tạm trong AppBar ví dụ trên) hoặc tự xây dựng widget riêng. TabPageSelector là để hiển thị thôi, không phải để tương tác. Cân nhắc ngữ cảnh: Nó cực kỳ hiệu quả cho các màn hình giới thiệu (onboarding), gallery ảnh, hoặc các bước trong một quy trình (ví dụ: đăng ký nhiều bước). Ứng Dụng Thực Tế: "Ai đang dùng nó ngoài kia?" Em có thể thấy TabPageSelector (hoặc các biến thể của nó) ở rất nhiều nơi: Màn hình Onboarding: Khi em cài app mới, thường có vài trang giới thiệu tính năng. Mấy cái chấm dưới cùng chính là nó đó. Gallery ảnh sản phẩm: Trên các app thương mại điện tử, khi em xem nhiều ảnh của một sản phẩm, thường có chấm tròn báo hiệu em đang xem ảnh số mấy. Stories trên mạng xã hội: Mặc dù không phải lúc nào cũng là chấm tròn, nhưng cái ý tưởng "hiển thị tiến độ/vị trí" là tương tự. Các bước điền form: Một form có nhiều bước, mỗi bước là một trang, và các chấm tròn giúp người dùng biết họ đang ở bước nào. Thử Nghiệm Đã Từng và Lời Khuyên Nên Dùng Cho Case Nào: "Kinh nghiệm xương máu của anh Creyt!" Anh Creyt đã từng "lỡ tay" dùng TabPageSelector ở những nơi không phù hợp. Ví dụ, cố gắng biến nó thành một TabBar có thể bấm được. Kết quả là mất thời gian, code phức tạp và người dùng thì bối rối. Nên dùng khi: Em có một PageView hoặc TabBarView mà em muốn người dùng vuốt để chuyển trang, và chỉ cần một chỉ báo trực quan về vị trí hiện tại. Các màn hình giới thiệu sản phẩm/ứng dụng (onboarding flows). Các gallery ảnh, album. Màn hình hướng dẫn từng bước mà không cần người dùng phải bấm vào các bước để nhảy cóc. Không nên dùng khi: Em muốn người dùng tương tác trực tiếp với các chỉ báo (ví dụ, bấm vào chấm thứ 3 để nhảy đến trang 3). Lúc này, TabBar hoặc các custom navigation widget mới là chân ái. Số lượng trang quá lớn (ví dụ, 20-30 trang). Một hàng dài chấm chấm sẽ trông rất rối mắt và không hiệu quả. Lúc đó, có lẽ em cần một cách điều hướng khác như danh sách hoặc menu. Nhớ nhé các em, TabPageSelector không phải là một chiến binh mạnh mẽ tự thân, mà nó là một "trợ lý đắc lực" giúp "ông chủ" TabController hoặc PageController tỏa sáng, mang lại trải nghiệm mượt mà và trực quan cho người dùng. Nắm vững nó, và giao diện của em sẽ "hack não" người dùng một cách tích cực đấy! Cố lê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é!

35 Đọc tiếp
TabControllerState: Phù Thủy Điều Khiển Tab View trong Flutter
21/03/2026

TabControllerState: Phù Thủy Điều Khiển Tab View trong Flutter

Chào các đồ công nghệ của anh Creyt! Hôm nay chúng ta sẽ giải mã một cái tên nghe có vẻ hàn lâm nhưng lại là "tay chơi" cực kỳ quan trọng trong thế giới Flutter: TabControllerState. 1. TabControllerState là gì? (Giải thích siêu đơn giản theo GenZ) Tưởng tượng mà xem, các em có một cái TV hiện đại (chính là TabBarView) và một cái điều khiển từ xa xịn sò (chính là TabBar). Khi em bấm nút số 1, TV hiện kênh VTV1. Bấm nút số 2, TV hiện kênh VTV2. Vậy ai là người đứng sau hậu trường, đảm bảo rằng cái TV nó nghe lời cái điều khiển? Chính là TabControllerState đấy! Nói một cách hoa mỹ hơn, TabControllerState là bộ não, là linh hồn kết nối giữa TabBar (cái hàng tab mà các em bấm vào) và TabBarView (cái nội dung tương ứng bên dưới). Nó giữ trách nhiệm theo dõi xem tab nào đang được chọn, và đảm bảo nội dung phù hợp được hiển thị. Nó không chỉ là một cái công tắc, mà còn là một nhạc trưởng điều phối mọi thứ, giúp các em có thể chuyển tab bằng code, lắng nghe sự kiện chuyển tab, hay thậm chí là tùy chỉnh animation chuyển đổi. Hiểu nôm na là, nếu không có nó, cái TabBar và TabBarView của các em sẽ như hai thằng bạn thân nhưng không ai chịu nói chuyện với ai, mỗi đứa một thế giới vậy! 2. Code Ví Dụ Minh Họa (Chuẩn kiến thức, dễ hiểu) Chúng ta có hai cách chính để sử dụng TabController: Cách 1: Đơn giản với DefaultTabController (Cho người mới bắt đầu) Đây là cách "mì ăn liền", Flutter tự động tạo và quản lý TabController cho các em. Phù hợp cho các trường hợp đơn giản, không cần can thiệp sâu. 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: 'Creyt Tab Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const SimpleTabScreen(), ); } } class SimpleTabScreen extends StatelessWidget { const SimpleTabScreen({super.key}); @override Widget build(BuildContext context) { const int numberOfTabs = 3; // Quan trọng: số tab phải khớp số view return DefaultTabController( length: numberOfTabs, child: Scaffold( appBar: AppBar( title: const Text('TabControllerState Đơn Giản'), bottom: const TabBar( tabs: [ Tab(icon: Icon(Icons.home), text: 'Trang Chủ'), Tab(icon: Icon(Icons.settings), text: 'Cài Đặt'), Tab(icon: Icon(Icons.info), text: 'Thông Tin'), ], ), ), body: const TabBarView( children: [ Center(child: Text('Đây là nội dung Trang Chủ nè!')), Center(child: Text('Đây là nội dung Cài Đặt nè!')), Center(child: Text('Đây là nội dung Thông Tin nè!')), ], ), ), ); } } Giải thích: DefaultTabController sẽ tự động tạo một TabController và truyền xuống cây Widget. Các em chỉ cần khai báo length (số lượng tab) và nó sẽ tự động đồng bộ TabBar và TabBarView. Cách 2: Nâng cao với TabController tự định nghĩa (Kiểm soát tuyệt đối) Khi các em muốn làm chủ cuộc chơi, muốn chuyển tab bằng code, lắng nghe sự kiện, hoặc tích hợp với các giải pháp quản lý state khác, thì đây là cách dành cho các em. Chúng ta cần một StatefulWidget và SingleTickerProviderStateMixin. 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: 'Creyt Tab Demo Nâng Cao', theme: ThemeData(primarySwatch: Colors.green), home: const AdvancedTabScreen(), ); } } class AdvancedTabScreen extends StatefulWidget { const AdvancedTabScreen({super.key}); @override State<AdvancedTabScreen> createState() => _AdvancedTabScreenState(); } class _AdvancedTabScreenState extends State<AdvancedTabScreen> with SingleTickerProviderStateMixin { // <<< Đây là key! late TabController _tabController; final List<String> _tabs = ['Sản Phẩm', 'Đánh Giá', 'Liên Quan']; @override void initState() { super.initState(); _tabController = TabController(length: _tabs.length, vsync: this); // vsync cần SingleTickerProviderStateMixin // Lắng nghe sự kiện chuyển tab _tabController.addListener(() { if (_tabController.indexIsChanging) { // Đây là khi người dùng bắt đầu vuốt hoặc bấm tab print('Tab sắp chuyển sang index: ${_tabController.index}'); } else { // Đây là khi tab đã chuyển xong print('Tab đã chuyển xong tới index: ${_tabController.index}'); // Ví dụ: Load dữ liệu mới cho tab vừa chọn _loadDataForTabIndex(_tabController.index); } }); } void _loadDataForTabIndex(int index) { // Giả lập việc load dữ liệu print('Đang tải dữ liệu cho tab: ${_tabs[index]}'); // Các em có thể gọi API ở đây } @override void dispose() { _tabController.dispose(); // <<< Quan trọng: Dọn dẹp để tránh rò rỉ bộ nhớ super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TabControllerState Nâng Cao'), bottom: TabBar( controller: _tabController, // Gán controller tự định nghĩa tabs: _tabs.map((tab) => Tab(text: tab)).toList(), ), ), body: TabBarView( controller: _tabController, // Gán controller tự định nghĩa children: _tabs.map((tab) => Center(child: Text('Nội dung của tab $tab'))).toList(), ), floatingActionButton: FloatingActionButton( onPressed: () { // Ví dụ: Chuyển sang tab kế tiếp bằng code int nextIndex = (_tabController.index + 1) % _tabs.length; _tabController.animateTo(nextIndex); // Chuyển có animation // _tabController.index = nextIndex; // Chuyển ngay lập tức }, child: const Icon(Icons.arrow_forward), ), ); } } Giải thích: SingleTickerProviderStateMixin: Cung cấp Ticker cần thiết cho các animation của TabController. Các em cứ hiểu đơn giản là nó giúp cho việc chuyển tab mượt mà hơn, có hiệu ứng đẹp mắt. _tabController = TabController(...): Khởi tạo TabController với số lượng tab (length) và vsync (chính là this từ SingleTickerProviderStateMixin). _tabController.addListener(): Đây là nơi các em có thể "nghe lén" xem khi nào tab chuyển đổi. Rất hữu ích để trigger các hành động như load dữ liệu, gửi analytics. _tabController.dispose(): Cực kỳ quan trọng! Luôn luôn gọi phương thức này trong dispose() của State để giải phóng bộ nhớ khi widget không còn được sử dụng nữa. Nếu quên, ứng dụng của các em sẽ bị rò rỉ bộ nhớ, dần dần chạy chậm và có thể crash. _tabController.animateTo(index): Phương thức này cho phép các em chuyển tab một cách lập trình, có hiệu ứng trượt mượt mà. 3. Mẹo Vặt & Best Practices từ Creyt (Ghi nhớ & Dùng thực tế) DefaultTabController là "thằng lười nhưng hiệu quả": Dùng khi chỉ cần tab hoạt động cơ bản, không cần can thiệp sâu vào logic chuyển tab. Nó giúp code của các em gọn gàng hơn nhiều. TabController là "tay chơi chuyên nghiệp": Dùng khi cần toàn quyền kiểm soát: chuyển tab bằng code, lắng nghe sự kiện, tích hợp với state management phức tạp. Đừng ngại dùng nó khi cần sự linh hoạt. "Đừng quên dọn dẹp nhà cửa": Luôn luôn dispose() cái TabController khi State bị hủy để tránh rò rỉ bộ nhớ. Đây là lỗi kinh điển mà nhiều lập trình viên mới mắc phải đấy! "Số lượng là vàng": Số lượng Tab trong TabBar phải khớp chính xác với số lượng Widget trong TabBarView. Sai một ly, đi một dặm (UI crash). Cẩn thận đếm cho đúng nhé! SingleTickerProviderStateMixin là "bạn thân" của TabController: Nó cung cấp Ticker cần thiết cho các animation của tab. Nhớ thêm nó vào StatefulWidget khi dùng TabController tự định nghĩa. 4. Ứng Dụng Thực Tế (Đã từng dùng ở đâu?) TabControllerState là một "ngôi sao thầm lặng", có mặt ở khắp mọi nơi mà các em không hề hay biết: Instagram Profile: Các tab "Bài viết", "Reels", "Được gắn thẻ" trên trang cá nhân của bạn bè. Khi bạn bấm vào, nội dung bên dưới thay đổi ngay lập tức. Shopee/Lazada (Trang chi tiết sản phẩm): Các tab "Mô tả sản phẩm", "Đánh giá", "Sản phẩm liên quan". Giúp người dùng dễ dàng chuyển đổi qua lại giữa các phần thông tin. Ứng dụng Tin tức (ví dụ: Google News, VnExpress): Các tab "Mới nhất", "Nổi bật", "Đã lưu" ở thanh điều hướng trên hoặc dưới. Cài đặt ứng dụng: Nhiều ứng dụng có màn hình cài đặt chia thành các tab như "Thông báo", "Tài khoản", "Bảo mật". Tất cả những nơi đó, đều có bóng dáng của TabControllerState đang âm thầm làm việc đấy các em! Nó 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. 5. Thử Nghiệm & Hướng Dẫn Sử Dụng (Khi nào nên dùng gì?) Anh Creyt khuyên các em nên thử nghiệm cả hai cách để hiểu rõ hơn bản chất của TabControllerState. Nên dùng DefaultTabController khi: UI đơn giản, các tab không cần logic phức tạp hoặc tương tác đặc biệt (ví dụ: một màn hình cài đặt có vài tab tĩnh). Các em mới bắt đầu và muốn nhanh chóng có tab hoạt động mà không cần lo lắng về quản lý state. Khi TabBar và TabBarView nằm cùng một StatelessWidget hoặc một StatefulWidget không cần TabController để làm gì khác ngoài đồng bộ hóa. Nên dùng TabController tự định nghĩa khi: Cần chuyển tab sau khi gọi API, hoặc sau một sự kiện nào đó (ví dụ: bấm nút "Tiếp tục" ở màn hình khác, tự động chuyển sang tab kế tiếp). Muốn theo dõi sự kiện chuyển tab để gửi analytics, load dữ liệu mới hoặc cập nhật UI ở nơi khác trong ứng dụng. Cần tùy chỉnh animation khi chuyển tab hoặc muốn kiểm soát tốc độ chuyển đổi. Tích hợp với các giải pháp quản lý state phức tạp hơn như Provider, BLoC, GetX... (ví dụ: khi tab chuyển, bắn event vào BLoC để load state mới). Ví dụ: Trang onboarding có các tab đại diện cho các bước, hoặc một form nhiều bước mà các em muốn điều hướng qua lại giữa các bước. Nhớ kỹ nhé các đồ công nghệ, việc lựa chọn đúng công cụ sẽ giúp các em tiết kiệm rất nhiều thời gian và công sức trong quá trình phát triển ứng dụng Flutter. Cứ mạnh dạn thử nghiệm và mày mò, rồi các em sẽ thấy TabControllerState là một người bạn đồng hành cực kỳ đắc lực! 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
Stepper Flutter: Thầy Creyt giải mã hành trình từng bước cho Gen Z
21/03/2026

Stepper Flutter: Thầy Creyt giải mã hành trình từng bước cho Gen Z

Chào các "chiến thần code" tương lai! Hôm nay, Thầy Creyt sẽ dẫn các bạn đi "farm" một con boss tên là Stepper trong thế giới Flutter. Nghe tên đã thấy mùi "từng bước, từng bước" rồi đúng không? Chính xác! Thằng này sinh ra để giúp chúng ta chia nhỏ những nhiệm vụ phức tạp thành các bước nhỏ hơn, dễ thở hơn, giống như cách các bạn chia nhỏ bài tập lớn thành từng phần để đỡ bị "overload" vậy. Stepper là gì và để làm gì? Thử tưởng tượng thế này: Bạn đang order trà sữa online. Bạn sẽ không bao giờ thấy một cái form dài dằng dặc yêu cầu bạn điền thông tin địa chỉ, chọn topping, chọn size, chọn thanh toán... tất cả trên cùng một màn hình đúng không? Mà nó sẽ chia ra thành: "Bước 1: Chọn món", "Bước 2: Điền thông tin giao hàng", "Bước 3: Thanh toán". Đó chính là Stepper trong thực tế! Trong Flutter, Stepper là một widget mạnh mẽ giúp bạn tạo ra các quy trình từng bước (stepped process). Nó giống như một "bản đồ kho báu" chỉ dẫn người dùng đi từng chặng một để hoàn thành một nhiệm vụ nào đó. Thay vì bắt người dùng "bơi" trong một biển thông tin, Stepper giúp họ "nhảy cóc" qua từng hòn đảo nhỏ, mỗi hòn đảo là một bước, một nhiệm vụ con. Để làm gì ư? Đơn giản là để: Cải thiện UX (User Experience): Người dùng không bị choáng ngợp, biết mình đang ở đâu và còn bao nhiêu bước nữa. Giảm thiểu "friction" (sự khó chịu). Quản lý quy trình phức tạp: Chia nhỏ các form đăng ký, quy trình thanh toán, hướng dẫn sử dụng (onboarding) thành các phần logic. Dễ dàng validation: Bạn có thể kiểm tra dữ liệu của từng bước trước khi cho phép người dùng qua bước tiếp theo. Code Ví Dụ Minh Họa: "Order Trà Sữa" phiên bản Flutter Chúng ta sẽ xây dựng một Stepper đơn giản với 3 bước: Chọn món, Thông tin giao hàng, và Thanh toán. 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 Stepper Demo', theme: ThemeData( primarySwatch: Colors.teal, ), home: const StepperHomePage(), ); } } class StepperHomePage extends StatefulWidget { const StepperHomePage({super.key}); @override State<StepperHomePage> createState() => _StepperHomePageState(); } class _StepperHomePageState extends State<StepperHomePage> { int _currentStep = 0; // Biến này sẽ theo dõi bước hiện tại // Dữ liệu giả định cho các bước String _selectedTea = 'Trà Sữa Trân Châu Đường Đen'; String _customerName = ''; String _customerAddress = ''; // GlobalKey để truy cập trạng thái của form (nếu có) final GlobalKey<FormState> _formKeyStep2 = GlobalKey<FormState>(); List<Step> get _steps => [ Step( title: const Text('Chọn Món'), content: Column( children: <Widget>[ RadioListTile<String>( title: const Text('Trà Sữa Trân Châu Đường Đen'), value: 'Trà Sữa Trân Châu Đường Đen', groupValue: _selectedTea, onChanged: (String? value) { setState(() { _selectedTea = value!; }); }, ), RadioListTile<String>( title: const Text('Trà Xanh Kem Cheese'), value: 'Trà Xanh Kem Cheese', groupValue: _selectedTea, onChanged: (String? value) { setState(() { _selectedTea = value!; }); }, ), RadioListTile<String>( title: const Text('Hồng Trà Sữa'), value: 'Hồng Trà Sữa', groupValue: _selectedTea, onChanged: (String? value) { setState(() { _selectedTea = value!; }); }, ), const SizedBox(height: 16), Text('Bạn đã chọn: $_selectedTea'), ], ), isActive: _currentStep >= 0, state: _currentStep > 0 ? StepState.complete : StepState.indexed, ), Step( title: const Text('Thông Tin Giao Hàng'), content: Form( key: _formKeyStep2, child: Column( children: <Widget>[ TextFormField( decoration: const InputDecoration(labelText: 'Tên của bạn'), onSaved: (value) => _customerName = value!, validator: (value) { if (value == null || value.isEmpty) { return 'Vui lòng nhập tên'; } return null; }, ), TextFormField( decoration: const InputDecoration(labelText: 'Địa chỉ giao hàng'), onSaved: (value) => _customerAddress = value!, validator: (value) { if (value == null || value.isEmpty) { return 'Vui lòng nhập địa chỉ'; } return null; }, ), ], ), ), isActive: _currentStep >= 1, state: _currentStep > 1 ? StepState.complete : StepState.indexed, ), Step( title: const Text('Thanh Toán'), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text('Món đã chọn: $_selectedTea'), Text('Người nhận: $_customerName'), Text('Địa chỉ: $_customerAddress'), const SizedBox(height: 16), const Text('Phương thức thanh toán: Tiền mặt khi nhận hàng'), const Text('Tổng cộng: 50.000 VNĐ (ví dụ)'), ], ), isActive: _currentStep >= 2, state: _currentStep == 2 ? StepState.editing : StepState.indexed, ), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Order Trà Sữa Cùng Thầy Creyt'), ), body: Stepper( type: StepperType.vertical, // Có thể là .horizontal currentStep: _currentStep, onStepContinue: () { // Logic khi nhấn nút 'Tiếp tục' final isLastStep = _currentStep == _steps.length - 1; if (isLastStep) { // Xử lý hoàn tất đơn hàng ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Đơn hàng của bạn đã được đặt!')), ); // Reset về bước đầu tiên hoặc chuyển sang màn hình khác setState(() { _currentStep = 0; _customerName = ''; _customerAddress = ''; _selectedTea = 'Trà Sữa Trân Châu Đường Đen'; }); } else { // Kiểm tra validation cho bước 2 trước khi qua bước tiếp theo if (_currentStep == 1) { if (_formKeyStep2.currentState!.validate()) { _formKeyStep2.currentState!.save(); setState(() => _currentStep += 1); } else { // Nếu validation thất bại, không chuyển bước } } else { setState(() => _currentStep += 1); } } }, onStepCancel: () { // Logic khi nhấn nút 'Quay lại' if (_currentStep == 0) return; // Không lùi được nữa setState(() => _currentStep -= 1); }, onStepTapped: (step) { // Logic khi người dùng chạm vào một bước bất kỳ setState(() => _currentStep = step); }, // Tùy chỉnh các nút điều khiển controlsBuilder: (context, details) { return Padding( padding: const EdgeInsets.only(top: 16.0), child: Row( children: <Widget>[ Expanded( child: ElevatedButton( onPressed: details.onStepContinue, child: Text(details.currentStep == _steps.length - 1 ? 'Hoàn Tất' : 'Tiếp Tục'), ), ), const SizedBox(width: 10), if (details.currentStep != 0) Expanded( child: OutlinedButton( onPressed: details.onStepCancel, child: const Text('Quay Lại'), ), ), ], ), ); }, steps: _steps, ), ); } } Giải thích Code: _currentStep: Đây là biến int quan trọng nhất, nó lưu trữ chỉ số của bước hiện tại. Khi _currentStep thay đổi, UI của Stepper sẽ tự động cập nhật. Stepper Widget: Widget chính. type: Có thể là StepperType.vertical (mặc định, các bước xếp dọc) hoặc StepperType.horizontal (các bước xếp ngang, thường dùng cho ít bước). currentStep: Gán bằng _currentStep của StatefulWidget của chúng ta. onStepContinue: Hàm được gọi khi người dùng nhấn nút "Tiếp tục". Đây là nơi bạn xử lý logic chuyển bước, kiểm tra dữ liệu, hoặc gửi dữ liệu lên server. onStepCancel: Hàm được gọi khi người dùng nhấn nút "Quay lại". onStepTapped: Hàm được gọi khi người dùng chạm vào tiêu đề của một bước bất kỳ để nhảy đến bước đó. Thầy Creyt thường dùng để cho phép người dùng quay lại các bước trước để chỉnh sửa. steps: Một List<Step> chứa tất cả các bước của quy trình. Step Widget: Mỗi Step đại diện cho một bước trong quy trình. title: Tiêu đề của bước (ví dụ: const Text('Chọn Món')). content: Nội dung chính của bước, có thể là bất kỳ widget nào (ví dụ: Column chứa RadioListTile hoặc TextFormField). isActive: Boolean. Nếu true, bước đó được đánh dấu là đang hoạt động hoặc đã hoàn thành. Thường là _currentStep >= index_của_bước. state: Trạng thái của bước. Có các giá trị như StepState.indexed (mặc định), StepState.editing (đang chỉnh sửa), StepState.complete (đã hoàn thành), StepState.error (có lỗi), StepState.disabled (bị vô hiệu hóa). Việc này giúp Stepper hiển thị icon tương ứng (số, bút chì, dấu tích, dấu chấm than). controlsBuilder: Đây là một callback cho phép bạn tùy chỉnh hoàn toàn giao diện của các nút "Tiếp tục" và "Quay lại". Trong ví dụ, Thầy Creyt đã biến chúng thành ElevatedButton và OutlinedButton để trông "xịn" hơn và thay đổi text tùy theo bước cuối cùng. Validation (Bước 2): Thầy Creyt đã tích hợp Form và TextFormField với validator và GlobalKey để đảm bảo người dùng nhập đủ thông tin trước khi chuyển sang bước thanh toán. Đây là một "chiêu" cực kỳ quan trọng để dữ liệu không bị "rác" và trải nghiệm người dùng không bị "hụt hẫng". Mẹo (Best Practices) từ Thầy Creyt để "hack" Stepper hiệu quả: Giữ các bước ngắn gọn, súc tích: Đừng biến một bước thành một "cuộc marathon" thông tin. Mỗi bước nên có một mục tiêu rõ ràng, duy nhất. Phản hồi rõ ràng: Luôn dùng isActive và state để người dùng biết họ đang ở đâu, bước nào đã xong, bước nào đang lỗi. "Feedback is king" trong UX. Validation là bạn: Luôn kiểm tra dữ liệu đầu vào ở mỗi bước trước khi cho phép người dùng onStepContinue. Không ai muốn điền xong 5 bước rồi mới biết bước 1 sai chính tả tên mình. Tùy biến controlsBuilder: Các nút mặc định của Stepper hơi "cổ điển". Hãy tận dụng controlsBuilder để "phù phép" cho chúng trông hiện đại và phù hợp với design system của app bạn hơn. Thử nghiệm StepperType.horizontal: Đối với các quy trình ít bước (2-3 bước), horizontal có thể hiệu quả hơn, tiết kiệm không gian và trực quan hơn trên các màn hình rộng. Xử lý "Loading States": Nếu onStepContinue kích hoạt một API call, hãy hiển thị CircularProgressIndicator hoặc disable nút "Tiếp tục" để tránh người dùng nhấn liên tục và tạo ra các request không cần thiết. Ví dụ thực tế các ứng dụng/website đã ứng dụng (hoặc concept tương tự): E-commerce Checkout: Các trang như Tiki, Shopee, Lazada đều có quy trình checkout từng bước: Giỏ hàng -> Địa chỉ -> Thanh toán -> Xác nhận. Đây là ứng dụng kinh điển của Stepper. Onboarding Flows: Khi bạn cài đặt một ứng dụng mới lần đầu, thường có các màn hình hướng dẫn sử dụng từng tính năng chính. Đó chính là Stepper được "phù phép" dưới dạng các trang giới thiệu. Form Đăng Ký/Thiết Lập Hồ Sơ: Các trang mạng xã hội, dịch vụ email khi bạn đăng ký tài khoản mới, thường yêu cầu bạn điền thông tin qua nhiều bước (tên, email, mật khẩu, ảnh đại diện, sở thích...). Wizard Installer: Các phần mềm máy tính khi cài đặt cũng dùng cơ chế "Next > Next > Finish" tương tự. Thử nghiệm của Thầy Creyt và Hướng dẫn nên dùng cho case nào: Thầy Creyt đã từng "đau đầu" với một dự án làm một cái form đăng ký tour du lịch dài dằng dặc, đủ các loại thông tin từ cá nhân, lịch trình, yêu cầu đặc biệt, thanh toán... Ban đầu, cứ nhét hết vào một ScrollView và kết quả là "thảm họa" UX. Người dùng nhìn vào là "bỏ chạy" ngay. Sau đó, Thầy đã quyết định "đập đi xây lại" với Stepper. Chia nhỏ thành: Bước 1: Thông tin cá nhân (Tên, email, SĐT) Bước 2: Lựa chọn tour (Điểm đến, ngày khởi hành) Bước 3: Tùy chọn nâng cao (Xe đưa đón, khách sạn, yêu cầu ăn uống) Bước 4: Thanh toán và xác nhận Kết quả là tỉ lệ hoàn thành form tăng vọt! Khách hàng cảm thấy "nhẹ nhàng" hơn rất nhiều. Việc này chứng minh rằng Stepper không chỉ là một widget, mà là một chiến lược thiết kế UX. Nên dùng Stepper khi nào? Khi bạn có một quy trình có thứ tự rõ ràng, mà bước sau phụ thuộc vào bước trước. Khi một nhiệm vụ có nhiều thông tin cần nhập hoặc nhiều quyết định cần đưa ra. Khi bạn muốn giảm tải nhận thức (cognitive load) cho người dùng, giúp họ tập trung vào từng phần nhỏ của nhiệm vụ. Để tạo ra một trải nghiệm người dùng chuyên nghiệp và có cấu trúc cho các tác vụ quan trọng như mua hàng, đăng ký, thiết lập. Tuyệt đối tránh dùng Stepper cho những tác vụ đơn giản, chỉ cần một vài trường nhập liệu. Đừng "làm màu" quá mức cần thiết, vì đôi khi, sự đơn giản lại là đỉnh cao của thiết kế. Vậy đó, các bạn trẻ! Stepper không phải là một con boss khó nhằn nếu bạn biết cách "farm" nó đúng kỹ thuật. Hãy thực hành, mày mò và biến những quy trình phức tạp thành những trải nghiệm "smooth như kem" cho người dùng nhé! 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é!

44 Đọc tiếp
StackedBarChart Flutter: Kể chuyện dữ liệu đa sắc cùng anh Creyt!
21/03/2026

StackedBarChart Flutter: Kể chuyện dữ liệu đa sắc cùng anh Creyt!

Các em Gen Z thân mến, hôm nay anh Creyt sẽ dẫn các em đi khám phá một "công cụ kể chuyện" dữ liệu siêu xịn sò trong thế giới lập trình Flutter, đó chính là Stacked Bar Chart (Biểu đồ Cột Chồng). Nghe cái tên đã thấy "nghệ" rồi đúng không? Nói nôm na, các em cứ hình dung một Stacked Bar Chart giống như một tháp Lego đa màu sắc vậy. Mỗi cái cột (bar) là một "tổng thể" nào đó, ví dụ như tổng doanh thu một tháng. Còn từng mảnh Lego xếp chồng lên nhau trong cái cột đó chính là "thành phần" cấu tạo nên cái tổng thể đó. Ví dụ, trong tổng doanh thu tháng đó, có bao nhiêu đến từ sản phẩm A, bao nhiêu từ sản phẩm B, bao nhiêu từ dịch vụ C. Tất cả chúng nó xếp chồng lên nhau, tạo nên chiều cao của cái cột tổng thể. Vậy, mục đích của nó là gì? Đơn giản thôi: "Bóc tách" cái tổng thể: Cho chúng ta thấy rõ từng phần đóng góp vào một cái tổng như thế nào. So sánh sự thay đổi: Nhìn qua các cột, ta có thể thấy được sự thay đổi của từng thành phần, và cả sự thay đổi của tổng thể theo thời gian hoặc theo các danh mục khác nhau. Ví dụ, tháng này sản phẩm A đóng góp nhiều hơn tháng trước, nhưng tổng doanh thu lại giảm, vậy là có vấn đề gì đó ở các sản phẩm khác rồi! Kể chuyện trực quan: Thay vì nhìn một đống số khô khan, biểu đồ này giúp chúng ta "đọc" được câu chuyện đằng sau dữ liệu một cách nhanh chóng, dễ hiểu. Code Ví Dụ minh hoạ (Flutter, fl_chart) Để triển khai Stacked Bar Chart trong Flutter, anh em mình sẽ "triệu hồi" thư viện fl_chart – một "phù thủy" vẽ biểu đồ cực kỳ mạnh mẽ và linh hoạt. Đầu tiên, nhớ thêm nó vào pubspec.yaml nhé: dependencies: flutter: sdk: flutter fl_chart: ^0.68.0 # Hoặc phiên bản mới nhất Sau đó, chạy flutter pub get. Bây giờ, cùng xem ví dụ về doanh thu từ 3 loại sản phẩm (A, B, C) qua 3 quý nhé: import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; class StackedBarChartPage extends StatefulWidget { const StackedBarChartPage({Key? key}) : super(key: key); @override State<StackedBarChartPage> createState() => _StackedBarChartPageState(); } class _StackedBarChartPageState extends State<StackedBarChartPage> { // Dữ liệu mẫu: Doanh thu của sản phẩm A, B, C theo quý // Cấu trúc: [Quý 1, Quý 2, Quý 3] // Mỗi quý là một Map: {'productA': value, 'productB': value, 'productC': value} final List<Map<String, double>> _quarterlySales = [ {'productA': 30, 'productB': 20, 'productC': 15}, // Quý 1 {'productA': 25, 'productB': 35, 'productC': 20}, // Quý 2 {'productA': 40, 'productB': 25, 'productC': 10}, // Quý 3 ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Doanh Thu Sản Phẩm Theo Quý'), backgroundColor: Colors.deepPurple, ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: AspectRatio( aspectRatio: 1.5, // Tỷ lệ khung hình của biểu đồ child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: 100, // Tổng doanh thu tối đa có thể đạt được (max sum of A+B+C is 40+35+20 = 95) barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipBgColor: Colors.blueGrey, getTooltipItem: (group, groupIndex, rod, rodIndex) { String product; switch (rodIndex) { case 0: product = 'Sản phẩm A'; break; case 1: product = 'Sản phẩm B'; break; case 2: product = 'Sản phẩm C'; break; default: product = ''; } return BarTooltipItem( '$product: ${rod.toY.toInt()}K\n', const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ); }, ), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, getTitlesWidget: (value, meta) { String text; switch (value.toInt()) { case 0: text = 'Q1'; break; case 1: text = 'Q2'; break; case 2: text = 'Q3'; break; default: text = ''; break; } return SideTitleWidget( axisSide: meta.axisSide, space: 4, child: Text(text, style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 14)), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, getTitlesWidget: (value, meta) { return Text('${value.toInt()}K', style: const TextStyle(color: Colors.black, fontSize: 12)); }, ), ), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData( show: true, border: Border.all(color: const Color(0xff37434d), width: 1), ), barGroups: _quarterlySales.asMap().entries.map((entry) { int index = entry.key; Map<String, double> sales = entry.value; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: sales['productA']! + sales['productB']! + sales['productC']!, // Tổng chiều cao cột width: 16, borderRadius: BorderRadius.zero, // Không bo tròn đầu cột rodStackItems: [ BarChartRodStackItem(0, sales['productA']!, Colors.redAccent), // Sản phẩm A BarChartRodStackItem(sales['productA']!, sales['productA']! + sales['productB']!, Colors.green), // Sản phẩm B BarChartRodStackItem(sales['productA']! + sales['productB']!, sales['productA']! + sales['productB']! + sales['productC']!, Colors.blue), // Sản phẩm C ], ), ], ); }).toList(), ), ), ), ), ), ); } } Mẹo (Best Practices) từ anh Creyt để ghi nhớ và dùng thực tế: Màu sắc là "linh hồn": Các em chọn màu cho từng phần chồng lên nhau phải thật sự khác biệt nhưng vẫn hài hòa. Đừng dùng màu "chói chang" quá, nhìn vào dễ "tụt mood". Tối đa 5-7 màu là đẹp, nhiều quá là thành "bát cháo lòng" đó! Thứ tự là "chìa khóa": Luôn giữ một thứ tự nhất quán cho các thành phần chồng lên nhau trên tất cả các cột. Ví dụ, luôn đặt "Sản phẩm A" ở dưới cùng, rồi đến "Sản phẩm B", "Sản phẩm C". Điều này giúp người xem dễ dàng so sánh và theo dõi sự thay đổi của từng phần. Nhãn và Tooltip "thần thánh": Đừng quên các nhãn trục (axis labels) rõ ràng và đặc biệt là Tooltip (hiển thị thông tin chi tiết khi chạm/di chuột vào cột). Chúng là "người phiên dịch" giúp dữ liệu của em "nói" được câu chuyện của nó. "Tầm nhìn" tổng thể: Đảm bảo maxY (giá trị tối đa của trục Y) đủ lớn để chứa tất cả các cột, tránh tình trạng cột bị "cắt cụt" nhìn rất "thiếu chuyên nghiệp". Đừng "tham lam" dữ liệu: Anh đã từng thấy nhiều em tân binh cố nhồi nhét 10-15 loại sản phẩm vào một cái stacked bar chart... kết quả là như bát cháo lòng, đẹp thì không mà nhìn vào thì loạn cào cào! Nếu dữ liệu quá nhiều, hãy cân nhắc nhóm lại hoặc dùng biểu đồ khác. Ứng dụng thực tế: Ai đang "xài" Stacked Bar Chart? Không ít đâu nhé! Các em sẽ thấy "ông bạn" này xuất hiện khắp mọi nơi: Dashboard tài chính cá nhân/doanh nghiệp: Như ứng dụng Money Lover hay các hệ thống ERP, BI. Họ dùng để phân tích chi tiêu theo từng hạng mục (ăn uống, đi lại, giải trí) hoặc doanh thu theo từng dòng sản phẩm, từng khu vực. Google Analytics / Webmaster Tools: Để xem lưu lượng truy cập website đến từ những nguồn nào (trực tiếp, tìm kiếm tự nhiên, mạng xã hội, quảng cáo) theo thời gian. Các ứng dụng quản lý dự án: JIRA, Trello (dạng tùy biến) có thể dùng để hiển thị tiến độ công việc theo từng giai đoạn, từng thành viên hoặc từng loại công việc. Ứng dụng sức khỏe/dinh dưỡng: MyFitnessPal dùng để bóc tách lượng calo từ protein, carb, fat trong bữa ăn hàng ngày. Thử nghiệm của anh Creyt và lời khuyên nên dùng cho case nào: Anh Creyt đã "chinh chiến" với đủ loại biểu đồ rồi. Và kinh nghiệm xương máu cho thấy: NÊN DÙNG Stacked Bar Chart khi: Em muốn "mổ xẻ" một tổng thể thành các phần cấu thành và xem sự đóng góp của từng phần. Em muốn so sánh sự thay đổi của cấu trúc bên trong các tổng thể (ví dụ: tỷ lệ đóng góp của từng sản phẩm thay đổi thế nào qua các quý). Tổng thể (chiều cao của cột) cũng quan trọng như các phần bên trong nó. Số lượng các thành phần để "stack" không quá nhiều (lý tưởng là 3-5, tối đa 7). KHÔNG NÊN DÙNG Stacked Bar Chart khi: Em chỉ muốn so sánh trực tiếp các giá trị độc lập của từng danh mục (ví dụ: chỉ muốn so sánh doanh thu sản phẩm A với sản phẩm B, không quan tâm tổng thể). Lúc này, Bar Chart thông thường hoặc Grouped Bar Chart sẽ hiệu quả hơn. Dữ liệu của em có nhiều giá trị âm. Stacked Bar Chart rất khó để biểu diễn giá trị âm một cách trực quan. Có quá nhiều thành phần để chồng lên nhau. Như anh nói đấy, thành "bát cháo lòng" ngay! Tóm lại, Stacked Bar Chart là một công cụ mạnh mẽ để kể chuyện về sự phân bổ và thay đổi của dữ liệu. Hãy dùng nó một cách thông minh, các em sẽ biến những con số khô khan thành những câu chuyện đầy màu sắc và ý nghĩa! Chúc các em "code" vui vẻ và thành công! 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é!

50 Đọc tiếp
SliverToBoxAdapter: Biến 'Hộp' thành 'Sliver' trong Flutter!
21/03/2026

SliverToBoxAdapter: Biến 'Hộp' thành 'Sliver' trong Flutter!

Chào các GenZ năng động của anh Creyt! Hôm nay, chúng ta sẽ cùng “mổ xẻ” một khái niệm nghe thì có vẻ “học thuật” nhưng lại cực kỳ thực tế và mạnh mẽ trong Flutter: SliverToBoxAdapterElement. Tuy nhiên, như thường lệ, anh Creyt sẽ biến nó thành một câu chuyện dễ hiểu, dí dỏm để các em “nuốt” trọn không sót chữ nào. 1. SliverToBoxAdapterElement là gì và để làm gì? (aka. Chiếc cầu nối diệu kỳ) Đầu tiên, hãy quên cái đuôi Element đi đã nhé. Trong Flutter, Element là một khái niệm nội bộ, giống như mấy cái mạch điện li ti bên trong chiếc smartphone của các em vậy. Các em dùng smartphone thì sướng, chứ ít khi cần biết mạch điện nó chạy ra sao. Chúng ta sẽ tập trung vào “người hùng” chính: SliverToBoxAdapter. Tưởng tượng thế này: Các em đang xây dựng một con đường cao tốc siêu hiện đại (đó chính là CustomScrollView trong Flutter). Con đường này được thiết kế đặc biệt để các phương tiện siêu tốc, siêu tiết kiệm năng lượng (mà anh Creyt gọi là Slivers) lướt đi một cách mượt mà, tối ưu nhất có thể. Các Sliver này không chỉ cuộn mà còn có thể thay đổi kích thước, biến hình theo kiểu “Transformers” khi cuộn – ví dụ như SliverAppBar (cái thanh app bar co giãn trên đầu), hay SliverList, SliverGrid (danh sách và lưới các item được tối ưu hóa). Nhưng bỗng nhiên, các em lại muốn đặt một căn nhà nhỏ (một widget thông thường, ví dụ như Container, Text, Image, Column, Row – những thứ mà Flutter gọi chung là Box widgets) ngay giữa con đường cao tốc đó. Rõ ràng, căn nhà không phải là một “phương tiện siêu tốc” và không thể tự chạy hay biến hình theo kiểu Sliver được. Nó là một “hộp” tĩnh, cứng nhắc. Thế thì làm sao? Đập đường xây lại à? Không! Đây chính là lúc SliverToBoxAdapter xuất hiện như một “chiếc xe tải chuyên dụng có sàn phẳng”. Nhiệm vụ của nó là gì? Đơn giản là đặt cái “căn nhà” (Box widget) của các em lên chiếc xe tải đó. Chiếc xe tải này (SliverToBoxAdapter) được thiết kế để di chuyển trên đường cao tốc CustomScrollView và mang theo “căn nhà” của các em đi cùng với các Slivers khác một cách êm ru. Nó biến cái “không phải sliver” thành “có thể là sliver” để hòa nhập vào hệ sinh thái cuộn tối ưu của CustomScrollView. Tóm lại: SliverToBoxAdapter dùng để “đóng gói” một widget thông thường (Box widget) thành một Sliver, giúp nó có thể được hiển thị bên trong một CustomScrollView cùng với các Sliver khác. Nó đảm bảo mọi thứ cuộn mượt mà mà không gặp lỗi. 2. Code Ví Dụ Minh Hoạ Rõ Ràng Để các em dễ hình dung, anh Creyt sẽ cho một ví dụ kinh điển: Một trang profile có banner co giãn, sau đó là thông tin người dùng (một Container cố định), rồi mới đến danh sách bài viế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: 'Creyt\'s Sliver Demo', theme: ThemeData( primarySwatch: Colors.blueGrey, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const ProfilePage(), ); } } class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ // 1. SliverAppBar: Cái banner co giãn trên đầu (một Sliver xịn xò) SliverAppBar( expandedHeight: 200.0, floating: false, pinned: true, flexibleSpace: FlexibleSpaceBar( title: const Text('Creyt\'s Profile', style: TextStyle(color: Colors.white)), background: Image.network( 'https://picsum.photos/seed/creyt/800/400', fit: BoxFit.cover, ), ), ), // 2. SliverToBoxAdapter: Đóng gói một Box widget (Container) vào làm Sliver // Đây chính là 'chiếc xe tải' chở 'căn nhà' thông tin người dùng. SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(16.0), child: Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), spreadRadius: 2, blurRadius: 5, offset: const Offset(0, 3), ), ], ), child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tên: Creyt - Giảng viên lập trình lão luyện', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text( 'Slogan: Code is poetry, bugs are plot twists.', style: TextStyle(fontSize: 16, fontStyle: FontStyle.italic), ), SizedBox(height: 8), Text( 'Nghề: Biến khái niệm phức tạp thành chuyện tiếu lâm.', style: TextStyle(fontSize: 16), ), ], ), ), ), ), // 3. SliverList: Danh sách các bài viết (một Sliver khác) SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), elevation: 2, child: ListTile( leading: CircleAvatar(child: Text('${index + 1}')), title: Text('Bài viết số ${index + 1}'), subtitle: Text('Đây là nội dung tóm tắt của bài viết số ${index + 1}.'), onTap: () { // Handle tap }, ), ); }, childCount: 20, // 20 bài viết ), ), ], ), ); } } Trong ví dụ trên: SliverAppBar là một Sliver “chính hãng”, tự biết cách co giãn. SliverToBoxAdapter là “chiếc xe tải” chở theo Container chứa thông tin profile. Cái Container đó là một Box widget thông thường, không biết co giãn theo kiểu Sliver. SliverList lại là một Sliver “chính hãng” khác, dùng để hiển thị danh sách các bài viết một cách hiệu quả. Thấy chưa? Tất cả đều cuộn mượt mà trong CustomScrollView nhờ có SliverToBoxAdapter làm cầu nối. 3. Mẹo (Best Practices) từ anh Creyt để dùng SliverToBoxAdapter “Dùng đúng lúc, đúng chỗ, như gia vị”: SliverToBoxAdapter là cứu cánh khi em cần chèn MỘT HOẶC VÀI widget cố định, không phải dạng danh sách dài vô tận, vào CustomScrollView. Đừng lạm dụng nó để bọc từng item trong một danh sách lớn, vì như vậy sẽ mất đi hiệu quả tối ưu của SliverList/SliverGrid (chỉ render những cái đang nhìn thấy). “Hiểu rõ bản chất”: Hãy nhớ, nó biến Box thành Sliver, nhưng nó không biến Box thành một Sliver “thông minh” có khả năng tối ưu hóa cuộn như SliverList.builder. Nó vẫn sẽ render toàn bộ nội dung của Box widget bên trong nó, dù Box đó có đang hiển thị trên màn hình hay không. Nên nếu Box widget đó quá lớn và phức tạp, hiệu năng có thể bị ảnh hưởng nhẹ. “Đừng nhầm lẫn với SliverFillRemaining hay SliverFillViewport”: Mấy ông kia là để lấp đầy không gian còn trống, hoặc đảm bảo mỗi item chiếm hết viewport. SliverToBoxAdapter chỉ đơn giản là “đóng gói” một Box widget vào. 4. Ứng dụng thực tế: Ai đã dùng SliverToBoxAdapter? Thực ra, các em dùng mấy app sau mỗi ngày đều có thể thấy bóng dáng của nó: Facebook/Instagram Profile: Trang cá nhân của các em thường có ảnh đại diện, cover photo (SliverAppBar), sau đó là một khối thông tin cá nhân (có thể là một SliverToBoxAdapter chứa Column hoặc Container), rồi mới đến danh sách bài đăng (SliverList). Shopee/Lazada Product Page: Trang chi tiết sản phẩm thường có carousel ảnh sản phẩm (có thể là Sliver), sau đó là khối thông tin giá, tên sản phẩm, mô tả ngắn gọn (một SliverToBoxAdapter chứa các Text, Row, Column), rồi đến phần đánh giá, sản phẩm liên quan (SliverList). Các ứng dụng Tin tức/Blog: Một bài viết có tiêu đề lớn (SliverAppBar), sau đó là thông tin tác giả, ngày đăng (một SliverToBoxAdapter), rồi mới đến nội dung bài viết dài (SliverList hoặc một SingleChildScrollView trong SliverToBoxAdapter). Nói chung, bất cứ khi nào em thấy một trang cuộn phức tạp mà có sự kết hợp giữa các phần tử “biến hình” (Slivers) và các phần tử “cố định” (Box widgets) thì khả năng cao là có SliverToBoxAdapter đang làm nhiệm vụ “điều phối giao thông” đấy! 5. Thử nghiệm và khi nào nên dùng SliverToBoxAdapter Anh Creyt đã từng “nghịch” khá nhiều với Slivers. Hồi mới làm quen, anh cũng thử nhét thẳng một Container vào CustomScrollView và… báo lỗi ngay! Đó là lúc anh nhận ra “sức mạnh” của SliverToBoxAdapter. Nên dùng khi: Kết hợp các loại widget: Khi em cần đặt một widget thông thường (như Container, Text, Image, Column, Row, Card...) vào một CustomScrollView cùng với các Sliver khác (SliverAppBar, SliverList, SliverGrid). Đây là trường hợp phổ biến nhất. Tạo khoảng trống/đệm: Muốn thêm một khoảng trống cố định (SizedBox) hoặc padding (Padding) vào giữa các Slivers mà không muốn dùng SliverPadding (vì SliverPadding sẽ áp dụng cho cả Sliver bên trong nó). Thêm các phần tử không cuộn được: Ví dụ, một nút bấm “Load More” ở cuối danh sách, hoặc một banner quảng cáo tĩnh. Không nên dùng khi: Danh sách dài các item giống nhau: Nếu em có một danh sách 1000 item giống nhau, mỗi item là một Card, thì đừng dùng SliverToBoxAdapter bọc từng Card rồi cho vào SliverList đâu nhé. Hãy dùng SliverList.builder hoặc SliverGrid.builder trực tiếp, chúng sinh ra là để làm việc đó, hiệu quả hơn gấp vạn lần. Toàn bộ nội dung là Box: Nếu toàn bộ màn hình của em chỉ là một đống Box widgets và chỉ cần cuộn đơn giản, hãy dùng SingleChildScrollView với Column thay vì CustomScrollView với SliverToBoxAdapter cho toàn bộ. Đừng biến mọi thứ thành phức tạp không cần thiết. Nhớ nhé, Flutter cho các em rất nhiều công cụ. Việc của một developer lão luyện là chọn đúng công cụ cho đúng việc. SliverToBoxAdapter là một công cụ mạnh, nhưng phải dùng đúng lúc, đúng chỗ thì mới phát huy hết sức mạnh của nó. Giống như việc em không dùng búa tạ để đóng đinh nhỏ vậy! Chúc các em code vui vẻ và làm ra những app Flutter mượt mà, đỉ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é!

39 Đọc tiếp
SliverSafeArea: Vệ Sĩ Tối Thượng Cho Nội Dung Cuộn Của GenZ
21/03/2026

SliverSafeArea: Vệ Sĩ Tối Thượng Cho Nội Dung Cuộn Của GenZ

SliverSafeArea: Khi Nội Dung Cuộn Của Bạn Cần Một Vệ Sĩ Tinh Tế Chào các "đệ tử" mê code, đặc biệt là các bạn GenZ luôn muốn UI app của mình phải "mượt như lụa", không một hạt sạn. Hôm nay, chúng ta sẽ "bóc tách" một khái niệm mà tưởng chừng nhỏ nhặt nhưng lại là "người hùng thầm lặng" trong thế giới Flutter: SliverSafeArea. Nghe cái tên đã thấy hơi "lắt léo" rồi đúng không? Đừng lo, anh Creyt sẽ giải thích cặn kẽ, dùng phép ẩn dụ cho các em dễ hiểu. 1. SliverSafeArea là gì và để làm gì? (Giải mã "vệ sĩ" cho nội dung cuộn) Các em cứ hình dung thế này: Điện thoại bây giờ đủ loại hình dáng, từ "tai thỏ", "giọt nước", "nốt ruồi" đến mấy cái thanh điều hướng ảo dưới đáy màn hình. Mấy cái đó gọi chung là "system UI overlays" – các phần tử giao diện hệ thống. Nếu nội dung app của chúng ta cứ "vô tư" mà hiển thị, thì rất dễ bị mấy cái "chướng ngại vật" này che khuất, trông rất "phèn" và khó chịu. SafeArea thông thường là một widget rất tốt, nó sẽ tự động thêm padding vào các cạnh của widget con để tránh bị mấy cái "system UI" đó che. Nó như một cái "áo giáp" bảo vệ toàn bộ màn hình của em. Nhưng vấn đề nảy sinh khi chúng ta dùng các widget "phân mảnh" (hay còn gọi là "Sliver"). Sliver là "linh hồn" của các hiệu ứng cuộn phức tạp trong Flutter, ví dụ như khi em cuộn một danh sách mà tiêu đề nó cứ "dính" ở trên, hay một lưới ảnh mà các item cứ trượt mượt mà. CustomScrollView hay NestedScrollView là những "ông trùm" sử dụng Sliver. Thế thì SliverSafeArea chính là phiên bản "cao cấp" của SafeArea, được thiết kế riêng cho các widget Sliver. Nó không chỉ bảo vệ nội dung khỏi bị che, mà còn làm điều đó một cách "thông minh" và "linh hoạt" trong ngữ cảnh cuộn. Tưởng tượng em có một danh sách cuộn dài, nếu chỉ dùng SafeArea thông thường, nó sẽ thêm padding cho cả màn hình, nhưng khi cuộn, có thể nội dung ở phía dưới vẫn bị thanh điều hướng che. SliverSafeArea sẽ đảm bảo rằng, dù em cuộn đến đâu, nội dung trong Sliver đó vẫn nằm trong vùng an toàn, không bị "tai nạn" với các system UI. Nó như một "người điều phối không gian" tài ba, luôn giữ cho nội dung của em "thoáng đãng" và "dễ nhìn" trên mọi thiết bị. Nói tóm lại: SafeArea: Bảo vệ toàn bộ màn hình hoặc một phần lớn của UI khỏi system UI overlays. SliverSafeArea: Bảo vệ nội dung bên trong các widget Sliver khỏi system UI overlays, đặc biệt quan trọng khi cuộn, đảm bảo trải nghiệm liền mạch. 2. Code Ví Dụ Minh Họa (Thực hành ngay cho nóng!) Giờ thì, lý thuyết suông mãi chán lắm. Chúng ta cùng xem một ví dụ "ngon lành cành đào" để thấy SliverSafeArea hoạt động như thế nào nhé. Anh sẽ tạo một CustomScrollView với một SliverList và SliverGrid bên trong. 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: 'SliverSafeArea Demo by Creyt', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const SliverSafeAreaScreen(), ); } } class SliverSafeAreaScreen extends StatelessWidget { const SliverSafeAreaScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ // SliverAppBar để có thanh tiêu đề cuộn được SliverAppBar( title: const Text('SliverSafeArea Demo'), floating: true, // App bar sẽ xuất hiện lại khi cuộn lên pinned: true, // App bar sẽ ghim ở trên cùng expandedHeight: 200.0, flexibleSpace: FlexibleSpaceBar( background: Image.network( 'https://picsum.photos/800/200', // Ảnh nền đẹp đẽ fit: BoxFit.cover, ), ), ), // Vấn đề: Nếu không có SliverSafeArea, các item đầu tiên có thể bị che bởi status bar // Và các item cuối cùng có thể bị che bởi navigation bar // Đây là lúc SliverSafeArea ra tay! // Dùng SliverSafeArea để bảo vệ SliverList SliverSafeArea( // `top: false` vì chúng ta đã có SliverAppBar xử lý phần trên rồi. // Nếu không có AppBar, bạn có thể để `top: true` hoặc bỏ qua. top: false, sliver: SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, color: index.isEven ? Colors.lightBlue[100] : Colors.blue[100], height: 100.0, child: Text( 'List Item $index', style: const TextStyle(fontSize: 20), ), ); }, childCount: 20, // 20 item trong danh sách ), ), ), // Thêm một khoảng trống để dễ hình dung hơn const SliverToBoxAdapter( child: SizedBox(height: 20), ), // Dùng SliverSafeArea cho SliverGrid SliverSafeArea( // `bottom: false` nếu bạn có một PersistentFooterButtons hoặc không muốn padding ở dưới // Nhưng trong trường hợp này, cứ để mặc định để thấy hiệu quả ở đáy màn hình sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, // 2 cột crossAxisSpacing: 8.0, mainAxisSpacing: 8.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, color: index.isEven ? Colors.green[100] : Colors.lightGreen[100], child: Text( 'Grid Item $index', style: const TextStyle(fontSize: 18), ), ); }, childCount: 10, // 10 item trong lưới ), ), ), // Thêm một SliverToBoxAdapter ở cuối để đảm bảo thấy rõ padding ở bottom khi cuộn hết const SliverToBoxAdapter( child: SizedBox(height: 50), ), ], ), ); } } Giải thích code: Trong ví dụ trên, anh dùng CustomScrollView để chứa SliverAppBar (thanh tiêu đề cuộn) và hai Sliver chính: SliverList và SliverGrid. SliverAppBar đã tự động xử lý phần top của màn hình (tránh status bar) khi nó được ghim. SliverSafeArea được bọc quanh SliverList và SliverGrid. Với SliverList, anh đặt top: false vì SliverAppBar đã lo phần trên rồi. Nếu không có SliverAppBar mà SliverList nằm ngay đầu CustomScrollView, bạn nên để top: true (hoặc mặc định) để nó tự động đẩy nội dung xuống dưới status bar/notch. Với SliverGrid, anh để mặc định, nó sẽ tự động tính toán padding cần thiết cho cả trên và dưới (nếu cần), đảm bảo các item không bị che bởi thanh điều hướng ở đáy màn hình khi cuộn đến cuối. Khi chạy code này trên một thiết bị có notch hoặc thanh điều hướng ảo, em sẽ thấy nội dung của danh sách và lưới sẽ "tự động né tránh" các khu vực đó một cách mượt mà. 3. Mẹo Vặt (Best Practices) từ "Lão Làng" Creyt Để dùng SliverSafeArea một cách "thông thái" và hiệu quả, nhớ vài mẹo nhỏ này nhé: Chỉ dùng khi cần: Đừng lạm dụng SliverSafeArea nếu SafeArea thông thường đã làm tốt công việc của nó cho toàn bộ màn hình. SliverSafeArea sinh ra để giải quyết vấn đề trong ngữ cảnh Sliver (trong CustomScrollView, NestedScrollView...). Hiểu rõ các cạnh: SliverSafeArea có các thuộc tính left, top, right, bottom để em kiểm soát việc thêm padding cho từng cạnh. Ví dụ, nếu em có SliverAppBar ở trên cùng, thì SliverSafeArea cho Sliver bên dưới có thể đặt top: false để tránh padding kép. "Thừa còn hơn thiếu" nhưng "thừa quá" thì lại gây ra khoảng trống không cần thiết, làm UI trông "dở hơi". minimum padding: Đôi khi em chỉ muốn thêm một chút padding rất nhỏ, không phải toàn bộ kích thước của system UI. Thuộc tính minimum cho phép em xác định kích thước padding tối thiểu mà SliverSafeArea sẽ áp dụng. Rất hữu ích cho các trường hợp "tinh chỉnh" UI. Luôn test trên nhiều thiết bị: "Học đi đôi với hành", "code đi đôi với test". Hãy chạy app của em trên các thiết bị có notch, tai thỏ, hoặc thanh điều hướng khác nhau để đảm bảo SliverSafeArea hoạt động đúng như mong đợi. 4. Ứng Dụng Thực Tế (Ai đã dùng rồi?) Em có thể thấy SliverSafeArea (hoặc các cơ chế tương tự) ở khắp mọi nơi trong các ứng dụng di động hiện đại, đặc biệt là những app có giao diện cuộn phức tạp: Các ứng dụng mạng xã hội (TikTok, Instagram, Facebook): Khi em cuộn feed, các nội dung video, hình ảnh luôn được hiển thị trọn vẹn, không bị che bởi status bar hay thanh điều hướng, ngay cả khi em kéo đến tận cùng danh sách. Ứng dụng đọc báo/tin tức: Các bài viết dài thường được trình bày trong CustomScrollView để có các hiệu ứng cuộn mượt mà. SliverSafeArea đảm bảo văn bản không bị che khi đọc. Ứng dụng thương mại điện tử (Shopee, Lazada): Trang chi tiết sản phẩm thường có rất nhiều thành phần cuộn (ảnh, mô tả, đánh giá...). SliverSafeArea giúp các thông tin quan trọng luôn hiển thị rõ ràng. Bất kỳ app nào sử dụng CustomScrollView hay NestedScrollView để tạo ra các hiệu ứng cuộn độc đáo mà vẫn muốn đảm bảo nội dung không bị che khuất. 5. Thử Nghiệm và Nên Dùng Cho Case Nào? Thử nghiệm: Để thấy rõ sự khác biệt, em hãy thử chạy đoạn code ví dụ trên: Không có SliverSafeArea: Bỏ SliverSafeArea ra khỏi SliverList và SliverGrid. Chạy trên giả lập hoặc thiết bị thật có notch/thanh điều hướng. Cuộn lên xuống và quan sát xem các item đầu/cuối có bị che không. Có SliverSafeArea: Bọc lại như code mẫu. So sánh sự khác biệt. Em sẽ thấy một "khoảng trống" an toàn được thêm vào, đẩy nội dung ra xa khỏi vùng nguy hiểm. Nên dùng cho case nào? SliverSafeArea là "vũ khí" đắc lực của em khi: Làm việc với CustomScrollView hoặc NestedScrollView: Đây là môi trường sống chính của các Sliver. Nội dung của Sliver có nguy cơ bị che: Đặc biệt là các SliverList, SliverGrid, SliverFixedExtentList mà nội dung của chúng có thể đụng vào status bar, notch, dynamic island, hoặc thanh điều hướng ảo. Cần kiểm soát chi tiết padding cho từng Sliver: Thay vì áp dụng SafeArea cho toàn bộ Scaffold.body (có thể gây ra padding thừa thãi cho các widget không cần), SliverSafeArea cho phép em bảo vệ từng Sliver một cách có chọn lọc. Tối ưu trải nghiệm người dùng trên đa dạng thiết bị: Đảm bảo app của em trông "pro" và "mượt mà" trên mọi loại điện thoại, từ "tai thỏ" đến "màn hình đục lỗ". Nhớ nhé các em, trong lập trình, đôi khi những chi tiết nhỏ như SliverSafeArea lại tạo nên sự khác biệt lớn giữa một ứng dụng "tàm tạm" và một ứng dụng "đỉnh của chóp". Hãy luôn chú ý đến trải nghiệm người dùng, và SliverSafeArea chính là một công cụ tuyệt vời để làm điều đó! Hẹn gặp lại trong bài học tiếp theo! 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
SliverPrototypeExtentList: Bí kíp Vô Hình để List Flutter Mượt Như Bơ!
21/03/2026

SliverPrototypeExtentList: Bí kíp Vô Hình để List Flutter Mượt Như Bơ!

Ê mấy đứa, hôm nay anh Creyt lại có món ngon cho tụi bây đây! Trong thế giới Flutter đầy rẫy những widget, đôi khi chúng ta gặp phải mấy cái tên nghe 'hàn lâm' muốn xỉu, nhưng thực ra lại là mấy 'vũ khí bí mật' giúp app mình mượt mà như bơ, không giật không lag. Và hôm nay, 'vũ khí' mà anh muốn giới thiệu chính là SliverPrototypeExtentList. Nghe cái tên đã thấy 'pro' rồi đúng không? Nhưng đừng lo, anh sẽ bóc tách nó ra từng miếng nhỏ cho tụi bây dễ nuốt. Tưởng tượng thế này: tụi bây đang lướt TikTok, Instagram, hay YouTube Shorts, cái feed nó cứ cuộn, cuộn mãi mà không thấy giật tí nào. Đó không phải tự nhiên đâu, đằng sau đó là cả một 'binh đoàn' các kỹ thuật tối ưu, và SliverPrototypeExtentList là một trong những 'chiến binh' thầm lặng đó. 1. SliverPrototypeExtentList Là Gì Mà Nghe Có Vẻ Ghê Gớm Vậy Anh? Trước hết, mình phải hiểu 'Sliver' là gì đã. Tưởng tượng màn hình điện thoại của tụi bây là một tờ giấy dài, và cái tờ giấy đó có thể cuộn lên cuộn xuống. Một 'Sliver' chính là một 'mảnh ghép' nhỏ, một 'lát cắt' của cái tờ giấy đó. Nó không phải là cả một tờ giấy to đùng (như ListView bình thường), mà là một phần nhỏ, linh hoạt, có thể tự cuộn hoặc kết hợp với các 'Sliver' khác để tạo thành một khu vực cuộn lớn hơn. Điều này giúp Flutter chỉ cần vẽ những gì đang hiển thị trên màn hình, tiết kiệm tài nguyên cực kỳ. Còn 'PrototypeExtentList' thì sao? Đây mới là phần hay ho này. Khi tụi bây có một danh sách cực dài (ví dụ: hàng ngàn bài post, hàng ngàn comment), mà tất cả các bài post đó nhìn chung có cùng một chiều cao (hoặc chiều rộng, nếu cuộn ngang) thì sao? Bình thường, Flutter sẽ phải 'đo' từng item một khi nó chuẩn bị xuất hiện trên màn hình để biết nó cao bao nhiêu, từ đó mới tính toán được tổng chiều dài của cái list và vị trí cuộn. Cái việc 'đo đạc' này, tuy nhỏ, nhưng nếu làm với hàng trăm, hàng ngàn item, nó sẽ gây ra cái mà dân dev gọi là 'jank' – tức là cảm giác giật giật, khựng khựng khi cuộn. SliverPrototypeExtentList ra đời để giải quyết bài toán này. Nó hoạt động như một 'thằng nhóc tiền trạm' thông minh. Thay vì đo tất cả, nó chỉ cần tụi bây cung cấp MỘT item MẪU (prototype item) – coi như là 'đại diện' cho tất cả các item khác. Nó sẽ bí mật render (vẽ) cái item mẫu này ngoài màn hình để 'đo đạc' chiều cao của nó. Sau khi có được chiều cao của 'thằng tiền trạm' này, nó sẽ 'tự động hiểu' rằng TẤT CẢ các item còn lại trong danh sách cũng sẽ có chiều cao tương tự. Vậy là từ giờ, Flutter không cần phải đo đạc từng cái item nữa! Nó chỉ cần biết chiều cao của 'thằng mẫu' là xong, rồi cứ thế mà nhân lên với số lượng item để tính toán vị trí cuộn và tổng chiều dài list. Kết quả: App của tụi bây cuộn mượt mà như bơ, không một chút giật lag nào, dù danh sách có dài đến mấy đi chăng nữa. Ngon lành cành đào chưa? 2. Code Ví Dụ Minh Hoạ Rõ Ràng, Chuẩn Kiến Thức Để tụi bây dễ hình dung, anh Creyt sẽ phác thảo một ví dụ đơn giản nhé. Tưởng tượng tụi bây có một danh sách các 'bài đăng' (post) trên mạng xã hội, và các bài đăng này có cấu trúc khá giống nhau, nên chiều cao của chúng cũng xêm xêm nhau. 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: 'SliverPrototypeExtentList Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); // Giả lập một danh sách các bài đăng rất dài final List<String> _posts = const [ 'Flutter là số 1!', 'Học SliverPrototypeExtentList để app mượt như lụa.', 'Creyt giảng bài dễ hiểu bá cháy con bọ chét!', 'Làm dev Gen Z phải biết tối ưu hiệu năng chứ!', 'Cuộn cuộn, lướt lướt, không giật lag là auto yêu.', 'Widget này hay ho phết, dùng cho list dài là chuẩn.', 'Đừng quên like và subscribe kênh anh Creyt nhé!', 'Hôm nay có ai học được gì mới không?', 'Mỗi dòng là một post, chiều cao tương đối.', 'Thử kéo xuống cuối xem có mượt không nhé!', 'Flutter community is awesome!', 'Dart is a beautiful language.', 'Build amazing UIs with Flutter.', 'Performance matters in mobile apps.', 'Slivers provide great flexibility.', 'Prototype item is the key here.', 'Understand the why behind the how.', 'Keep learning, keep building.', 'Gen Z devs are the future!', 'This list goes on and on...', ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SliverPrototypeExtentList Demo'), ), body: CustomScrollView( slivers: <Widget>[ // Đây là prototype item. Nó sẽ được render ngoài màn hình // để tính toán chiều cao. Quan trọng là nó phải đại diện // cho kích thước của các item thật. SliverPrototypeExtentList( // Key cho prototype item, giúp Flutter nhận diện. // Có thể bỏ qua nếu không cần thiết, nhưng dùng thì tốt hơn. prototypeItem: _buildPostItem('Đây là một bài đăng mẫu để đo chiều cao.'), // Delegate cung cấp các item thật sự cho danh sách. delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { // Các item thật sự trong danh sách // Chúng ta giả định tất cả đều có cấu trúc và chiều cao tương tự // như prototypeItem. return _buildPostItem(_posts[index % _posts.length]); }, childCount: 1000, // Một danh sách rất dài để thấy hiệu quả ), ), ], ), ); } // Hàm tạo một widget bài đăng đơn giản Widget _buildPostItem(String text) { return Container( margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10.0), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), spreadRadius: 1, blurRadius: 5, offset: const Offset(0, 3), // changes position of shadow ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Người dùng Gen Z', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 4), Text( text, style: const TextStyle(fontSize: 14), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ Icon(Icons.thumb_up_alt_outlined, size: 18, color: Colors.grey), Icon(Icons.comment_outlined, size: 18, color: Colors.grey), Icon(Icons.share_outlined, size: 18, color: Colors.grey), ], ), ], ), ); } } Trong ví dụ trên, cái _buildPostItem chính là cái 'khuôn' để tạo ra các bài đăng. Anh dùng nó để tạo cả prototypeItem lẫn các item thật trong SliverChildBuilderDelegate. Điều này đảm bảo rằng 'thằng tiền trạm' và 'binh đoàn' của nó có cùng kích thước, và đó là chìa khóa để SliverPrototypeExtentList hoạt động hiệu quả. 3. Một Vài Mẹo (Best Practices) Từ Anh Creyt Để Nhớ Và Dùng Thực Tế Để không biến 'vũ khí bí mật' thành 'vũ khí tự hủy', tụi bây nhớ mấy mẹo này nhé: Chỉ dùng khi 'Đồng phục': SliverPrototypeExtentList chỉ phát huy tối đa sức mạnh khi TẤT CẢ các item trong danh sách của tụi bây có chiều cao (hoặc chiều rộng) gần như nhau. Nếu item cao thấp khác nhau quá nhiều, nó sẽ tính toán sai, và kết quả là… vẫn giật lag như thường, hoặc thậm chí còn tệ hơn vì nó 'đo nhầm'. Lúc đó thà dùng SliverList bình thường còn hơn. 'Thằng tiền trạm' phải chuẩn: Cái prototypeItem mà tụi bây cung cấp phải là một bản sao chính xác (về cấu trúc và kích thước) của các item thật. Đừng có đưa một cái prototype đơn giản quá, trong khi item thật lại phức tạp hơn nhiều. Nó sẽ đo sai bét nhè. Tránh 'kích thước động' trong Prototype: Trong prototypeItem, hạn chế tối đa các widget có thể thay đổi kích thước của nó một cách linh hoạt (ví dụ: Expanded, Flexible mà không có flex cụ thể, hoặc text mà không có giới hạn maxLines rõ ràng nếu nó có thể co giãn). Mục tiêu là để nó có một kích thước cố định, dễ đo đạc. Hiểu về 'Jank': Hãy nhớ, mục đích chính là giảm 'jank' – tức là những khoảnh khắc mà UI bị khựng lại do CPU và GPU phải làm việc quá sức để tính toán và vẽ. SliverPrototypeExtentList giúp giảm tải cho chúng bằng cách 'đo trước' một lần duy nhất. Không phải lúc nào cũng cần: Nếu list của tụi bây ngắn, hoặc các item có kích thước đã được xác định rõ ràng từ đầu (ví dụ: tất cả đều cao 50px), thì dùng SliverFixedExtentList hoặc thậm chí ListView bình thường với itemExtent còn dễ hơn và hiệu quả tương tự. 4. Ứng Dụng Thực Tế: Ai Đã Dùng 'Vũ Khí' Này? Mấy cái app mà tụi bây hay dùng hàng ngày, khả năng cao là họ đã dùng những kỹ thuật tương tự (hoặc chính nó) để tối ưu hiệu năng đó: Feed mạng xã hội (Facebook, Instagram, TikTok): Mặc dù các bài đăng có thể khác nhau về nội dung (ảnh, video, chữ), nhưng thường thì các 'khung' bài đăng (chứa avatar, tên, nút like/comment) có kích thước tương đối đồng nhất. Nếu họ có một loại bài đăng đặc trưng với chiều cao cố định, SliverPrototypeExtentList là một ứng cử viên sáng giá. Danh sách sản phẩm (Shopee, Lazada, Tiki): Khi tụi bây lướt qua hàng ngàn sản phẩm, mỗi sản phẩm hiển thị trong một 'card' có kích thước giống nhau. Việc đo đạc từng card sẽ là thảm họa hiệu năng. Danh sách chat (Zalo, Messenger): Nếu các bong bóng chat có chiều cao khá đồng đều (ví dụ: tin nhắn ngắn, không có ảnh/video quá lớn), thì việc dùng kỹ thuật này sẽ giúp cuộn danh sách tin nhắn mượt mà hơn rất nhiều. 5. Thử Nghiệm Và Hướng Dẫn Nên Dùng Cho Case Nào Anh Creyt đã từng thử nghiệm nhiều cách tối ưu list trong Flutter, và đây là kinh nghiệm xương máu: Nên dùng khi: Tụi bây có một danh sách RẤT DÀI (hàng trăm, hàng ngàn item). Tất cả các item trong danh sách có cấu trúc và kích thước (chiều cao/rộng) gần như giống hệt nhau. Tụi bây không biết chính xác kích thước của item từ trước, mà phải để Flutter tự tính toán (ví dụ: chiều cao của một bài đăng phụ thuộc vào độ dài của đoạn text). Tụi bây đang gặp vấn đề 'jank' (giật lag) khi cuộn danh sách và đã thử các cách khác mà chưa hiệu quả. Không nên dùng khi: Danh sách của tụi bây ngắn (vài chục item đổ lại). Việc tối ưu này có thể không cần thiết và đôi khi còn làm phức tạp code hơn. Các item trong danh sách có kích thước KHÁC NHAU RẤT NHIỀU. Lúc này, SliverPrototypeExtentList sẽ gây sai lệch trong tính toán và không mang lại hiệu quả. Hãy dùng SliverList bình thường hoặc ListView.builder và để Flutter tự quản lý kích thước từng item. Tụi bây ĐÃ BIẾT CHÍNH XÁC kích thước của từng item (ví dụ: mỗi item cao đúng 60px). Trong trường hợp này, SliverFixedExtentList với thuộc tính itemExtent sẽ là lựa chọn tối ưu hơn, vì nó còn đơn giản hơn nữa và hiệu quả y hệt. Nhớ nhé, không có 'viên đạn bạc' nào trong lập trình cả. Mỗi 'vũ khí' đều có điểm mạnh, điểm yếu và trường hợp sử dụng riêng. Hiểu rõ nó là gì, dùng để làm gì, và khi nào nên dùng, đó mới là phong thái của một dev Gen Z 'chất'! Chúc tụi bây code mượt, app chạy nhanh, và đừng quên thực hành để biến kiến thức thành kỹ năng nhé! Anh Creyt đi pha trà đây! 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
SliverMultiBoxAdaptorElement: Bí mật của Flutter mượt mà
21/03/2026

SliverMultiBoxAdaptorElement: Bí mật của Flutter mượt mà

Chào các dân chơi code, anh Creyt đây! Hôm nay chúng ta sẽ bóc tách một khái niệm nghe hơi hàn lâm nhưng lại là 'xương sống' giúp các app Flutter của mấy đứa mượt mà như lướt ván trên mạng xã hội vậy: SliverMultiBoxAdaptorElement. Nghe tên thôi đã thấy dài và 'nguy hiểm' rồi phải không? Nhưng yên tâm, qua tay anh Creyt thì cái gì cũng hóa 'kẹo ngọt' hết! 1. SliverMultiBoxAdaptorElement là gì mà nghe 'ghê' vậy anh Creyt? Để anh Creyt kể cho nghe một câu chuyện meta-verse: Tưởng tượng thế giới app của mấy đứa là một cái rạp chiếu phim vĩ đại, nơi mỗi widget là một diễn viên. Và cái màn hình điện thoại của user chính là cái sân khấu chính. Khi mấy đứa có một danh sách cuộn dài dằng dặc (như cái feed TikTok không đáy của mấy đứa vậy), thì việc 'đẩy' tất cả các diễn viên lên sân khấu cùng lúc là điều không thể. Vừa tốn không gian, vừa tốn điện, lại còn làm rạp cháy máy nữa chứ! SliverMultiBoxAdaptorElement chính là vị đạo diễn đại tài và cực kỳ thông minh của cái rạp chiếu phim đó. Nhiệm vụ của ổng là: chỉ đưa những diễn viên nào sắp xuất hiện trên sân khấu (tức là sắp lọt vào tầm nhìn của người dùng) lên thôi. Mấy diễn viên khác cứ việc nghỉ ngơi trong hậu trường (bộ nhớ), không cần phải tốn công hóa trang hay ra sân khấu làm gì. Khi người dùng cuộn, ổng lại điều phối liên tục, diễn viên cũ hết vai thì về hậu trường, diễn viên mới thì lên sân khấu. Quá thông minh phải không? Nói cách khác, đây là cái 'bộ não' đằng sau các widget như ListView.builder, GridView.builder, hay CustomScrollView với SliverList/SliverGrid dùng SliverChildBuilderDelegate. Nó giúp Flutter chỉ render (vẽ) và build (xây dựng) những item cần thiết, tiết kiệm tài nguyên hệ thống một cách đáng kinh ngạc. Đây chính là chìa khóa của 'lazy loading' hay 'virtualization' trong Flutter đó mấy đứa. 2. Code Ví Dụ Minh Họa: 'Đạo Diễn' tài ba trong thực tế Để thấy rõ công việc của vị đạo diễn này, chúng ta hãy xem một ví dụ kinh điển với ListView.builder. Tuy mấy đứa chỉ gọi ListView.builder, nhưng đằng sau nó là cả một hệ thống SliverList và SliverMultiBoxAdaptorElement đang làm việc cật lực đó. 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: 'Creyt\'s Sliver 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> { final List<String> _items = List.generate(1000, (index) => 'Item số ${index + 1}'); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Creyt\'s Lazy List'), ), body: ListView.builder( itemCount: _items.length, itemBuilder: (context, index) { // Đây chính là nơi 'đạo diễn' SliverMultiBoxAdaptorElement // quyết định khi nào thì build (tạo) widget này. // Chỉ khi nó sắp xuất hiện trên màn hình thì mới được gọi. print('Building item ${index + 1}'); // Để xem nó build khi nào return Card( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), elevation: 4, child: Padding( padding: const EdgeInsets.all(16.0), child: Text( _items[index], style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), ); }, ), ); } } Khi mấy đứa chạy app này và cuộn danh sách, hãy mở console (Debug Console trong VS Code hoặc Android Studio) ra mà xem. Mấy đứa sẽ thấy dòng Building item X chỉ xuất hiện khi các item đó sắp hoặc đang hiển thị trên màn hình. Các item từ 500 trở đi sẽ không bao giờ được build nếu mấy đứa không cuộn tới đó. Thấy quyền năng của 'đạo diễn' chưa? 3. Mẹo Vặt (Best Practices) từ anh Creyt để dùng 'đạo diễn' hiệu quả Luôn dùng builder cho danh sách dài: Đây là quy tắc vàng! Tránh xa ListView (không có .builder) nếu danh sách của mấy đứa có thể dài vô tận hoặc rất nhiều item. ListView thường build tất cả mọi thứ cùng lúc, dễ làm app lag, giật như 'ma làm'. Cung cấp key cho các item động: Nếu danh sách của mấy đứa có thể thay đổi thứ tự, thêm/xóa item, hãy dùng Key (ví dụ: ValueKey, ObjectKey) cho từng item trong itemBuilder. Điều này giúp Flutter nhận diện và tái sử dụng các widget một cách hiệu quả hơn, tránh việc phải rebuild lại toàn bộ khi có thay đổi nhỏ. Nó giống như việc đạo diễn biết rõ từng diễn viên, không bị nhầm lẫn khi thay đổi kịch bản vậy. Tối ưu hóa widget con: Mấy đứa có thể giúp 'đạo diễn' làm việc nhẹ nhàng hơn bằng cách tối ưu bản thân các diễn viên (widget con). Dùng const constructors khi có thể, hoặc cân nhắc dùng RepaintBoundary cho những widget con phức tạp, ít thay đổi để giới hạn vùng vẽ lại. Hiểu về addAutomaticKeepAlives và addRepaintBoundaries: Trong SliverChildBuilderDelegate, mấy đứa có thể điều chỉnh các thuộc tính này. addAutomaticKeepAlives: true (mặc định) giúp giữ trạng thái của các widget con khi chúng cuộn ra khỏi màn hình (như giữ nguyên vị trí cuộn của video TikTok). addRepaintBoundaries: true (mặc định) giúp Flutter tối ưu việc vẽ lại, chỉ vẽ lại phần widget con bị thay đổi thôi. Tùy trường hợp mà mấy đứa có thể tắt chúng đi để tối ưu hơn nữa, nhưng hãy cẩn thận và hiểu rõ tác dụng. 4. Ứng dụng thực tế: Ai đang dùng 'đạo diễn' này? Thực ra, gần như mọi ứng dụng có danh sách cuộn dài đều đang dùng cơ chế này, dù trực tiếp hay gián tiếp. Ví dụ: Các mạng xã hội (Facebook, Instagram, TikTok): Feed của mấy đứa dài vô tận, nhưng app vẫn mượt mà. Đó là nhờ SliverMultiBoxAdaptorElement chỉ load những post đang hiển thị. Ứng dụng mua sắm (Shopee, Lazada): Danh sách sản phẩm khổng lồ, nhưng khi cuộn vẫn không bị lag. Tương tự, chỉ những sản phẩm trên màn hình mới được build. Ứng dụng tin tức, đọc truyện: Các bài viết, chương truyện dài hàng trăm trang, nhưng app vẫn 'bay'. Bất cứ khi nào mấy đứa thấy một danh sách cuộn mượt mà không giới hạn, thì 99% là có bàn tay của SliverMultiBoxAdaptorElement đang điều phối hậu trường đó! 5. Thử nghiệm và Nên dùng cho case nào? Anh Creyt đã từng 'ngây thơ' dùng Column bọc trong SingleChildScrollView để hiển thị một danh sách 500 item. Kết quả? App crash ngay lập tức vì quá tải bộ nhớ và CPU. Đó là bài học xương máu về việc phải dùng builder cho các danh sách động và lớn. Nên dùng SliverMultiBoxAdaptorElement (thông qua ListView.builder, GridView.builder, CustomScrollView với SliverList/SliverGrid): Khi mấy đứa có một danh sách lớn hoặc có khả năng lớn vô hạn (ví dụ: feed mạng xã hội, danh sách chat, danh sách sản phẩm). Khi các item trong danh sách phức tạp và tốn tài nguyên để build (có nhiều widget con, animation, hoặc load ảnh). Khi mấy đứa muốn tối ưu hiệu suất và tiết kiệm bộ nhớ cho app. Tránh dùng: Cho các danh sách cực kỳ nhỏ và cố định (ví dụ: 3-5 item). Trong trường hợp này, ListView hoặc Column trong SingleChildScrollView có thể đơn giản hơn và không gây ra vấn đề hiệu suất. Nhớ nhé, SliverMultiBoxAdaptorElement là người bạn tốt nhất của mấy đứa khi muốn xây dựng một app Flutter mượt mà, chuyên nghiệp. Hiểu được cách nó hoạt động sẽ giúp mấy đứa viết code 'xịn' hơn và tránh được nhiều lỗi hiệu suất đáng tiếc. Keep coding, Gen Z! 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é!

34 Đọc tiếp
Lướt Mượt Mà: SliverFixedExtentList - Sức Mạnh Ẩn Dấu Của List Flutter
21/03/2026

Lướt Mượt Mà: SliverFixedExtentList - Sức Mạnh Ẩn Dấu Của List Flutter

Chào các em, hôm nay anh Creyt sẽ bật mí một "bí kíp võ công" trong Flutter giúp danh sách (list) của chúng ta mượt mà như lướt TikTok, không giật lag dù có hàng nghìn item. Đó chính là SliverFixedExtentList – Nghe tên thôi đã thấy "chuyên nghiệp" rồi đúng không? Đừng lo, anh sẽ "giải mã" nó theo cách dễ hiểu nhất, đảm bảo các em "ngấm" ngay! 1. SliverFixedExtentList là gì và để làm gì? Tưởng tượng thế này, em có một thư viện sách khổng lồ, và tất cả các cuốn sách trong thư viện đó đều có chiều cao y hệt nhau. Khi em cần tìm một cuốn sách nào đó, người thủ thư (hay chính là Flutter engine) sẽ không cần phải đo đạc từng cuốn một để xem nó cao bao nhiêu rồi mới biết vị trí của nó trên kệ. Thay vào đó, anh ta chỉ cần biết "À, mỗi cuốn cao 20cm, vậy cuốn thứ 100 sẽ nằm ở vị trí 2000cm tính từ đầu kệ." Việc này nhanh hơn gấp bội! SliverFixedExtentList chính là cái "kệ sách" đặc biệt đó trong Flutter. Nó là một loại Sliver (một phần của vùng cuộn) được thiết kế để hiển thị các danh sách mà tất cả các item con đều có cùng một chiều cao cố định. Mục đích chính? Tối ưu hiệu suất! Khi Flutter biết trước chiều cao của mỗi item, nó không cần phải tính toán lại kích thước cho từng item mỗi khi danh sách cuộn. Điều này giúp giảm tải cho CPU, làm cho việc cuộn danh sách trở nên siêu mượt mà, đặc biệt với những danh sách dài lê thê. 2. Code Ví Dụ Minh Hoạ Rõ Ràng Để sử dụng SliverFixedExtentList, chúng ta thường đặt nó bên trong một CustomScrollView (vì Sliver chỉ sống trong CustomScrollView mà thôi). Hãy xem ví dụ dưới đâ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: 'SliverFixedExtentList Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ const SliverAppBar( expandedHeight: 200.0, floating: false, pinned: true, flexibleSpace: FlexibleSpaceBar( title: Text('Anh Creyt dạy Fixed List'), background: Image( image: NetworkImage('https://picsum.photos/800/600'), fit: BoxFit.cover, ), ), ), // Đây rồi! SliverFixedExtentList của chúng ta SliverFixedExtentList( // 'itemExtent' là chiều cao CỐ ĐỊNH của MỖI item. // Đây là yếu tố then chốt cho hiệu suất! itemExtent: 80.0, // Ví dụ: mỗi item cao 80 pixel delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( alignment: Alignment.center, color: index % 2 == 0 ? Colors.blue[100] : Colors.blue[300], child: Text( 'Item số ${index + 1}', style: const TextStyle(fontSize: 20, color: Colors.black87), ), ); }, childCount: 50, // Tạo 50 item để dễ dàng cuộn thử ), ), ], ), ); } } Trong ví dụ trên, điểm mấu chốt là thuộc tính itemExtent: 80.0. Nó nói cho Flutter biết rằng MỖI item trong danh sách này đều cao 80 pixel. Nhờ đó, Flutter có thể tính toán vị trí và hiển thị các item một cách cực kỳ hiệu quả. 3. Mẹo (Best Practices) để ghi nhớ và dùng thực tế Khi nào dùng? Chỉ dùng SliverFixedExtentList khi bạn chắc chắn rằng TẤT CẢ các item trong danh sách của bạn có cùng một chiều cao. Nếu item có chiều cao khác nhau (ví dụ: tin nhắn chat có thể dài ngắn khác nhau), thì bạn phải dùng SliverList thông thường, dù hiệu suất có thể không bằng. itemExtent là "chìa khóa vàng": Thuộc tính này là linh hồn của SliverFixedExtentList. Đừng quên set nó và hãy đảm bảo giá trị của nó chính xác bằng chiều cao của item con. Nếu bạn set sai, giao diện có thể bị lỗi hiển thị (ví dụ: các item bị chồng lên nhau hoặc có khoảng trắng thừa). Hiệu suất vượt trội: Với SliverFixedExtentList, Flutter không cần phải gọi builder cho từng item để đo kích thước của nó. Nó chỉ cần biết index của item và itemExtent để tính ra vị trí. Điều này giúp giảm đáng kể số lượng công việc phải làm trên mỗi frame khi cuộn, dẫn đến trải nghiệm người dùng mượt mà hơn. Ghi nhớ bằng hình ảnh: Hãy nhớ lại cái "kệ sách đồng bộ" của anh Creyt. Khi mọi thứ đều có kích thước chuẩn, việc quản lý và tìm kiếm sẽ dễ dàng hơn rất nhiều! 4. Ví dụ thực tế các ứng dụng/website đã ứng dụng Trong thế giới thực, SliverFixedExtentList được ứng dụng nhiều hơn bạn nghĩ, đặc biệt trong các trường hợp cần sự đồng bộ về giao diện và hiệu suất cao: Danh bạ điện thoại: Hầu hết các item trong danh bạ (tên, số điện thoại) đều có cùng một chiều cao. SliverFixedExtentList là lựa chọn lý tưởng. Menu cài đặt (Settings): Các mục cài đặt thường là các ListTile có chiều cao đồng nhất, rất phù hợp để dùng SliverFixedExtentList. Các danh sách sản phẩm đơn giản: Nếu bạn có một danh sách sản phẩm mà mỗi item hiển thị chỉ có tên và giá, và bạn đã thiết kế chúng với chiều cao cố định, SliverFixedExtentList sẽ giúp list của bạn cuộn mượt mà hơn. Lịch (Calendar view): Khi hiển thị các ngày trong tháng dưới dạng lưới hoặc danh sách với chiều cao ô cố định. 5. Thử nghiệm của anh Creyt và hướng dẫn nên dùng cho case nào Hồi mới tập tành code Flutter, anh Creyt cũng từng "ngây thơ" dùng SliverList cho mọi thứ, từ danh sách tin nhắn chat (mà tin nhắn thì dài ngắn khác nhau) đến danh sách cài đặt. Đến khi làm một app có danh sách hàng nghìn item đồng bộ, và anh thấy app cứ "giật đùng đùng" như phim hành động mỗi khi cuộn nhanh. Lúc đó, anh mới bắt đầu đào sâu về Sliver và "ngộ" ra SliverFixedExtentList là chân ái cho những list "đều tăm tắp"! Anh đã thử nghiệm bằng cách xây dựng hai danh sách giống hệt nhau, một dùng SliverList và một dùng SliverFixedExtentList với 10.000 item. Khi chạy trên thiết bị cấu hình thấp và bật công cụ Performance Overlay của Flutter, sự khác biệt là "một trời một vực". SliverFixedExtentList duy trì 60fps một cách ổn định, trong khi SliverList dễ dàng bị tụt frame, đặc biệt khi cuộn nhanh. Khi nào nên dùng? Khi bạn có một danh sách dài (trên vài chục item) mà mỗi item có chiều cao không đổi. Khi bạn muốn tối ưu hóa hiệu suất cuộn để mang lại trải nghiệm người dùng tốt nhất, đặc biệt trên các thiết bị có cấu hình không quá mạnh. Khi bạn cần sự đồng bộ và gọn gàng trong thiết kế giao diện, nơi mỗi hàng đều có kích thước chuẩn. Nhớ nhé các em, trong lập trình, việc chọn đúng công cụ cho đúng việc là yếu tố quyết định sự "sang chảnh" của app và trải nghiệm người dùng. SliverFixedExtentList là một trong những "công cụ vàng" mà các em cần nắm vững! 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
SliverFillRemainingBoxAdaptor: Kẻ Lấp Đầy Khoảng Trống Trong Flutter!
21/03/2026

SliverFillRemainingBoxAdaptor: Kẻ Lấp Đầy Khoảng Trống Trong Flutter!

Anh em Gen Z mê code ơi, hôm nay anh Creyt sẽ dắt tụi em đi “bóc phốt” một thằng cha khá thú vị trong hội “Sliver” của Flutter: SliverFillRemainingBoxAdaptor. Nghe tên dài ngoằng, khó nuốt đúng không? Yên tâm, anh em mình sẽ biến nó thành món gà rán giòn tan, dễ hiểu cực kỳ! 1. SliverFillRemainingBoxAdaptor là gì? Để làm gì mà nó “ngầu” vậy? Để hiểu thằng cha này, trước hết mình phải hiểu “Sliver” là gì cái đã. Tưởng tượng một cái cuộn phim (scroll view) dài ngoằng của tụi em đó, thì mỗi phân đoạn trên cuộn phim đó chính là một “Sliver”. Thay vì dùng mấy cái widget “full-size” như ListView hay SingleChildScrollView mà nó cứ render tùm lum tà la, thì CustomScrollView kết hợp với các Sliver sẽ chỉ render những gì thực sự cần thiết trên màn hình thôi. Tiết kiệm tài nguyên vãi chưởng! Thế còn SliverFillRemainingBoxAdaptor? À, thằng này nó là “kẻ lấp đầy khoảng trống còn lại” của cái cuộn phim đó. Nghe nó cứ “chiếm hữu” sao đó ha? Đúng vậy! Tưởng tượng tụi em có một CustomScrollView mà nội dung bên trên nó ngắn ngủn, không đủ lấp đầy màn hình. Thay vì để một khoảng trắng “vô duyên” ở dưới, thì thằng cha SliverFillRemainingBoxAdaptor này sẽ nhảy vào, chiếm trọn phần không gian còn trống đó và “ôm” lấy widget con của nó. Nó giống như cái ông hàng xóm nhiệt tình quá mức, thấy nhà mình còn trống cái gì là ổng mang đồ qua lấp đầy hết vậy đó! Mục đích chính của nó: Đảm bảo một widget (ví dụ: nút "Thêm vào giỏ hàng", thanh nhập tin nhắn chat, hoặc một cái footer) luôn luôn hiển thị ở cuối vùng cuộn và chiếm trọn phần không gian còn lại nếu các nội dung khác không đủ dài để lấp đầy. 2. Code Ví Dụ Minh Họa Rõ Ràng, Chuẩn Kiến Thức Thôi lý thuyết đủ rồi, giờ mình đi vào thực chiến cho máu! Anh em xem ví dụ này để thấy thằng cha SliverFillRemainingBoxAdaptor nó hoạt động như thế nào 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: 'Creyt\'s Sliver Demo', theme: ThemeData( primarySwatch: Colors.blueGrey, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const SliverFillRemainingDemo(), ); } } class SliverFillRemainingDemo extends StatelessWidget { const SliverFillRemainingDemo({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Creyt dạy SliverFillRemaining'), ), body: CustomScrollView( slivers: <Widget>[ // Header cứng đầu, chỉ cao 100px SliverToBoxAdapter( child: Container( height: 100.0, color: Colors.amber[200], alignment: Alignment.center, child: const Text( 'Đây là Header (SliverToBoxAdapter)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), ), // Danh sách các item ngắn ngủn SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50.0, color: index % 2 == 0 ? Colors.blue[100] : Colors.blue[200], alignment: Alignment.center, child: Text('Item ${index + 1}'), ); }, childCount: 5, // Chỉ có 5 item, không đủ lấp đầy màn hình ), ), // Chính nó đây rồi: Kẻ lấp đầy khoảng trống! SliverFillRemaining( // hasScrollBody: true, // Thử bật cái này nếu nội dung bên trong cũng cần cuộn child: Container( color: Colors.green[100], alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Đây là phần còn lại được lấp đầy (SliverFillRemaining)', textAlign: TextAlign.center, style: TextStyle(fontSize: 16, color: Colors.green), ), const SizedBox(height: 10), ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Nút này nằm ở cuối!')), ); }, child: const Text('Nút hành động ở cuối trang'), ), ], ), ), ), ], ), ); } } Giải thích code: Chúng ta có một CustomScrollView chứa các slivers. SliverToBoxAdapter: Dùng để đưa một widget RenderBox (như Container) vào làm Sliver. Ở đây là cái Header cao 100px. SliverList: Tạo một danh sách các item. Anh em thấy đó, anh Creyt chỉ cho 5 item thôi, nên nó không đủ dài để lấp đầy màn hình. SliverFillRemaining: Đây là nhân vật chính của chúng ta. Nó sẽ "ngốn" hết phần không gian còn lại trên màn hình sau khi SliverToBoxAdapter và SliverList đã render. Bên trong nó, anh em có thể đặt bất kỳ widget nào, ví dụ như cái Container màu xanh lá cây với một cái nút hành động đó. Khi chạy code này, dù danh sách chỉ có 5 item ngắn ngủn, cái Container màu xanh lá cây chứa nút bấm vẫn sẽ luôn luôn nằm ở cuối màn hình và chiếm trọn phần không gian thừa ra. Đỉnh của chóp! 3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế "Thằng cha tham lam": Luôn nhớ SliverFillRemainingBoxAdaptor là một thằng rất "tham lam". Nó sẽ lấy toàn bộ không gian còn lại mà không hỏi ý kiến ai. Đừng đặt nó ở giữa một đống Sliver khác mà anh em muốn kiểm soát chiều cao chặt chẽ, dễ bị vỡ layout lắm! Bạn thân của CustomScrollView: Nó chỉ có ý nghĩa khi dùng trong CustomScrollView (hoặc các widget dựa trên Sliver khác). Đừng cố nhét nó vào ListView hay Column thường, nó sẽ không hoạt động đúng đâu. hasScrollBody - Cái này hay nè!: Mặc định, SliverFillRemaining sẽ không cho phép nội dung bên trong nó tự cuộn. Nếu nội dung bên trong SliverFillRemaining của tụi em cũng cần cuộn (ví dụ: một ListView con bên trong nó), hãy set hasScrollBody: true. Khi đó, SliverFillRemaining sẽ tự nó quản lý việc cuộn của nội dung con, và nó sẽ chỉ cuộn khi toàn bộ CustomScrollView đã cuộn hết các Sliver khác. Phân biệt với SliverToBoxAdapter: SliverToBoxAdapter dùng khi anh em muốn một widget có chiều cao cố định. SliverFillRemaining dùng khi anh em muốn một widget chiếm phần không gian còn lại. 4. Học thuật sâu của anh Creyt: "BoxAdaptor" là gì? Trong Flutter, mọi thứ đều là widget, và đằng sau mỗi widget là một RenderObject chịu trách nhiệm vẽ và bố cục. Các Sliver cũng vậy, chúng có RenderSliver riêng. SliverFillRemainingBoxAdaptor là một Sliver đặc biệt, nó có nhiệm vụ chuyển đổi một RenderBox thông thường (như Container, Column, Row, Text,...) thành một RenderSliver. Cái đuôi "BoxAdaptor" trong tên nó chính là nói lên điều này: nó "adapt" (thích nghi) một "Box" widget (RenderBox) để hoạt động như một "Sliver". Khi CustomScrollView bố cục, nó sẽ hỏi từng Sliver xem mày cần bao nhiêu không gian. Các SliverList, SliverGrid sẽ tính toán dựa trên số lượng item. Riêng SliverFillRemaining, nó sẽ chờ cho các Sliver khác tính toán xong xuôi, rồi nó mới "xem xét" còn bao nhiêu không gian trống trên màn hình (viewport) và "chiếm trọn" phần đó. Đây là lý do tại sao nó luôn nằm ở cuối và lấp đầy. 5. Ví dụ thực tế các ứng dụng/website đã ứng dụng Anh em thấy SliverFillRemainingBoxAdaptor ở đâu ngoài đời không? Có chứ, nhiều là đằng khác! Màn hình chat: Tưởng tượng một ứng dụng chat. Các tin nhắn là một danh sách cuộn. Nhưng cái thanh nhập liệu (input field) với nút gửi tin nhắn thì luôn muốn nằm sát dưới cùng màn hình, dù danh sách tin nhắn có ngắn đến đâu. Đó chính là một case hoàn hảo cho SliverFillRemainingBoxAdaptor. Trang chi tiết sản phẩm: Một trang sản phẩm có ảnh, mô tả, giá cả,... và ở cuối cùng là nút "Thêm vào giỏ hàng" hoặc "Mua ngay". Nếu mô tả sản phẩm quá ngắn, tụi em không muốn cái nút đó lơ lửng giữa màn hình, mà nó phải "dính" vào cuối vùng cuộn. SliverFillRemainingBoxAdaptor làm được điều đó. Các form dài: Đôi khi tụi em có một form đăng ký hay điền thông tin dài ngoằng. Cái nút "Gửi" (Submit) luôn cần nằm ở cuối form, và nếu form đó không đủ dài, nó vẫn phải dính vào đáy màn hình. SliverFillRemainingBoxAdaptor lại phát huy tác dụng. 6. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào Anh Creyt đã từng "vật lộn" với việc làm sao để một cái nút "Add to Cart" luôn dính vào cuối màn hình trên một trang sản phẩm có nội dung động. Ban đầu, anh thử dùng Column với Expanded, nhưng khi nội dung dài ra thì cái nút đó lại bị đẩy ra ngoài vùng nhìn. Dùng Stack thì lại phức tạp khi muốn nó cuộn cùng với nội dung. Cuối cùng, SliverFillRemainingBoxAdaptor chính là "chân ái". Anh chỉ việc đặt các Sliver chứa nội dung sản phẩm lên trên, và cuối cùng là SliverFillRemaining bọc cái nút "Add to Cart". Đảm bảo nó luôn ở đúng vị trí! Nên dùng SliverFillRemainingBoxAdaptor khi nào? Khi tụi em muốn một widget luôn luôn chiếm trọn phần không gian còn lại của CustomScrollView (từ vị trí của nó đến cuối viewport). Khi tụi em cần một "footer" hay một "action bar" dính chặt vào cuối của một vùng cuộn, bất kể nội dung bên trên nó dài hay ngắn. Khi tụi em đang xây dựng một UI mà cần sự linh hoạt trong việc lấp đầy không gian còn trống một cách tự động. Không nên dùng khi nào? Nếu tụi em muốn một widget có chiều cao cố định và không thay đổi. Khi đó SliverToBoxAdapter là lựa chọn tốt hơn. Nếu tụi em không cần các tính năng của CustomScrollView và chỉ cần một danh sách đơn giản, ListView hoặc Column với Expanded (nếu không cuộn) sẽ đơn giản hơn. Nhớ nhé anh em, SliverFillRemainingBoxAdaptor không chỉ là một cái tên dài, nó là một công cụ cực kỳ mạnh mẽ để tạo ra các layout cuộn linh hoạt và đẹp mắt trong Flutter. Cứ thử nghiệm đi, có gì khó cứ hỏi 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
SliverConstraints: "Kịch bản" bí ẩn của vũ trụ cuộn Flutter
21/03/2026

SliverConstraints: "Kịch bản" bí ẩn của vũ trụ cuộn Flutter

SliverConstraints: "Kịch bản" bí ẩn của vũ trụ cuộn Flutter Chào các chiến hữu của Creyt! Hôm nay, chúng ta sẽ cùng nhau "đột nhập" vào một trong những khái niệm nền tảng nhưng cũng "khoai" nhất của vũ trụ cuộn trong Flutter: SliverConstraints. Nghe tên thôi đã thấy mùi học thuật rồi đúng không? Đừng lo, anh Creyt sẽ "tháo gỡ" nó cho các em dễ hiểu hơn cả crush rep tin nhắn! 1. SliverConstraints là gì mà ghê gớm vậy? Để dễ hình dung, các em hãy tưởng tượng thế này: Một CustomScrollView giống như một sân khấu lớn đang cuộn, và mỗi Sliver (ví dụ như SliverList, SliverGrid, SliverPersistentHeader) là một diễn viên đang biểu diễn trên sân khấu đó. Vậy thì, SliverConstraints chính là bản kịch và ánh đèn sân khấu dành riêng cho từng diễn viên Sliver. Nó không phải là một widget, mà là một đối tượng chứa thông tin quan trọng mà "đạo diễn" (ScrollView) truyền xuống cho "diễn viên" (Sliver) để diễn viên biết mình được phép làm gì, ở đâu, và trong phạm vi nào. Các thông tin này bao gồm: scrollOffset: Em đã cuộn được bao nhiêu "km" rồi? (Vị trí hiện tại của Sliver so với điểm đầu của ScrollView). viewportMainAxisExtent: Sân khấu này rộng/dài bao nhiêu "m"? (Kích thước của vùng nhìn thấy được – viewport – theo trục cuộn chính). precedingScrollExtent: Các diễn viên "đàn anh đàn chị" trước em đã chiếm bao nhiêu "diện tích" trên sân khấu rồi? (Tổng kích thước của các sliver đứng trước nó). remainingPaintExtent: Từ vị trí của em cho đến cuối sân khấu, còn bao nhiêu "đất" để em diễn? (Phần còn lại của viewport mà sliver có thể vẽ). crossAxisExtent: Sân khấu này rộng bao nhiêu theo chiều ngang (nếu cuộn dọc) hoặc chiều dọc (nếu cuộn ngang)? (Kích thước theo trục phụ). overlap: Em có đang bị "đè" bởi một Sliver khác (như SliverPersistentHeader ghim) không? Và đè bao nhiêu? (Giá trị này thường âm, dùng để điều chỉnh vị trí). Để làm gì? Đơn giản là để tối ưu hóa hiệu suất và tạo ra những hiệu ứng cuộn "ảo diệu"! Flutter cần SliverConstraints để biết chính xác khi nào một Sliver cần được vẽ, vẽ ở đâu, và vẽ bao nhiêu. Nhờ đó, nó chỉ render những phần thực sự nằm trong tầm nhìn của người dùng, giúp ứng dụng mượt mà như "nhung" dù danh sách có dài đến "vô tận" đi chăng nữa. 2. Code Ví Dụ Minh Hoạ: "Đạo diễn" hiệu ứng header co giãn Một trong những ứng dụng phổ biến nhất của SliverConstraints mà các em thường thấy chính là các SliverPersistentHeader – những cái header có thể co giãn, ghim lại khi cuộn. Nó không trực tiếp expose SliverConstraints cho chúng ta, nhưng nó cung cấp shrinkOffset và overlapsContent trong SliverPersistentHeaderDelegate, mà hai giá trị này lại được tính toán trực tiếp từ SliverConstraints đó! Anh Creyt sẽ demo cho các em thấy cách một SliverPersistentHeader dùng "bản kịch" này để thay đổi giao diện "như tắc kè hoa" khi người dùng cuộn. import 'package:flutter/material.dart'; class MyPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { final double minHeight; final double maxHeight; final Widget child; MyPersistentHeaderDelegate({ required this.minHeight, required this.maxHeight, required this.child, }); @override double get minExtent => minHeight; @override double get maxExtent => maxHeight; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { // shrinkOffset: Chính là mức độ header của chúng ta đã "co lại" bao nhiêu. // Giá trị này thay đổi từ 0 (khi header đầy đủ) đến (maxHeight - minHeight) // khi header co lại tối đa. // Nó liên quan trực tiếp đến scrollOffset và overlap từ SliverConstraints. // overlapsContent: Header có đang bị nội dung bên dưới "đè" lên không? // (Cũng được tính từ SliverConstraints.overlap) // Tính toán tỷ lệ co lại để thay đổi UI cho "nghệ thuật" final double collapseRatio = shrinkOffset / (maxHeight - minHeight); final double opacity = (1.0 - collapseRatio).clamp(0.0, 1.0); // Ví dụ: fade out text return Container( color: Colors.blueAccent.withOpacity(0.8 + 0.2 * collapseRatio), // Thay đổi màu theo scroll child: Stack( fit: StackFit.expand, children: [ // Background có thể scale hoặc parallax Image.network( 'https://picsum.photos/800/600', // Ảnh nền "đỉnh của chóp" fit: BoxFit.cover, // Hiệu ứng parallax nhẹ: ảnh cuộn chậm hơn nội dung alignment: Alignment(0, collapseRatio * 0.5 - 0.25), // Điều chỉnh vị trí ảnh ), Positioned( bottom: 16, left: 16, child: Opacity( opacity: opacity, // Text hiện dần khi header mở rộng child: Text( 'Chào mừng đến với SliverLand!', style: TextStyle( color: Colors.white, fontSize: 24 * (1 - 0.5 * collapseRatio).clamp(18.0, 24.0), // Scale text fontWeight: FontWeight.bold, ), ), ), ), Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.all(8.0), child: Text( 'Shrink Offset: ${shrinkOffset.toStringAsFixed(2)}', // Để thấy giá trị thay đổi style: const TextStyle(color: Colors.white70), ), ), ) ], ), ); } @override bool shouldRebuild(covariant MyPersistentHeaderDelegate oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; // Nếu các thuộc tính này thay đổi, cần rebuild } } class SliverConstraintsDemo extends StatelessWidget { const SliverConstraintsDemo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverPersistentHeader( delegate: MyPersistentHeaderDelegate( minHeight: kToolbarHeight, // Chiều cao tối thiểu khi cuộn lên hết (ví dụ: bằng AppBar) maxHeight: 250.0, // Chiều cao tối đa ban đầu của header child: Container(), // Child ở đây không thực sự dùng, mà nội dung nằm trong build của delegate ), pinned: true, // Ghim header lại khi cuộn lên, không cho nó biến mất hoàn toàn ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 100.0, color: index.isEven ? Colors.grey[200] : Colors.grey[300], child: Center(child: Text('Item ${index + 1}', style: const TextStyle(fontSize: 18))), ); }, childCount: 50, // 50 item để có thể cuộn thoải mái ), ), ], ), ); } } void main() { runApp(const MaterialApp(home: SliverConstraintsDemo())); } Chạy đoạn code trên, các em sẽ thấy một header ảnh nền lớn, khi cuộn lên nó sẽ co lại, chữ fade out, và ảnh nền có thể di chuyển chậm hơn một chút (hiệu ứng parallax). Tất cả những "phép thuật" này đều nhờ vào việc SliverPersistentHeaderDelegate nhận được thông tin từ SliverConstraints (dưới dạng shrinkOffset) và biết cách điều chỉnh giao diện của nó. 3. Mẹo "hack não" và Best Practices từ anh Creyt Đừng sợ hãi, hãy làm quen! SliverConstraints nghe có vẻ "to tát" nhưng thực chất nó chỉ là một gói thông tin. Hãy coi nó như "bộ chỉ dẫn" mà Flutter cung cấp cho các Sliver để chúng "biết điều" mà hoạt động. Nắm vững các thuộc tính chính: scrollOffset, viewportMainAxisExtent, remainingPaintExtent, crossAxisExtent là những "ngôi sao" mà em sẽ gặp đi gặp lại. Hiểu được chúng là hiểu được 80% câu chuyện rồi. Sử dụng SliverPersistentHeader để "làm quen": Đây là "trường học vỡ lòng" tuyệt vời để thấy SliverConstraints hoạt động như thế nào thông qua shrinkOffset và overlapsContent mà không cần phải "đụng chạm" vào RenderSliver phức tạp. Tối ưu hiệu suất là "chân ái": Luôn nhớ rằng mục đích của SliverConstraints là giúp Flutter chỉ vẽ những gì cần thiết. Khi tự custom RenderSliver, đừng cố gắng vẽ mọi thứ nếu nó nằm ngoài remainingPaintExtent hoặc paintExtent. "Tiết kiệm" tài nguyên là "phong cách" của dân dev chuyên nghiệp! Khi nào cần "đàm phán" trực tiếp với SliverConstraints? Khi các widget Sliver có sẵn không đủ "đô" cho ý tưởng "điên rồ" của em (ví dụ: một hiệu ứng cuộn hoàn toàn mới, một layout "tự chế" không giống ai). Lúc đó, việc tự viết một RenderSliver và "đọc" trực tiếp SliverConstraints là điều không thể tránh khỏi. Đó là lúc em trở thành "đạo diễn" thực thụ của sân khấu cuộn! 4. Ứng dụng thực tế: "Đâu đâu cũng thấy nó" Các em có biết không, SliverConstraints (hoặc cơ chế tương tự) có mặt ở khắp mọi nơi trong các ứng dụng "đỉnh cao" mà các em dùng hàng ngày: TikTok/Instagram/Facebook: Các feed cuộn vô tận, các story bar ở trên cùng (có thể ghim hoặc ẩn hiện) đều sử dụng cơ chế Sliver để tối ưu hiệu suất và tạo cảm giác cuộn mượt mà. Netflix/Spotify: Màn hình chi tiết phim/bài hát với header lớn, cuộn lên sẽ thu nhỏ lại hoặc biến mất, là ví dụ điển hình của SliverPersistentHeader dùng SliverConstraints để điều chỉnh. Các ứng dụng tin tức (VnExpress, Zing News): Các thanh tìm kiếm, banner quảng cáo ghim trên đầu hoặc thanh điều hướng tự động ẩn/hiện khi cuộn. Google Maps/Uber: Các sheet trượt từ dưới lên (như DraggableScrollableSheet) cũng dựa trên cơ chế Sliver để biết mình nên mở rộng bao nhiêu, co lại bao nhiêu tùy thuộc vào hành vi cuộn của người dùng. 5. Thử nghiệm của Creyt và lời khuyên "thực chiến" Anh Creyt đã từng "vò đầu bứt tóc" khi muốn tạo một hiệu ứng header cuộn mà ảnh nền "trồi lên" khi cuộn xuống và "chìm xuống" khi cuộn lên, kết hợp với text fade in/out. Ban đầu, anh cứ nghĩ phải dùng NotificationListener hay ScrollController để "nghe ngóng" sự kiện cuộn, rồi tự tính toán kích thước, vị trí – một công việc cực khổ và dễ sai sót. Nhưng khi "ngộ ra" SliverConstraints (cụ thể là shrinkOffset trong SliverPersistentHeaderDelegate), mọi thứ trở nên dễ dàng hơn nhiều! shrinkOffset đã "tóm gọn" tất cả thông tin về mức độ co giãn của header, việc của anh chỉ là dùng giá trị đó để "biến hóa" giao diện. Nó giống như việc bạn được cấp cho một "bản đồ" và "la bàn" chính xác thay vì phải mò mẫm trong bóng tối vậy. Vậy nên dùng SliverConstraints (hoặc các widget Sliver có sẵn) cho các case nào? Header co giãn (Collapsible/Expandable Header): Tạo các hiệu ứng header "động" như trong ví dụ trên. Parallax Effect: Khi muốn một phần nội dung (thường là ảnh nền) cuộn chậm hơn so với nội dung chính, tạo chiều sâu cho giao diện. Sticky Header/Footer: Ghim một phần nội dung (ví dụ: thanh tìm kiếm, nút hành động) lại khi cuộn, không để nó biến mất. Lazy Loading Lists/Grids: Các widget như SliverList, SliverGrid tận dụng SliverConstraints để chỉ xây dựng và render các item khi chúng sắp hoặc đã nằm trong vùng nhìn thấy, giúp tiết kiệm bộ nhớ và CPU. Layout cuộn "tự chế": Khi bạn cần một layout cuộn siêu đặc biệt, không có widget nào có sẵn đáp ứng được. Lúc đó, việc tự tạo RenderSliver và "đọc" SliverConstraints là con đường duy nhất. Hiểu SliverConstraints không chỉ là học một khái niệm, mà là mở ra cánh cửa đến thế giới của những hiệu ứng cuộn "đỉnh cao" và tối ưu hiệu suất trong Flutter. Hãy "chiến" nó, các em nhé! 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é!

43 Đọc tiếp
SliverAnimatedOpacity: Biến mất mượt mà trong Flutter!
21/03/2026

SliverAnimatedOpacity: Biến mất mượt mà trong Flutter!

Chào các dev tương lai, anh Creyt đây! Hôm nay chúng ta sẽ cùng “mổ xẻ” một cái tên nghe hơi “dài dòng” nhưng lại cực kỳ xịn sò trong Flutter: SliverAnimatedOpacity. Nghe thì có vẻ phức tạp, nhưng thực ra nó chỉ là bậc thầy của nghệ thuật “biến hình” nhẹ nhàng thôi. SliverAnimatedOpacity là gì mà “cool” vậy? Để dễ hình dung, các em cứ nghĩ thế này: trong thế giới số, đôi khi chúng ta không muốn một thứ gì đó đột ngột biến mất hay xuất hiện như một cú cắt cảnh “thô bạo” của mấy ông đạo diễn phim hành động hạng B. Chúng ta muốn sự mượt mà, uyển chuyển, như cách một DJ chuyên nghiệp fade out (làm mờ dần) một bản nhạc chứ không phải tắt phụt cái rụp. SliverAnimatedOpacity chính là cái “bàn DJ” đó, nhưng dành cho các Sliver trong Flutter. Sliver: Nhớ lại cái bài học về CustomScrollView không? Sliver là những mảnh ghép thông minh, linh hoạt để xây dựng các vùng cuộn (scrollable areas) hiệu quả hơn. Nó giống như các “modul” được tối ưu hóa để hiển thị nội dung, đặc biệt là khi danh sách của các em dài dằng dặc như danh sách crush của một hot girl vậy. AnimatedOpacity: Còn cái này thì đơn giản là một cái “công tắc điều chỉnh độ sáng” (dimmer switch) cho bất kỳ widget nào. Em muốn widget mờ đi, rõ lên, cứ đưa cho nó một giá trị opacity từ 0.0 (trong suốt hoàn toàn) đến 1.0 (rõ nét hoàn toàn), nó sẽ tự động làm mượt mà trong một khoảng thời gian nhất định. Vậy, SliverAnimatedOpacity chính là sự kết hợp hoàn hảo: nó cho phép các em điều chỉnh độ trong suốt của một Sliver con (một mảnh ghép trong danh sách cuộn) một cách mượt mà, có hiệu ứng chuyển động. Thay vì một cái item trong danh sách cuộn “póc” cái biến mất, nó sẽ từ từ mờ dần đi như một ảo thuật gia đang rút lui khỏi sân khấu vậy. Để làm gì? Đơn giản là để UI (giao diện người dùng) của các em trông “xịn” hơn, “pro” hơn, và mang lại trải nghiệm người dùng mượt mà, dễ chịu hơn. Nó giúp người dùng cảm thấy ứng dụng của các em “sống động” và “có hồn” hơn. Code Ví Dụ Minh Hoạ: Màn ảo thuật của Creyt Giờ thì chúng ta cùng xem cách SliverAnimatedOpacity hoạt động trong thực tế nhé. Anh sẽ làm một ví dụ đơn giản với một danh sách cuộn, và một item đặc biệt có thể “biến hình” mờ dần đi hoặc hiện ra. 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: 'SliverAnimatedOpacity Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const SliverAnimatedOpacityScreen(), ); } } class SliverAnimatedOpacityScreen extends StatefulWidget { const SliverAnimatedOpacityScreen({super.key}); @override State<SliverAnimatedOpacityScreen> createState() => _SliverAnimatedOpacityScreenState(); } class _SliverAnimatedOpacityScreenState extends State<SliverAnimatedOpacityScreen> { bool _isVisible = true; // Biến để kiểm soát trạng thái hiển thị @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SliverAnimatedOpacity by Creyt'), backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), body: CustomScrollView( slivers: <Widget>[ SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { // Chúng ta sẽ làm mờ item thứ 5 (index = 4) if (index == 4) { return SliverAnimatedOpacity( opacity: _isVisible ? 1.0 : 0.0, // Opacity thay đổi dựa vào _isVisible duration: const Duration(milliseconds: 700), // Thời gian chuyển động curve: Curves.easeInOut, // Kiểu chuyển động (nhanh dần rồi chậm dần) sliver: SliverToBoxAdapter( // Bọc widget con vào SliverToBoxAdapter child: Container( height: 120, color: Colors.redAccent.shade100, margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), alignment: Alignment.center, child: const Text( 'Tui là item biến hình nè!', style: TextStyle(color: Colors.deepPurple, fontSize: 20, fontWeight: FontWeight.bold), ), ), ), ); } // Các item còn lại của danh sách return Container( height: 80, color: index % 2 == 0 ? Colors.blueGrey[50] : Colors.blueGrey[100], margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), alignment: Alignment.center, child: Text( 'Item thứ ${index + 1}', style: TextStyle(fontSize: 16, color: Colors.blueGrey[800]), ), ); }, childCount: 20, // Tổng số item trong danh sách ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _isVisible = !_isVisible; // Đổi trạng thái hiển thị }); }, backgroundColor: Colors.deepPurple, child: Icon( _isVisible ? Icons.visibility_off : Icons.visibility, color: Colors.white, ), ), ); } } Giải thích nhanh: Chúng ta dùng CustomScrollView để chứa các Sliver. Trong SliverList, chúng ta tạo ra 20 Container. Đặc biệt, Container ở index == 4 (tức là item thứ 5) được bọc trong SliverAnimatedOpacity. Khi nhấn FloatingActionButton, biến _isVisible sẽ thay đổi, kéo theo opacity của SliverAnimatedOpacity thay đổi từ 1.0 xuống 0.0 (hoặc ngược lại) trong 700ms, tạo hiệu ứng mờ dần/hiện ra. Mẹo (Best Practices) của Creyt để "hack não" và dùng thực tế Thời gian là vàng (và bạc): Chọn duration cho animation thật hợp lý. Quá nhanh thì người dùng chưa kịp nhận ra hiệu ứng, trông sẽ bị giật. Quá chậm thì họ lại phải chờ đợi, gây khó chịu. Thông thường, 300-700ms là khoảng thời gian “vàng” cho các hiệu ứng mờ dần. Đừng quên curve: Thuộc tính curve giúp animation của em có “cảm xúc” hơn. Curves.easeInOut là lựa chọn an toàn, làm chuyển động bắt đầu và kết thúc nhẹ nhàng. Curves.fastOutSlowIn cũng là một lựa chọn tuyệt vời. Lưu ý quan trọng: SliverAnimatedOpacity không làm mất không gian! Khi opacity về 0.0, widget con bên trong vẫn chiếm chỗ trong layout. Nó chỉ trong suốt thôi, chứ không phải biến mất hoàn toàn khỏi cây widget. Nếu em muốn nó biến mất hoàn toàn và giải phóng không gian, em cần kết hợp thêm logic khác (ví dụ: dùng Visibility với maintainState: false, maintainAnimation: false, maintainSize: false hoặc loại bỏ widget đó khỏi cây sau khi animation kết thúc). Kết hợp sức mạnh: SliverAnimatedOpacity có thể kết hợp với các Sliver khác như SliverAppBar, SliverGrid để tạo ra những hiệu ứng phức tạp và đẹp mắt hơn nhiều. Ứng dụng thực tế: Ai đã dùng "bùa" này? Feed mạng xã hội (Facebook, Instagram): Khi em ẩn một bài viết, hoặc khi một thông báo mới xuất hiện, nó thường không “nhảy bổ” vào màn hình mà mờ dần xuất hiện, hoặc mờ dần biến mất khi em tương tác với nó. Ứng dụng quản lý tác vụ (Trello, Todoist): Khi em đánh dấu một nhiệm vụ là hoàn thành, thay vì biến mất ngay lập tức, nhiệm vụ đó có thể mờ dần đi, tạo cảm giác “từ từ hoàn tất” chứ không phải “biến mất không dấu vết”. E-commerce (Shopee, Lazada): Khi một sản phẩm hết hàng hoặc không còn khả dụng, nó có thể mờ đi một chút để báo hiệu cho người dùng mà không cần loại bỏ hoàn toàn khỏi danh sách sản phẩm. Loaders/Placeholders: Khi nội dung thực tế đang tải, một placeholder có thể mờ dần đi để nhường chỗ cho nội dung đã tải xong. Thử nghiệm và Nên dùng cho case nào? Anh Creyt đã từng thử nghiệm: Rất nhiều lần! Đặc biệt là khi làm các ứng dụng có danh sách dài và cần tương tác động với các phần tử. Ví dụ, khi người dùng xóa một item khỏi danh sách yêu thích, việc item đó mờ dần rồi biến mất tạo cảm giác tự nhiên và ít gây sốc hơn là “póc” cái item biến mất luôn. Nên dùng cho case nào? Hiển thị/ẩn các thông báo ngắn gọn trong danh sách: Ví dụ, một banner “Bạn có tin nhắn mới” xuất hiện ở đầu danh sách và mờ dần đi sau vài giây. Tương tác với các phần tử trong danh sách: Khi người dùng “swipe to dismiss” (vuốt để bỏ qua) một item, nó có thể mờ dần trước khi bị loại bỏ hoàn toàn. Thay đổi trạng thái của item: Một item trong danh sách chuyển từ trạng thái “đang xử lý” sang “hoàn thành” có thể có hiệu ứng mờ nhẹ để báo hiệu sự thay đổi. Load dữ liệu động: Khi một phần dữ liệu mới được tải vào danh sách cuộn, nó có thể mờ dần xuất hiện. Không nên dùng đơn độc khi: Em muốn widget biến mất hoàn toàn khỏi layout và giải phóng không gian. Trong trường hợp này, hãy kết hợp SliverAnimatedOpacity với việc loại bỏ widget khỏi cây sau khi animation kết thúc (ví dụ, dùng AnimatedSwitcher hoặc Visibility với các thuộc tính maintain là false). Em cần các loại animation phức tạp hơn như thay đổi kích thước, vị trí, hay xoay. Lúc đó, em sẽ cần đến các widget SliverAnimated khác hoặc tự xây dựng với AnimatedBuilder. Nhớ nhé các dev, animation không chỉ là “làm màu” mà nó còn là một phần quan trọng để tạo ra một trải nghiệm người dùng tuyệt vời. SliverAnimatedOpacity là một trong những công cụ mạnh mẽ giúp các em làm được điều đó. Cứ thực hành đi, rồi các em sẽ thấy nó “nghiện” như thế nào! 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é!

40 Đọc tiếp