Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
ViewportOffset: 'Mắt Thần' Của Flutter – Điều Khiển Mọi Cú Lướt!
23/03/2026

ViewportOffset: 'Mắt Thần' Của Flutter – Điều Khiển Mọi Cú Lướt!

Chào mấy đứa, Creyt đây! Hôm nay mình cùng giải mã một khái niệm nghe thì hàn lâm nhưng thực ra lại là 'trái tim' của mọi cú lướt mượt mà trên app Flutter của mấy đứa: ViewportOffset. 1. ViewportOffset là gì mà 'đỉnh của chóp' vậy? Tưởng tượng mấy đứa là một đạo diễn phim. Mấy đứa đang quay một cảnh cực dài, nhưng cái máy quay (hay cái viewfinder) của mấy đứa chỉ nhìn được một phần nhỏ của cảnh đó thôi, đúng không? Cái cảnh dài thượt kia chính là nội dung cuộn của mấy đứa (ví dụ, một danh sách sản phẩm dài dằng dặc trên Shopee). Còn cái 'viewfinder' nhỏ bé mà mấy đứa đang nhìn qua, đó chính là cái Viewport – cái cửa sổ nhìn thấy được trên màn hình điện thoại. Vậy thì, ViewportOffset chính là cái 'tọa độ' mà cái viewfinder của mấy đứa đang đứng trên cái cảnh phim dài đó. Nó cho mấy đứa biết chính xác 'tôi đang nhìn thấy đoạn nào của cái cảnh dài kia, từ điểm nào đến điểm nào'. Nghe hàn lâm hơn, nó là độ lệch của phần nội dung hiển thị (viewport) so với điểm gốc của toàn bộ nội dung cuộn (scrollable content). Để làm gì? Nó là chìa khóa để Flutter biết được 'Mày đang cuộn đến đâu rồi?' và từ đó render đúng các widget cần thiết. Không có nó, app của mấy đứa sẽ không thể cuộn, không thể biết khi nào cần load thêm dữ liệu (infinite scrolling), hay không thể tạo ra những hiệu ứng parallax 'ảo diệu' khi mấy đứa lướt màn hình. 2. Code Ví Dụ Minh Hoạ: Mở Mắt Thần Ra Xem! Nói suông thì khó hình dung, giờ mình 'flex' tí code để mấy đứa thấy nó hoạt động như nào nhé. Thường thì mấy đứa sẽ không tương tác trực tiếp với ViewportOffset mà sẽ thông qua ScrollPosition hoặc ScrollController. Nhưng để 'bóc tách' nó ra cho mấy đứa dễ hiểu, mình sẽ dùng NotificationListener để 'nghe lén' các sự kiện cuộn và lấy ra cái offset thần thánh nà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: 'ViewportOffset Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const ViewportOffsetScreen(), ); } } class ViewportOffsetScreen extends StatefulWidget { const ViewportOffsetScreen({super.key}); @override State<ViewportOffsetScreen> createState() => _ViewportOffsetScreenState(); } class _ViewportOffsetScreenState extends State<ViewportOffsetScreen> { double _currentOffset = 0.0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ViewportOffset: Mắt Thần Cuộn'), ), body: NotificationListener<ScrollNotification>( onNotification: (ScrollNotification notification) { // Chỉ quan tâm đến sự kiện cuộn (ScrollUpdateNotification) // hoặc khi cuộn xong (ScrollEndNotification) if (notification is ScrollUpdateNotification || notification is ScrollEndNotification) { setState(() { _currentOffset = notification.metrics.pixels; // Đây chính là ViewportOffset.pixels }); } return false; // Trả về false để cho phép các widget khác cũng nhận notification }, child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Text( 'Offset hiện tại: ${_currentOffset.toStringAsFixed(2)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), Expanded( child: ListView.builder( itemCount: 100, // Danh sách dài dằng dặc itemBuilder: (context, index) { return Container( height: 80, margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), color: index % 2 == 0 ? Colors.lightBlue[100] : Colors.blue[100], alignment: Alignment.center, child: Text( 'Item ${index + 1}', style: const TextStyle(fontSize: 20), ), ); }, ), ), ], ), ), ); } } Trong ví dụ trên, khi mấy đứa cuộn ListView, cái _currentOffset sẽ thay đổi liên tục. Nó chính là giá trị pixels của ScrollMetrics, đại diện cho ViewportOffset – cho mấy đứa biết 'cái viewfinder' đang ở đâu trên 'cảnh phim' dài 100 item kia. 3. Mẹo Hay & Best Practices Từ Creyt Đừng Đụng Trực Tiếp: ViewportOffset là một abstract class, mấy đứa sẽ không bao giờ tạo instance trực tiếp từ nó. Hãy nghĩ nó như một 'khái niệm' hơn là một 'đối tượng cụ thể'. Mấy đứa sẽ tương tác với nó thông qua ScrollPosition (mà ScrollController quản lý) hoặc thông qua ScrollMetrics trong các ScrollNotification. Nghe Lén Là Chính: Để tạo hiệu ứng động hay xử lý logic dựa trên vị trí cuộn, hãy dùng NotificationListener<ScrollNotification> (như ví dụ trên) hoặc ScrollController (để lấy offset qua controller.position.pixels). Đây là cách 'sạch sẽ' nhất để biết app đang cuộn đến đâu. Hiểu Rõ Chiều Dọc/Ngang: offset thường là giá trị dương, tăng dần khi cuộn xuống dưới (hoặc sang phải). Giá trị 0.0 thường là ở đầu danh sách. Giới Hạn Tần Suất: Các sự kiện cuộn xảy ra rất thường xuyên. Nếu mấy đứa thực hiện những tác vụ nặng bên trong onNotification hoặc addListener của ScrollController, hãy cân nhắc dùng throttle hoặc debounce để tối ưu hiệu suất, tránh làm giật lag app. 4. Ứng Dụng Thực Tế: 'Mắt Thần' Đang Ở Đâu? Mấy đứa có biết các app 'xịn xò' mà mấy đứa dùng hàng ngày đều có bóng dáng của ViewportOffset không? Instagram, Facebook, TikTok: Mấy cái feed cuộn vô tận (infinite scroll) đó, để biết khi nào cần load thêm bài viết mới, app sẽ kiểm tra ViewportOffset để xem người dùng đã cuộn gần đến cuối danh sách chưa. Hiệu Ứng Parallax: Khi mấy đứa cuộn, có những hình ảnh hoặc thành phần UI di chuyển với tốc độ khác nhau, tạo cảm giác chiều sâu. Đó chính là nhờ việc tính toán vị trí của từng phần tử dựa trên ViewportOffset và sau đó áp dụng các phép biến đổi (transform) tương ứng. Sticky Headers/Footers: Các header/footer tự động 'dính' lại ở đầu/cuối màn hình khi cuộn qua một ngưỡng nhất định. ViewportOffset giúp xác định ngưỡng đó. Load Ảnh Lazy Loading: Chỉ tải ảnh khi chúng sắp sửa hoặc đã xuất hiện trong tầm nhìn của người dùng, tiết kiệm băng thông và tăng tốc độ tải trang. 5. Thử Nghiệm & Nên Dùng Cho Case Nào? Creyt đã từng 'vật lộn' với ViewportOffset nhiều lần, đặc biệt là khi làm mấy cái hiệu ứng UI 'bay bổng' mà designer cứ đòi hỏi. Kinh nghiệm xương máu là: Nên dùng khi: Mấy đứa muốn tạo các hiệu ứng cuộn tùy chỉnh (custom scroll effects) như parallax, zoom khi cuộn. Cần biết chính xác vị trí cuộn để kích hoạt một hành động nào đó (ví dụ: hiển thị nút "Lên đầu trang" khi cuộn xuống một khoảng nhất định). Triển khai lazy loading cho hình ảnh hoặc dữ liệu khi chúng chuẩn bị vào viewport. Xây dựng các indicator cuộn tùy chỉnh (ví dụ: một thanh tiến độ cuộn). Đừng quá lạm dụng: Nếu chỉ cần cuộn đơn giản, ListView.builder hay CustomScrollView đã xử lý 'ngon lành cành đào' rồi, không cần đào sâu vào ViewportOffset chi cho phức tạp. Hãy dùng nó khi mấy đứa cần 'can thiệp' sâu hơn vào hành vi cuộn. Thử nghiệm: Mấy đứa có thể thử thay đổi ScrollNotification thành ScrollStartNotification hay OverscrollNotification để xem các loại sự kiện khác nhau và cách notification.metrics.pixels thay đổi. Hoặc thử dùng ScrollController để animateTo một offset cụ thể. Đó là cách tốt nhất để 'cảm' được nó. Tóm lại, ViewportOffset chính là 'mắt thần' của Flutter giúp app của mấy đứa 'nhìn' được mình đang cuộn đến đâu. Nắm vững nó, mấy đứa sẽ có thêm một 'siêu năng lực' để làm chủ mọi hiệu ứng cuộn và tạo ra những trải nghiệm người dùng 'mượt như lụa'. Cứ chill mà code thôi mấy đứa! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

45 Đọc tiếp
VerticalDivider: Phân làn giao diện Flutter chuẩn Gen Z!
23/03/2026

VerticalDivider: Phân làn giao diện Flutter chuẩn Gen Z!

Chào các em, lại là anh Creyt đây. Hôm nay chúng ta sẽ cùng "mổ xẻ" một anh bạn tưởng chừng đơn giản nhưng lại cực kỳ hữu ích trong Flutter, đó là VerticalDivider. Nghe tên là thấy "dọc" rồi đúng không? Chính xác! Nó là cái đường kẻ dọc mảnh mai, nhưng lại có võ đấy. VerticalDivider là gì? Để làm gì? Tưởng tượng thế này: các em đang lướt TikTok, thấy cái video của crush xong cái video quảng cáo, làm sao biết đâu là hết video này, đâu là video kia? Đơn giản là nó tự động chuyển cảnh. Nhưng trong UI của chúng ta, đôi khi cần một "dấu chấm câu" rõ ràng để người dùng biết "À, đây là hết phần A, giờ sang phần B rồi nhé!". VerticalDivider chính là "dấu chấm câu" đó, nhưng theo chiều dọc. Nói một cách "học thuật" hơn, VerticalDivider là một widget trong Flutter dùng để tạo ra một đường kẻ dọc mỏng, phân tách các nội dung khác nhau trong một bố cục theo chiều ngang (thường là trong một Row). Nó giống như cái vạch phân làn trên đường cao tốc vậy, giúp các xe (widget) không lấn sang nhau, giữ cho giao diện của chúng ta gọn gàng, dễ nhìn và có cấu trúc hơn. Tại sao lại cần nó? Đơn giản thôi: Tính thẩm mỹ và Trải nghiệm người dùng (UX). Một giao diện mà mọi thứ cứ dính chùm vào nhau thì chẳng khác nào đọc một cuốn sách không có đoạn văn, không có chương mục cả. VerticalDivider giúp tạo ra khoảng trắng thị giác (visual whitespace), dẫn dắt mắt người dùng, và làm cho các phần tử UI trở nên dễ hiểu hơn, giảm gánh nặng nhận thức. Code Ví Dụ Minh Hoạ: Cùng "vạch" một đường! Giờ thì, lý thuyết suông mãi cũng chán, phải "thực chiến" mới ngấm đúng không? Anh em mình cùng xem VerticalDivider nó hoạt động như thế nào trong một Row đơn giản 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: 'VerticalDivider Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: const Text('VerticalDivider của Creyt'), ), body: Center( child: Container( height: 150, // Chiều cao của container chứa Row color: Colors.grey[200], child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ // Phần tử thứ nhất Container( padding: const EdgeInsets.all(8.0), color: Colors.red[100], child: const Text( 'Phần A: Thông tin', style: TextStyle(fontSize: 18), ), ), // Đây rồi! Anh bạn VerticalDivider của chúng ta const VerticalDivider( color: Colors.blue, // Màu của vạch thickness: 2, // Độ dày của vạch indent: 10, // Khoảng cách từ trên xuống đến vạch endIndent: 10, // Khoảng cách từ dưới lên đến vạch ), // Phần tử thứ hai Container( padding: const EdgeInsets.all(8.0), color: Colors.green[100], child: const Text( 'Phần B: Hành động', style: TextStyle(fontSize: 18), ), ), ], ), ), ), ), ); } } Trong ví dụ trên: color: Đơn giản là màu của cái vạch. Muốn nó "chìm" thì dùng màu xám, muốn nó "nổi" thì chơi màu tươi. thickness: Độ dày của vạch. Cứ nghĩ nó là "độ đậm" của nét bút ấy. indent: "Thụt vào" từ phía trên. Tức là, vạch sẽ không bắt đầu từ mép trên cùng của Row mà sẽ cách ra một đoạn. Giống như lề trên của trang giấy vậy. endIndent: "Thụt vào" từ phía dưới. Tương tự indent, nhưng là từ mép dưới. Mẹo và Best Practices từ Creyt Đừng lạm dụng: Giống như nước hoa, xịt ít thì thơm, xịt nhiều thì hắc. Dùng VerticalDivider quá nhiều sẽ làm UI của em trông như một cái bảng tính Excel, rất rối mắt. Chỉ dùng khi thực sự cần phân tách rõ ràng các nhóm nội dung. Khoảng cách là vàng: Luôn kết hợp VerticalDivider với Padding hoặc SizedBox xung quanh nó. Một cái vạch mà dính sát vào nội dung thì nó sẽ trông rất "ngộp". Hãy cho nó không gian để "thở" nhé. Context là vua: VerticalDivider sinh ra là để nằm trong Row. Nếu em muốn chia dọc trong một Column thì đó là lúc anh bạn Divider (không có "Vertical") lên sàn. Nhớ kỹ, dọc cho ngang, ngang cho dọc. Tối ưu với Expanded/Flexible: Khi đặt VerticalDivider trong một Row có nhiều widget, hãy nghĩ đến việc dùng Expanded hoặc Flexible cho các widget xung quanh để chúng tự điều chỉnh kích thước, tránh việc VerticalDivider bị đè bẹp hoặc chiếm quá nhiều không gian. Ứng dụng thực tế: Nó ở đâu trong thế giới số? Các em có để ý các ứng dụng như: File Explorer (trên desktop): Thường có một thanh dọc chia giữa danh sách thư mục bên trái và nội dung thư mục bên phải. Các ứng dụng quản lý dự án (Jira, Trello): Đôi khi trong giao diện xem chi tiết task, có các cột thông tin được phân tách bằng đường kẻ dọc. Settings của một số ứng dụng: Khi màn hình đủ rộng, có thể chia thành 2 cột: danh mục cài đặt bên trái và chi tiết cài đặt bên phải, giữa chúng có một đường phân cách. Các thanh công cụ (Toolbar) phức tạp: Ví dụ, trong các phần mềm thiết kế đồ họa, các nhóm công cụ thường được phân tách bằng các đường dọc nhỏ. Đó chính là những nơi mà ý tưởng của VerticalDivider hoặc các thành phần tương tự được áp dụng để tăng cường tính tổ chức và dễ đọc của giao diện. Thử nghiệm của Creyt và lời khuyên Anh Creyt đã từng thử dùng Container với width mỏng và color để làm vạch ngăn cách. Nó hoạt động, nhưng VerticalDivider thì "sinh ra để làm điều đó". Nó tối ưu hơn về mặt ngữ nghĩa (semantic) và dễ dàng tùy chỉnh indent, endIndent mà không cần phải tính toán thủ công. Nên dùng cho case nào? Phân chia các nhóm chức năng trong một thanh công cụ ngang (horizontal toolbar). Tạo ranh giới rõ ràng giữa hai vùng nội dung độc lập nhưng nằm cạnh nhau trong một Row. Ví dụ: danh sách bộ lọc và danh sách kết quả, hoặc thông tin cá nhân và các nút hành động liên quan. Khi muốn tạo một layout "Master-Detail" trên màn hình lớn (ví dụ tablet) với hai panel cạnh nhau. Tóm lại, VerticalDivider là một công cụ nhỏ nhưng có võ, giúp giao diện của các em "ngăn nắp" và "có gu" hơn rất nhiều. Hãy dùng nó một cách thông minh, và UI của các em sẽ "sáng" hơn hẳn đấy! Hẹn gặp lại trong bài giảng tiếp theo nhé! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

38 Đọc tiếp
Flutter Drawer: Flex Thông Tin User Với UserAccountsDrawerHeader
23/03/2026

Flutter Drawer: Flex Thông Tin User Với UserAccountsDrawerHeader

Ê mấy đứa GenZ mê code! Hôm nay, anh Creyt sẽ cùng mấy đứa khám phá một 'góc VIP' xịn xò trong thế giới Flutter UI: thằng UserAccountsDrawerHeader. Nghe cái tên dài ngoằng vậy thôi chứ nó là 'cánh tay phải' của mấy đứa khi muốn làm cái Drawer (cái menu trượt ra từ cạnh màn hình ấy) trông thật pro và cá nhân hóa. 1. UserAccountsDrawerHeader: Cái quái gì mà "VIP" thế? Tưởng tượng mà xem, app của mấy đứa giống như một khách sạn 5 sao. Cái Drawer chính là cái hành lang dẫn đến các phòng chức năng (các màn hình khác của app). Còn UserAccountsDrawerHeader á? Nó chính là cái 'quầy lễ tân đặc biệt' hay 'phòng chờ VIP' ngay đầu hành lang đó. Nơi mấy đứa sẽ 'flex' cái danh thiếp của user hiện tại: từ cái avatar chất lừ, tên user hoành tráng, cho đến cái email 'pro' của họ. Mục đích chính là để người dùng vừa mở Drawer ra là thấy ngay 'À, đây là tài khoản của mình!', tạo cảm giác cá nhân hóa và chuyên nghiệp cực mạnh. Nói một cách đơn giản, nó là một widget được thiết kế đặc biệt để nằm ở đầu tiên của một Drawer, chuyên trị việc hiển thị thông tin tài khoản của người dùng. Nó giúp app của mấy đứa trông 'có gu' và 'nghiêm túc' hơn hẳn so với việc chỉ quăng đại mấy cái Text hay Image vào đó. 2. Code Ví Dụ Minh Hoạ: Triển ngay cho nóng! Giờ thì, lý thuyết suông hoài chán lắm! Anh em mình quẩy code luôn cho hiểu rõ ngọn ngành. Đây là một ví dụ cơ bản nhất để mấy đứa hình dung UserAccountsDrawerHeader nó sống và thở như thế nào trong một một cái Drawer. 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 Drawer Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { String _userName = "Creyt Lão Luyện"; String _userEmail = "creyt.dev@example.com"; String _userAvatarUrl = "https://picsum.photos/200/300"; // Ảnh đại diện ngẫu nhiên @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("UserAccountsDrawerHeader Demo"), ), drawer: Drawer( child: ListView( padding: EdgeInsets.zero, // Quan trọng: Đặt padding.zero để header không bị thừa khoảng trắng children: <Widget>[ UserAccountsDrawerHeader( accountName: Text( _userName, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, ), ), accountEmail: Text( _userEmail, style: TextStyle( color: Colors.white.withOpacity(0.8), ), ), currentAccountPicture: CircleAvatar( backgroundImage: NetworkImage(_userAvatarUrl), backgroundColor: Colors.white, // Màu nền cho avatar nếu ảnh chưa load ), otherAccountsPictures: <Widget>[ // Thêm các avatar khác nếu user có nhiều tài khoản GestureDetector( onTap: () { // Xử lý khi nhấn vào avatar phụ print("Tài khoản phụ 1 được nhấn!"); setState(() { _userName = "Guest Account"; _userEmail = "guest@example.com"; _userAvatarUrl = "https://picsum.photos/200/300?random=1"; }); Navigator.pop(context); // Đóng drawer sau khi đổi tài khoản }, child: CircleAvatar( backgroundImage: NetworkImage("https://picsum.photos/200/300?random=2"), ), ), CircleAvatar( backgroundImage: NetworkImage("https://picsum.photos/200/300?random=3"), ), ], onDetailsPressed: () { // Xử lý khi nhấn vào mũi tên nhỏ để xem chi tiết tài khoản print("Chi tiết tài khoản được nhấn!"); // Thường thì sẽ mở một màn hình quản lý tài khoản hoặc đổi tài khoản Navigator.pop(context); // Đóng drawer // Navigator.push(context, MaterialPageRoute(builder: (context) => const AccountDetailsScreen())); }, decoration: BoxDecoration( image: DecorationImage( image: NetworkImage("https://picsum.photos/seed/picsum/800/400"), // Ảnh nền cho header fit: BoxFit.cover, ), ), ), ListTile( leading: const Icon(Icons.home), title: const Text('Trang Chủ'), onTap: () { Navigator.pop(context); // Đóng drawer // Xử lý điều hướng đến trang chủ }, ), ListTile( leading: const Icon(Icons.settings), title: const Text('Cài Đặt'), onTap: () { Navigator.pop(context); // Đóng drawer // Xử lý điều hướng đến trang cài đặt }, ), ListTile( leading: const Icon(Icons.logout), title: const Text('Đăng Xuất'), onTap: () { Navigator.pop(context); // Đóng drawer // Xử lý đăng xuất }, ), ], ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Chào mừng, $_userName!', style: Theme.of(context).textTheme.headlineMedium, ), Text( 'Email: $_userEmail', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Mở drawer nếu muốn Scaffold.of(context).openDrawer(); }, child: const Text('Mở Drawer'), ), ], ), ), ); } } Trong đoạn code trên, mấy đứa thấy rõ ràng các thuộc tính quan trọng của UserAccountsDrawerHeader: accountName: Tên của user, thường là một widget Text. accountEmail: Email của user, cũng là một widget Text. currentAccountPicture: Cái avatar chính của user, thường là một CircleAvatar bọc ImageProvider (NetworkImage, AssetImage, FileImage...). otherAccountsPictures: Một List<Widget> để hiển thị các tài khoản phụ, rất tiện cho mấy app có tính năng đổi tài khoản nhanh (như Gmail). onDetailsPressed: Một callback VoidCallback khi user nhấn vào mũi tên nhỏ bên cạnh thông tin tài khoản. Thường dùng để mở màn hình quản lý tài khoản hoặc danh sách tài khoản để đổi. decoration: Cho phép mấy đứa trang trí thêm cho cái header, ví dụ như thêm ảnh nền (Background Image) cho nó thêm phần lung linh, huyền ảo. 3. Mẹo Vặt (Best Practices) từ Creyt Lão Luyện: Giờ là lúc anh Creyt 'bóc phốt' vài chiêu hay ho để mấy đứa dùng UserAccountsDrawerHeader cho nó 'chuẩn bài', không bị 'quê' hay 'bug vặt': Quản lý State cho thông tin User (Dynamic Data): Đừng bao giờ hardcode (ghi trực tiếp) tên hay email user như anh ví dụ nhé! Trong thực tế, thông tin này phải lấy từ database, API, hoặc một service quản lý authentication. Nên mấy đứa cần dùng StatefulWidget (như anh đã dùng trong ví dụ) hoặc các giải pháp quản lý state xịn sò hơn như Provider, Bloc, Riverpod để cập nhật thông tin user khi họ đăng nhập/đăng xuất hoặc thay đổi profile. Ảnh Avatar: Luôn đảm bảo ảnh avatar được load 'mượt mà'. Nếu dùng ảnh từ mạng (NetworkImage), hãy cân nhắc dùng các package hỗ trợ caching ảnh như cached_network_image để tránh phải tải lại mỗi lần mở Drawer, vừa tiết kiệm data vừa nhanh hơn. Và nhớ, luôn có một placeholder hoặc backgroundColor cho CircleAvatar phòng trường hợp ảnh chưa load kịp hoặc bị lỗi. padding: EdgeInsets.zero cho ListView: Cái này quan trọng nè! Nếu mấy đứa bọc UserAccountsDrawerHeader trong một ListView mà không set padding: EdgeInsets.zero cho ListView đó, thì cái header sẽ bị thừa một khoảng trắng ở trên đầu, trông rất 'phèn'. padding: EdgeInsets.zero giúp nó 'ăn khớp' hoàn toàn với cạnh trên của Drawer. Xử lý onDetailsPressed: Đây là một điểm vàng để tăng trải nghiệm người dùng. Khi họ nhấn vào mũi tên này, hãy đưa họ đến một màn hình quản lý tài khoản hoặc một popup/bottom sheet để họ có thể đổi tài khoản hoặc xem chi tiết. Đừng để nó 'trơ trơ' không làm gì cả. Decoration Background: Tận dụng decoration để làm đẹp cái header. Có thể là một LinearGradient màu sắc, hoặc một DecorationImage với ảnh nền phù hợp. Tuy nhiên, nhớ là ảnh nền nên có độ tương phản tốt với màu chữ của accountName và accountEmail để dễ đọc nhé! Accessibility (Khả năng tiếp cận): Đảm bảo kích thước chữ đủ lớn, màu sắc tương phản tốt. Nếu có thể, thêm tooltip cho các CircleAvatar phụ để người dùng biết họ đang click vào cái gì. 4. Ứng dụng thực tế: Ai cũng dùng, sao mình không dùng? Mấy đứa có thấy quen không khi mở Gmail, Google Drive, hay thậm chí là một số ứng dụng ngân hàng? Yesss! Chính là nó đó. UserAccountsDrawerHeader hoặc một phiên bản tùy chỉnh của nó được dùng rộng rãi trong các ứng dụng có tính năng đăng nhập, nơi người dùng có thể có nhiều tài khoản hoặc cần xem nhanh thông tin của mình. Gmail/Google Drive: Mở Drawer ra là thấy ngay avatar, tên, email của tài khoản Google hiện tại, và có thể chuyển đổi nhanh sang các tài khoản Google khác. Facebook/Instagram: Mặc dù không dùng Drawer theo kiểu truyền thống nhiều, nhưng các ứng dụng này vẫn có một khu vực tương tự để hiển thị profile user và các tùy chọn liên quan. Các app quản lý công việc (Trello, Asana): Thường dùng để hiển thị thông tin người dùng và chuyển đổi giữa các không gian làm việc (workspace) hoặc tài khoản. 5. Nên dùng cho Case nào và kinh nghiệm xương máu của Creyt: Với kinh nghiệm 'cầm chuột' bao năm của anh, UserAccountsDrawerHeader là lựa chọn 'đỉnh của chóp' khi mấy đứa đang xây dựng một ứng dụng: Có hệ thống tài khoản người dùng (User Authentication): Rõ ràng rồi, có user thì mới có thông tin để hiển thị chứ. Cần một Drawer để điều hướng chính (Main Navigation Drawer): Nếu app của mấy đứa dùng Drawer làm menu chính, thì việc có một header cá nhân hóa sẽ làm tăng trải nghiệm người dùng lên đáng kể. Hỗ trợ nhiều tài khoản (Multiple Accounts): otherAccountsPictures sinh ra là để làm điều này. Giúp user chuyển đổi tài khoản 'trong một nốt nhạc'. Muốn app trông chuyên nghiệp và 'xịn sò': Một cái header được thiết kế tốt luôn tạo ấn tượng đầu tiên mạnh mẽ. Thử nghiệm đã từng: Anh từng làm một app quản lý tài chính cá nhân, ban đầu chỉ quăng mỗi cái Text tên user lên đầu Drawer. Khách hàng xài xong kêu 'Sao nhìn nó cứ thiếu thiếu, không có cảm giác là app của mình vậy anh?'. Sau đó, anh vứt ngay UserAccountsDrawerHeader vào, thêm avatar, email, đổi ảnh nền cho nó 'vibe' chút. Kết quả là khách hàng 'ồ à' khen lấy khen để, bảo 'Đúng cái chất em cần!'. Từ đó mới thấy, đôi khi những chi tiết nhỏ nhưng được chăm chút kỹ lưỡng lại mang lại hiệu quả cực lớn về trải nghiệm người dùng. Vậy đó, UserAccountsDrawerHeader không chỉ là một widget đơn thuần, nó là một 'statement' về sự chuyên nghiệp và quan tâm đến người dùng của mấy đứa. Hãy dùng nó một cách thông minh và sáng tạo nhé! Chúc mấy đứa code mượt! 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
UnmanagedRestorationScope: "Khu Vực Cấm Lưu" Trạng Thái Trong Flutter
23/03/2026

UnmanagedRestorationScope: "Khu Vực Cấm Lưu" Trạng Thái Trong Flutter

1. UnmanagedRestorationScope là cái gì mà nghe "ghê" vậy? Tưởng tượng thế này, app của các em giống như một game console cũ. Mỗi khi các em chơi xong, tắt máy, rồi bật lại, game sẽ bắt đầu lại từ đầu, đúng không? Nhưng có những game hiện đại hơn, nó có chức năng "save game" tự động, tức là nó sẽ ghi nhớ vị trí, điểm số, trang bị của các em ngay cả khi các em tắt máy. Trong Flutter, cái chức năng "save game" này chính là State Restoration (Khôi phục trạng thái). Khi các em thoát app (nhưng không phải force close hoàn toàn, ví dụ như hệ điều hành tự 'giết' app để giải phóng RAM), rồi mở lại, Flutter sẽ cố gắng 'khôi phục' lại trạng thái cuối cùng của app. Ví dụ, nếu các em đang ở màn hình thứ 3, với một text field đã điền sẵn, thì khi mở lại, nó sẽ quay về màn hình thứ 3 đó và giữ nguyên nội dung text field. UnmanagedRestorationScope giống như một 'khu vực cấm lưu' trong cái game đó vậy. Bất cứ thứ gì nằm trong phạm vi của UnmanagedRestorationScope sẽ KHÔNG được lưu lại trạng thái. Khi app được khôi phục, mọi thứ trong khu vực này sẽ trở về trạng thái ban đầu, như chưa từng có chuyện gì xảy ra. Nghe cool không? Để làm gì? Đơn giản là có những thứ các em KHÔNG muốn nó được lưu lại. Ví dụ, một cái loading spinner, một thông báo tạm thời, hoặc một cái form mà các em muốn nó luôn trống trơn khi người dùng mở lại. Nó giúp các em kiểm soát chặt chẽ hơn trải nghiệm người dùng, đảm bảo những gì cần "fresh" thì phải fresh. 2. Code Ví Dụ minh hoạ rõ ràng, chuẩn kiến thức. Nói có sách, mách có code! Đây là ví dụ kinh điển để các em thấy sự khác biệt giữa một widget được quản lý bởi RestorationScope và một widget bị UnmanagedRestorationScope 'từ chối' quản lý. 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( restorationScopeId: 'app', // Quan trọng để bật State Restoration cho toàn bộ app title: 'Creyt\'s Restoration Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with RestorationMixin { // Biến này sẽ được khôi phục final RestorableInt _managedCounter = RestorableInt(0); // Biến này KHÔNG được khôi phục vì nó nằm trong UnmanagedRestorationScope int _unmanagedCounter = 0; @override String? get restorationId => 'homePage'; // ID cho RestorationMixin @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { // Đăng ký biến _managedCounter để nó được khôi phục registerForRestoration(_managedCounter, 'managedCounter'); } @override void dispose() { _managedCounter.dispose(); // Đừng quên dispose Restorable-objects super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('UnmanagedRestorationScope Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ // --- Widget được quản lý bởi RestorationScope --- const Text( 'Managed Counter (sẽ được khôi phục):', style: TextStyle(fontWeight: FontWeight.bold), ), Text( '${_managedCounter.value}', style: Theme.of(context).textTheme.headlineMedium, ), ElevatedButton( onPressed: () { setState(() { _managedCounter.value++; }); }, child: const Text('Tăng Managed Counter'), ), const SizedBox(height: 40), // --- Widget bị UnmanagedRestorationScope "từ chối" khôi phục --- UnmanagedRestorationScope( bucket: null, // Rất quan trọng: truyền null để vô hiệu hóa khôi phục child: Column( children: <Widget>[ const Text( 'Unmanaged Counter (sẽ KHÔNG được khôi phục):', style: TextStyle(fontWeight: FontWeight.bold), ), Text( '$_unmanagedCounter', style: Theme.of(context).textTheme.headlineMedium, ), ElevatedButton( onPressed: () { setState(() { _unmanagedCounter++; }); }, child: const Text('Tăng Unmanaged Counter'), ), ], ), ), ], ), ), ); } } Cách test: Các em chạy app, tăng cả hai counter lên một vài giá trị. Sau đó, đừng nhấn nút back, mà hãy đưa app xuống nền (minimize) và sau đó 'giết' app từ trình quản lý ứng dụng của điện thoại (hoặc dùng flutter run --trace-startup rồi nhấn r để hot restart với flutter run bình thường, hoặc dùng IDE để stop và run lại nhưng đảm bảo app đã bị killed hoàn toàn). Khi mở lại app, các em sẽ thấy Managed Counter vẫn giữ nguyên giá trị cũ, còn Unmanaged Counter đã reset về 0. Thấy sức mạnh của 'khu vực cấm lưu' chưa? 3. Một vài mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế. Anh Creyt có vài chiêu để các em nhớ lâu và dùng cho đúng case này: Dùng khi nào cần 'tươi mới': Nếu có một phần UI hoặc dữ liệu mà các em muốn nó luôn bắt đầu lại từ đầu mỗi khi app được mở lại (sau khi bị hệ điều hành 'giết' đi), thì UnmanagedRestorationScope là chân ái. Không lạm dụng: Đừng bao giờ quăng UnmanagedRestorationScope vào mọi chỗ. Hầu hết các trạng thái trong app của các em đều muốn được khôi phục để mang lại trải nghiệm liền mạch cho người dùng. Chỉ dùng nó khi thật sự cần ngăn chặn việc khôi phục. Hiểu rõ bucket: null: Điểm mấu chốt là các em phải truyền bucket: null vào UnmanagedRestorationScope. Nếu không, nó sẽ không hoạt động như mong đợi đâu. Nó giống như nói "không có cái xô nào để lưu trạng thái ở đây cả!" Phân biệt với RestorationScope: RestorationScope là để tạo một phạm vi khôi phục mới với một restorationId cụ thể, trong khi UnmanagedRestorationScope là để ngăn chặn việc khôi phục trong một phạm vi đã có (hoặc một phạm vi mặc định). Thường dùng cho các transient UI: Ví dụ như các AlertDialog, BottomSheet tạm thời, các widget hiển thị thông báo ngắn hạn, hoặc các form nhập liệu mà các em muốn luôn trống khi người dùng mở lại. 4. Văn phong học thuật sâu của anh Creyt, dạy dễ hiểu tuyệt đối. Nhìn sâu hơn chút, các em sẽ thấy Flutter dùng một hệ thống gọi là RestorationBucket để lưu trữ và khôi phục trạng thái. Mỗi RestorationScope sẽ tạo ra một RestorationBucket riêng, được định danh bằng restorationId. Khi app bị 'giết' và khởi động lại, Flutter sẽ tìm kiếm các RestorationBucket này, đọc dữ liệu đã lưu và 'bơm' lại vào các RestorableProperty (như RestorableInt, RestorableString,...). UnmanagedRestorationScope với bucket: null về cơ bản là đang 'cắt đứt' cái sợi dây liên kết giữa phần UI bên trong nó với hệ thống RestorationBucket của cha mẹ nó. Nó nói rằng: 'Phần này tự quản lý, không cần hệ thống lưu trữ chung đâu'. Vì vậy, khi hệ thống khôi phục trạng thái được kích hoạt, nó sẽ 'nhảy' qua phần này và không hề đụng chạm gì đến các trạng thái bên trong nó. Mọi thứ bên trong UnmanagedRestorationScope sẽ được khởi tạo lại như lần đầu tiên widget đó được dựng lên, bất kể trước đó nó có trạng thái gì. 5. Ví dụ thực tế các ứng dụng/website đã ứng dụng. Trong thế giới thực, các em sẽ gặp nó ở đâu? Ứng dụng ngân hàng/tài chính: Khi các em mở lại app sau một thời gian dài, màn hình đăng nhập hoặc các trường nhập liệu nhạy cảm (như mã PIN, số tiền giao dịch) luôn luôn phải trống. Không ai muốn thông tin cá nhân bị khôi phục sẵn đâu, đúng không? Game mobile: Màn hình "New Game" hoặc "Start Over". Nếu người chơi đang ở giữa một màn chơi, thoát ra rồi vào lại, họ có thể muốn tiếp tục. Nhưng nếu họ chọn "New Game", thì mọi thứ phải được reset về 0, không dính dáng gì đến trạng thái cũ. Ứng dụng ghi chú/editor: Các cửa sổ soạn thảo tạm thời, hoặc các trường tìm kiếm nhanh. Nếu các em đang gõ dở một cái gì đó vào ô tìm kiếm, thoát app rồi vào lại, có thể các em muốn ô tìm kiếm đó trống để bắt đầu tìm kiếm mới. Các pop-up, dialog tạm thời: Một SnackBar thông báo "Đã lưu thành công!" hiển thị rồi biến mất. Khi app được khôi phục, các em không muốn cái SnackBar đó hiện lại nữa. 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 state restoration khi phát triển một app ghi chú. Ban đầu, anh cứ nghĩ 'cứ lưu tất tần tật' là tốt. Nhưng sau đó nhận ra, có những popup xác nhận xóa, hay những trường nhập liệu tạm thời, nếu được khôi phục lại sẽ gây khó chịu cho người dùng. Ví dụ, người dùng đang ở màn hình xác nhận xóa một ghi chú, thoát app, rồi vào lại, tự nhiên thấy lại cái popup xác nhận xóa đó mà không hiểu tại sao. Rất khó chịu! Từ đó, anh học được rằng: Nên dùng cho: Các widget hiển thị thông tin transient (tạm thời) như SnackBar, Toast, AlertDialog (nếu không muốn chúng xuất hiện lại sau khôi phục). Các form nhập liệu nhạy cảm hoặc cần được reset hoàn toàn khi người dùng mở lại app (ví dụ: form đăng nhập, form tạo mới). Các UI elements chỉ có giá trị trong phiên làm việc hiện tại và không cần duy trì qua các lần khởi động lại app. Các widget chứa trạng thái mà việc khôi phục chúng sẽ dẫn đến trải nghiệm người dùng không mong muốn hoặc không an toàn. Không nên dùng cho: Các trạng thái quan trọng của ứng dụng (ví dụ: dữ liệu người dùng, vị trí hiện tại trong một danh sách dài, trạng thái của các tab điều hướng). Bất kỳ trạng thái nào mà người dùng mong đợi sẽ được giữ nguyên khi quay lại ứng dụng. Hãy luôn đặt mình vào vị trí người dùng để quyết định. Nếu họ sẽ bất ngờ hoặc khó chịu khi thấy trạng thái đó được khôi phục, thì UnmanagedRestorationScope chính là cứu tinh của các em! 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
UnconstrainedBox: Giải phóng Layout Flutter, Bung Lụa Kích Thước!
23/03/2026

UnconstrainedBox: Giải phóng Layout Flutter, Bung Lụa Kích Thước!

Chào các "dev-er" tương lai của gen Z! Anh Creyt lại lên sóng đây, hôm nay chúng ta sẽ cùng "mổ xẻ" một "bí kíp" trong Flutter mà nghe tên thì hơi "khoa học viễn tưởng", nhưng thực chất lại là "thần đèn" giải phóng cho layout của các bạn: UnconstrainedBox. 1. UnconstrainedBox là gì mà "ghê gớm" vậy? (Giải thích theo Gen Z) Nói đơn giản thế này: Trong Flutter, mọi widget đều sống trong một "khuôn khổ" nhất định do cha mẹ (parent widget) nó đặt ra. Giống như bạn muốn mua đôi giày "chất chơi" size 42, nhưng mẹ bạn (parent) lại bảo: "Đi size 38 thôi, chân con nhỏ mà!". Kết quả là bạn phải đi đôi giày chật chội, khó chịu. UnconstrainedBox chính là "ông chú chơi hệ thoải mái là nhất" của bạn. Khi bạn đặt một widget con vào trong UnconstrainedBox, ông chú này sẽ nói với widget con: "Con ơi, con cứ là chính con đi! Con muốn to bao nhiêu, con cứ to bấy nhiêu!" Tức là, UnconstrainedBox sẽ loại bỏ tất cả các ràng buộc kích thước mà cha mẹ nó áp đặt lên con của nó. Widget con sẽ được phép tính toán kích thước "tự nhiên" (intrinsic size) hoặc kích thước mong muốn của nó mà không bị giới hạn. Để làm gì? Để giải cứu những widget con bị "bóp méo", "co rúm" hoặc không thể hiển thị đúng kích thước mong muốn chỉ vì cha mẹ nó quá "khó tính" về kích thước. Nó cho phép bạn "phá vỡ quy tắc" layout truyền thống một cách có kiểm soát. Điểm mấu chốt cần nhớ: UnconstrainedBox chỉ giải phóng cho con của nó, còn bản thân UnconstrainedBox vẫn sẽ cố gắng "ngoan ngoãn" tuân thủ các ràng buộc kích thước từ cha mẹ của chính nó. Nếu đứa con "bung lụa" quá đà, nó có thể tràn ra ngoài phạm vi của UnconstrainedBox đó nha! 2. Code Ví Dụ Minh Hoạ: "Thằng nhóc" được bung lụa! Giờ thì anh em mình cùng xem "thần đèn" này hoạt động như thế nào qua vài dòng code "thần thánh" nhé. Ta sẽ có một Container muốn có kích thước 200x50, nhưng bị một Container cha có kích thước 100x100 giới hạ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( home: Scaffold( appBar: AppBar(title: const Text('UnconstrainedBox Demo của Creyt')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Ví dụ 1: Container bị giới hạn (mặc định) const Text('1. Container bị giới hạn (mặc định):', style: TextStyle(fontWeight: FontWeight.bold)), Container( color: Colors.red.shade100, // Màu nền của Container cha width: 100, // Cha chỉ cho con 100 chiều rộng height: 100, alignment: Alignment.center, margin: const EdgeInsets.all(10), child: Container( color: Colors.red, // Container con muốn 200x50 width: 200, // Mong muốn 200, nhưng sẽ bị giới hạn bởi cha (100) height: 50, child: const Text('200x50 (bị giới hạn)', style: TextStyle(color: Colors.white)), ), ), const SizedBox(height: 30), // Ví dụ 2: Với UnconstrainedBox - Con được tự do! const Text('2. Với UnconstrainedBox: Con được tự do!', style: TextStyle(fontWeight: FontWeight.bold)), Container( color: Colors.blue.shade100, // Màu nền của Container cha width: 100, // Cha vẫn chỉ cho 100 chiều rộng height: 100, alignment: Alignment.center, margin: const EdgeInsets.all(10), child: UnconstrainedBox( // Thần đèn đây rồi! Giải phóng cho đứa con! child: Container( color: Colors.blue, // Giờ thì nó bung lụa được 200x50 rồi! width: 200, height: 50, child: const Text('200x50 (đã bung lụa)', style: TextStyle(color: Colors.white)), ), ), ), const SizedBox(height: 30), // Ví dụ 3: UnconstrainedBox với FittedBox (Vừa bung lụa, vừa được scale cho vừa) const Text('3. UnconstrainedBox + FittedBox: Bung lụa rồi scale!', style: TextStyle(fontWeight: FontWeight.bold)), Container( color: Colors.green.shade100, // Màu nền của Container cha width: 100, height: 100, alignment: Alignment.center, margin: const EdgeInsets.all(10), child: FittedBox( // FittedBox sẽ scale đứa con tự do của UnconstrainedBox fit: BoxFit.contain, // Scale sao cho vừa vặn trong không gian cha child: UnconstrainedBox( child: Container( color: Colors.green, width: 200, height: 50, child: const Text('200x50 (bung lụa & scale)', style: TextStyle(color: Colors.white)), ), ), ), ), ], ), ), ), ); } } Giải thích code: Ví dụ 1: Container màu đỏ con muốn width: 200, nhưng Container cha chỉ có width: 100. Kết quả là Container con bị giới hạn chỉ còn width: 100. Nó không thể "bung lụa" được. Ví dụ 2: Ta bọc Container con màu xanh vào UnconstrainedBox. Mặc dù Container cha vẫn chỉ có width: 100, nhưng UnconstrainedBox đã "phá luật" cho Container con. Giờ đây, Container con được phép hiển thị đúng width: 200 mà nó mong muốn. Các bạn sẽ thấy nó tràn ra ngoài Container cha. Ví dụ 3: Đây là một combo "đỉnh của chóp"! UnconstrainedBox cho Container con màu xanh lá "bung lụa" kích thước 200x50. Sau đó, FittedBox sẽ "bắt" cái Container đã bung lụa đó và co giãn nó lại cho vừa vặn trong không gian 100x100 của cha, mà vẫn giữ đúng tỷ lệ. Quá "ảo diệu"! 3. Mẹo (Best Practices) từ "lão làng" Creyt Ghi nhớ thần chú: "UnconstrainedBox = 'Con ơi, con cứ là chính con đi, đừng sợ ai giới hạn!'" Nó là tấm vé tự do cho widget con. Cẩn thận với Overflow: Khi con được tự do, nó có thể "bành trướng" quá đà và tràn ra ngoài màn hình, gây ra lỗi Overflow (cái vạch vàng đen xấu xí đó). Luôn kiểm tra kỹ sau khi dùng nhé. Combo "bất bại": Thường thì UnconstrainedBox hay đi kèm với OverflowBox (để cho phép con tràn ra ngoài mà không bị cắt) hoặc FittedBox (để sau khi con bung lụa, ta lại scale nó cho vừa vặn một cách thông minh). Đây là những "chiến hữu" cực kỳ ăn ý. Debug layout "thần tốc": Nếu bạn đang đau đầu vì một widget con nào đó cứ bị co rúm, không hiển thị đúng kích thước mong muốn, hãy thử bọc nó trong UnconstrainedBox để xem nó có thực sự muốn kích thước lớn hơn không. Đây là một mẹo debug cực kỳ hiệu quả! 4. Văn phong học thuật sâu của anh Creyt: Hiểu bản chất "công lực"! Trong hệ thống layout của Flutter, mỗi widget khi được render đều trải qua một quá trình "truyền tải ràng buộc" (constraint propagation) từ cha xuống con, và sau đó "truyền tải kích thước" (size propagation) từ con lên cha. UnconstrainedBox can thiệp vào giai đoạn đầu tiên. Nó nhận các ràng buộc từ cha của nó, nhưng khi truyền xuống cho con, nó sẽ truyền một BoxConstraints với minWidth: 0, maxWidth: double.infinity, minHeight: 0, maxHeight: double.infinity. Điều này có nghĩa là đứa con được hoàn toàn tự do chọn kích thước mà nó mong muốn. Sự khác biệt giữa UnconstrainedBox và OverflowBox là gì? UnconstrainedBox cho phép con của nó chọn kích thước mà không bị ràng buộc. OverflowBox thì khác, nó vẫn giữ kích thước của chính nó theo ràng buộc của cha, nhưng cho phép con của nó vẽ ra ngoài cái hộp đó. Hay nói cách khác, UnconstrainedBox thay đổi cách con được đo kích thước, còn OverflowBox thay đổi cách con được đặt vị trí và vẽ so với cha. 5. Ví dụ thực tế các ứng dụng/website đã ứng dụng Trong thế giới thực, UnconstrainedBox không phải là widget bạn dùng "nhan nhản" mọi nơi, nhưng nó cực kỳ quan trọng trong những trường hợp đặc biệt: Custom UI Components: Khi bạn xây dựng các widget UI phức tạp, ví dụ như một Tooltip (chú thích nhỏ hiện ra khi di chuột/chạm) hoặc một Dropdown Menu tùy chỉnh. Nội dung bên trong Tooltip hoặc Dropdown thường cần hiển thị theo kích thước tự nhiên của nó mà không bị giới hạn bởi không gian nhỏ hẹp của widget cha. UnconstrainedBox giúp nội dung này "bung lụa" đúng kích thước, sau đó Overlay hoặc Positioned sẽ lo phần vị trí. Icon với kích thước cố định: Đôi khi bạn muốn một Icon hoặc một Image nhỏ luôn giữ kích thước pixel cố định của nó, bất kể nó được đặt trong một Row hay Column có Expanded đang cố gắng co giãn nó. Bọc nó trong UnconstrainedBox sẽ đảm bảo kích thước tự nhiên của nó được tôn trọng. Biểu đồ hoặc đồ thị tùy chỉnh: Trong các thư viện vẽ biểu đồ, đôi khi các điểm dữ liệu hoặc nhãn cần được vẽ ở một vị trí và kích thước chính xác mà không bị ảnh hưởng bởi layout xung quanh. UnconstrainedBox có thể được dùng để tạo một "khu vực vẽ tự do" cho các thành phần này. 6. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào Anh Creyt đã từng "đụng độ" UnconstrainedBox khi làm một thanh AppBar tùy chỉnh. Trong AppBar đó có một Text widget cần hiển thị đầy đủ, không bị cắt. Tuy nhiên, Text đó lại nằm trong một Row với các Action button khác, và đôi khi Row này có thể hết chỗ, khiến Text bị ellipsis (hiện ...). Bằng cách bọc Text trong UnconstrainedBox (và sau đó là FittedBox), anh Creyt đã cho phép Text tính toán kích thước đầy đủ của nó, rồi FittedBox sẽ co giãn nó lại cho vừa, đảm bảo nội dung luôn hiển thị đầy đủ mà không bị cắt. Khi nào nên dùng UnconstrainedBox? Khi bạn muốn một widget con hoàn toàn bỏ qua các ràng buộc kích thước từ cha mẹ nó và render ở kích thước tự nhiên của nó (hoặc kích thước bạn chỉ định rõ). Khi bạn cần một widget con có thể tràn ra ngoài phạm vi của UnconstrainedBox (lúc này thường kết hợp với OverflowBox để kiểm soát việc tràn, hoặc chấp nhận overflow nếu đó là ý đồ của bạn). Khi bạn muốn dùng FittedBox để scale một widget con đã được UnconstrainedBox "giải phóng" về kích thước tự nhiên, sau đó FittedBox sẽ co giãn nó cho vừa vặn trong không gian có sẵn. Để debug các vấn đề layout khi bạn nghi ngờ một widget con đang bị ràng buộc kích thước không mong muốn. Lời khuyên "thực chiến": UnconstrainedBox là một công cụ mạnh mẽ, nhưng hãy dùng nó có chừng mực. Việc "giải phóng" quá nhiều widget có thể dẫn đến layout khó kiểm soát và dễ gây overflow. Luôn tự hỏi: "Có cách nào khác để đạt được hiệu ứng này mà không cần phá vỡ ràng buộc không?" Nếu câu trả lời là không, hoặc cách khác quá phức tạp, thì UnconstrainedBox chính là "cứu tinh" của bạn! Hy vọng với bài giảng này, các bạn gen Z đã nắm rõ "công lực" của UnconstrainedBox và biết cách sử dụng nó để "phá đảo" mọi layout trong Flutter nhé! Hẹn gặp lại trong những buổi học "bùng cháy" 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é!

53 Đọc tiếp
TweenSequence: DJ Phối Nhạc Cho Animation Flutter Của Bạn!
23/03/2026

TweenSequence: DJ Phối Nhạc Cho Animation Flutter Của Bạn!

Chào các đệ tử của anh Creyt! Hôm nay, chúng ta sẽ "bóc tách" một khái niệm mà nhiều bạn trẻ hay lúng túng khi muốn tạo ra những animation "xịn xò" hơn là chỉ đi từ A đến B một cách nhàm chán. Đó chính là TweenSequence – và cái "ruột gan" của nó là TweenSequenceState. Nghe tên có vẻ hàn lâm, nhưng thật ra nó là một ông DJ cực chất, giúp bạn phối nhạc cho các hiệu ứng chuyển động trong app Flutter của mình đấy! 1. TweenSequence là gì và để làm gì? (Và TweenSequenceState là "trái tim" của nó) Tưởng tượng thế này: Bạn muốn làm một cái animation. Nếu chỉ là đổi màu từ đỏ sang xanh, hay phóng to từ 0 lên 100, thì một cái Tween đơn giản là đủ. Nó như một chuyến bay thẳng từ Hà Nội vào Sài Gòn vậy. Easy game. Nhưng đời đâu phải lúc nào cũng đơn giản đúng không? Giờ bạn muốn cái app của mình nó 'bay' thế này: Đầu tiên, cái nút nó rung nhẹ một tí, xong nhảy lên một đoạn, rồi phát sáng rực rỡ, xong mới hạ cánh xuống vị trí cuối cùng. Đó, một loạt các hành động nối tiếp nhau, mỗi hành động lại có một 'cá tính' riêng (tốc độ, kiểu chuyển động, thời gian). Nếu dùng Tween đơn lẻ, bạn sẽ phải ngồi canh thời gian, tính toán tùm lum, đau đầu lắm. Đây chính là lúc TweenSequence xuất hiện như một "vị cứu tinh". Nó cho phép bạn xâu chuỗi nhiều Tween nhỏ lại với nhau thành một chuỗi animation liền mạch. Mỗi Tween nhỏ trong chuỗi được gọi là một TweenSequenceItem. Anh em cứ hình dung TweenSequence như một đạo diễn tài ba, sắp xếp từng cảnh quay (từng TweenSequenceItem) một cách hợp lý để tạo nên một bộ phim (animation) hoàn chỉnh. Còn TweenSequenceState? À, đó chính là "bộ não" hay "trái tim" của ông đạo diễn TweenSequence này đấy. Nó là cái thứ đứng sau cánh gà, âm thầm quản lý từng giai đoạn của chuỗi, tính toán xem ở thời điểm hiện tại thì cái Tween nào đang chạy, nó chạy được bao nhiêu phần trăm rồi, và trả về giá trị tương ứng. Chúng ta thường không tương tác trực tiếp với TweenSequenceState mà là thông qua TweenSequence thôi, nhưng biết nó tồn tại và làm nhiệm vụ gì thì sẽ giúp bạn hiểu sâu hơn về cách animation của Flutter hoạt động. 2. Code Ví Dụ Minh Hoạ: "Nút Bấm Nhảy Múa" Để dễ hình dung, anh em mình cùng làm một cái nút bấm nó 'nhảy múa' theo nhiều giai đoạn nhé. Nút này sẽ: Nhỏ lại một chút. Phóng to ra một chút. Đổi màu từ xanh sang vàng. Cuối cùng là trở về trạng thái ban đầu. Chúng ta sẽ dùng TweenSequence để điều khiển cả kích thước và màu sắc. import 'package:flutter/material.dart'; class TweenSequenceDemo extends StatefulWidget { const TweenSequenceDemo({super.key}); @override State<TweenSequenceDemo> createState() => _TweenSequenceDemoState(); } class _TweenSequenceDemoState extends State<TweenSequenceDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _sizeAnimation; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 4), // Tổng thời gian animation ); // Định nghĩa sequence cho kích thước _sizeAnimation = TweenSequence<double>([ TweenSequenceItem<double>( tween: Tween<double>(begin: 1.0, end: 0.8), // Giai đoạn 1: Nhỏ lại weight: 20.0, // Chiếm 20% tổng thời gian ), TweenSequenceItem<double>( tween: Tween<double>(begin: 0.8, end: 1.2), // Giai đoạn 2: Phóng to weight: 30.0, // Chiếm 30% tổng thời gian ), TweenSequenceItem<double>( tween: Tween<double>(begin: 1.2, end: 1.0), // Giai đoạn 3: Về kích thước ban đầu weight: 50.0, // Chiếm 50% tổng thời gian ), ]).animate(_controller); // Định nghĩa sequence cho màu sắc _colorAnimation = TweenSequence<Color?>([ TweenSequenceItem<Color?>( tween: ColorTween(begin: Colors.blue, end: Colors.red), // Giai đoạn 1: Xanh -> Đỏ weight: 50.0, // Chiếm 50% tổng thời gian ), TweenSequenceItem<Color?>( tween: ColorTween(begin: Colors.red, end: Colors.green), // Giai đoạn 2: Đỏ -> Xanh lá weight: 50.0, // Chiếm 50% tổng thời gian ), ]).animate(_controller); // Lặp lại animation sau khi hoàn thành _controller.repeat(reverse: true); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('TweenSequence Demo')), body: Center( child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: _sizeAnimation.value, child: Container( width: 100, height: 100, decoration: BoxDecoration( color: _colorAnimation.value, borderRadius: BorderRadius.circular(20), ), child: Center( child: Text( 'Tap Me!', style: TextStyle( color: Colors.white, fontSize: 16 * _sizeAnimation.value, // Font size cũng thay đổi theo scale ), ), ), ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { if (_controller.isAnimating) { _controller.stop(); } else { _controller.repeat(reverse: true); } }, child: Icon(_controller.isAnimating ? Icons.pause : Icons.play_arrow), ), ); } } void main() { runApp(const MaterialApp(home: TweenSequenceDemo())); } Trong ví dụ trên: Chúng ta tạo ra hai chuỗi TweenSequence độc lập: một cho _sizeAnimation (kích thước) và một cho _colorAnimation (màu sắc). Mỗi TweenSequenceItem có một tween riêng và một weight. weight ở đây không phải là trọng lượng, mà là "tỷ lệ thời gian" mà tween đó chiếm trong tổng thời gian của TweenSequence. Tổng weight của tất cả TweenSequenceItem trong một TweenSequence sẽ được dùng để tính toán tỷ lệ. Ví dụ, nếu tổng weight là 100, thì weight: 20 có nghĩa là tween đó chạy trong 20% tổng thời gian của _controller. 3. Mẹo (Best Practices) từ ông Creyt để "quẩy" với TweenSequence weight là chìa khóa, không phải thời gian tuyệt đối: Nhớ nhé, weight là tỷ lệ, không phải số giây. Tổng weight của tất cả TweenSequenceItem trong một TweenSequence sẽ được chuẩn hóa. Ví dụ, nếu bạn có 3 item với weight là 1, 2, 1, thì tổng là 4. Item 1 sẽ chiếm 1/4 thời gian, item 2 chiếm 2/4, item 3 chiếm 1/4. Đừng cố gắng làm tổng weight luôn bằng 100 nếu không cần thiết, cứ để nó tự tính toán. Kết hợp Curve cho mỗi TweenSequenceItem: Mỗi Tween trong TweenSequenceItem có thể có Curve riêng của nó (ví dụ: Curve.easeIn, Curve.bounceOut). Điều này giúp bạn tạo ra các hiệu ứng chuyển động rất "mượt mà" và "có hồn" hơn. Đừng chỉ dùng Linear, hãy thử nghiệm! Dùng AnimatedBuilder: Như ví dụ trên, AnimatedBuilder là cách "chuẩn bài" để lắng nghe sự thay đổi của AnimationController và rebuild widget một cách hiệu quả, tránh setState toàn bộ widget tree không cần thiết. Suy nghĩ "theo chuỗi": Khi nào bạn thấy cần một hiệu ứng mà nó diễn ra theo từng bước, từng giai đoạn rõ ràng, thì TweenSequence chính là ứng cử viên sáng giá. Còn nếu chỉ là một chuyển động đơn giản từ A đến B, thì Tween thường (không có Sequence) sẽ nhẹ nhàng và dễ quản lý hơn. 4. Ứng Dụng Thực Tế: Ai đã dùng TweenSequence rồi? Nói về ứng dụng thực tế thì nhiều vô kể, anh em cứ để ý mấy cái app hay game đẹp đẽ mà xem: Màn hình Intro/Loading: Khi app khởi động, các icon có thể từ từ hiện ra, phóng to, đổi màu theo một trình tự nhất định. Onboarding Flow: Mấy cái màn hình giới thiệu tính năng lúc mới cài app ấy. Các phần tử UI có thể di chuyển, biến đổi theo từng bước, dẫn dắt người dùng. Game UI/Animations: Trong game, khi nhân vật tung chiêu, nhận sát thương, hay đơn giản là đứng yên (idle animation), các hiệu ứng có thể là một chuỗi các chuyển động nhỏ nối tiếp nhau. Phản hồi người dùng (User Feedback): Khi nhấn một nút, nút đó không chỉ đổi màu mà có thể "nhảy" lên một tí, rung nhẹ, hoặc phát sáng theo một chuỗi. Giúp người dùng cảm thấy "có tương tác" hơn. TikTok-style Transitions: Mấy hiệu ứng chuyển cảnh "mượt mà" và phức tạp giữa các video, các story trên Instagram/Facebook cũng có thể được xây dựng bằng cách xâu chuỗi nhiều animation nhỏ. 5. Thử Nghiệm và Nên Dùng Cho Case Nào? (Kinh nghiệm xương máu của Creyt) Anh Creyt đã từng "vật lộn" với việc tạo ra các animation phức tạp mà không dùng TweenSequence rồi. Hồi đó, cứ phải ngồi tính toán interval thủ công, dùng CurvedAnimation với các begin và end khác nhau, xong rồi addListener tùm lum để cập nhật trạng thái. Kết quả là code dài dòng, khó đọc, và dễ phát sinh bug khi cần chỉnh sửa thời gian. Từ khi biết đến TweenSequence, mọi thứ trở nên "dễ thở" hơn hẳn. Anh em nên dùng TweenSequence khi: Animation có nhiều "chặng" rõ ràng: Mỗi chặng có một hành vi riêng (tăng tốc, giảm tốc, đổi màu, di chuyển...). Cần sự phối hợp nhịp nhàng giữa các hiệu ứng: Ví dụ, một vật thể di chuyển xong thì mới đổi màu, đổi màu xong mới xoay. TweenSequence giúp bạn định nghĩa trình tự này một cách trực quan. Muốn code "sạch" và dễ bảo trì hơn: Thay vì nhiều Tween độc lập và CurvedAnimation lồng ghép, TweenSequence gom chúng lại thành một khối logic. Tuy nhiên, đừng lạm dụng nó nhé! Nếu animation của bạn chỉ là một chuyển động đơn giản, hoặc các hiệu ứng diễn ra song song mà không cần trình tự, thì việc dùng TweenSequence có thể hơi "quá đà". Đôi khi, một Tween kết hợp với CurvedAnimation đã là đủ rồi. Quan trọng là chọn đúng công cụ cho đúng việc, giống như việc bạn chọn đúng loại cà phê cho buổi sáng vậy: đôi khi chỉ cần đen đá, đôi khi lại cần một ly Latte cầu kỳ. Vậy đó, TweenSequence không chỉ là một công cụ, nó là một tư duy giúp bạn "điều phối" các animation phức tạp một cách thanh lịch. Hãy thực hành và biến những ý tưởng animation "điên rồ" nhất của bạn thành hiện thực nhé cá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é!

44 Đọc tiếp
TweenSequenceItem: Hoạt hình Flutter đa chặng, mượt mà như lụa!
23/03/2026

TweenSequenceItem: Hoạt hình Flutter đa chặng, mượt mà như lụa!

Chào mấy đứa "coder hệ Gen Z"! Hôm nay, "giảng viên Creyt" của mấy đứa sẽ "mổ xẻ" một "bí kíp" cực "chất" trong Flutter để tạo ra những hoạt ảnh (animation) "mượt như bơ", đó chính là TweenSequenceItem. Nghe cái tên thì hơi "khoa học viễn tưởng" một tí, nhưng mà "đảm bảo" sau bài này, mấy đứa sẽ "phê" với những gì nó làm được! 1. TweenSequenceItem là "cái vẹo" gì và để làm gì? "Thôi được rồi, vào thẳng vấn đề luôn cho "nóng"! Mấy đứa cứ hình dung thế này: Khi mấy đứa muốn "thả thính" một đối tượng nào đó, đâu phải lúc nào cũng "tấn công" một kiểu từ đầu đến cuối đúng không? Lúc thì "nhẹ nhàng", lúc thì "mạnh bạo", lúc lại "lùi một bước tiến ba bước". TweenSequenceItem trong Flutter nó cũng y chang vậy đó! Nó không phải là một hoạt ảnh độc lập, mà là một "chặng" trong một "chuỗi hành trình" hoạt ảnh lớn hơn (mà cái hành trình lớn đó gọi là TweenSequence). Mỗi TweenSequenceItem giống như một "người chạy tiếp sức" trong đường đua animation vậy. Mỗi người có một quãng đường (được định nghĩa bằng weight) và một phong cách chạy (được định nghĩa bằng tween và curve) riêng. Khi các "người chạy" này kết hợp lại, chúng ta sẽ có một "đoạn phim" hoạt ảnh liền mạch, có nhiều "phân cảnh" khác nhau. Để làm gì ư? Đơn giản là để mấy đứa tạo ra những animation "phức tạp hóa" mà không phải "vật lộn" với việc tính toán thời gian thủ công hay tạo ra quá nhiều AnimationController. Ví dụ, mấy đứa muốn một cái nút ban đầu màu đỏ, sau đó từ từ chuyển sang vàng, rồi nhanh chóng đổi sang xanh lá, và cuối cùng "nháy" một cái thành xanh dương. Nếu không có TweenSequenceItem, mấy đứa sẽ phải "hack não" lắm đó! 2. Code Ví Dụ Minh Họa: "Thấy tận mắt, sờ tận tay" "Giờ thì "lý thuyết suông" đủ rồi, "xắn tay áo" vào "thực hành" thôi! Anh sẽ cho mấy đứa xem cách một cái hộp đổi màu "thần kỳ" qua nhiều giai đoạn khác nhau nhé. "Đảm bảo" dễ hiểu hơn "người yêu cũ" của mấy đứa luô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: 'TweenSequenceItem Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const TweenSequenceItemExample(), ); } } class TweenSequenceItemExample extends StatefulWidget { const TweenSequenceItemExample({super.key}); @override _TweenSequenceItemExampleState createState() => _TweenSequenceItemExampleState(); } class _TweenSequenceItemExampleState extends State<TweenSequenceItemExample> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 4), // Tổng thời gian của toàn bộ sequence vsync: this, ); // Đây là "trái tim" của chúng ta: TweenSequence chứa các TweenSequenceItem! _colorAnimation = TweenSequence<Color?>([ // Chặng 1: Đỏ -> Vàng (chiếm 25% tổng thời gian, tức 1 giây) TweenSequenceItem( tween: ColorTween(begin: Colors.red, end: Colors.yellow).chain(CurveTween(curve: Curves.easeIn)), weight: 0.25, ), // Chặng 2: Vàng -> Xanh lá (chiếm 50% tổng thời gian, tức 2 giây) TweenSequenceItem( tween: ColorTween(begin: Colors.yellow, end: Colors.green).chain(CurveTween(curve: Curves.bounceOut)), weight: 0.5, ), // Chặng 3: Xanh lá -> Xanh dương (chiếm 25% tổng thời gian, tức 1 giây) TweenSequenceItem( tween: ColorTween(begin: Colors.green, end: Colors.blue).chain(CurveTween(curve: Curves.fastOutSlowIn)), weight: 0.25, ), ]).animate(_controller); _controller.repeat(reverse: true); // Lặp lại animation, đi tới rồi đi lui } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("TweenSequenceItem Demo")), body: Center( child: AnimatedBuilder( animation: _colorAnimation, builder: (context, child) { return Container( width: 200, height: 200, color: _colorAnimation.value, // Màu của container sẽ thay đổi theo animation child: const Center( child: Text( "Màu sắc chuyển đổi", style: TextStyle(color: Colors.white, fontSize: 18), ), ), ); }, ), ), ); } } "Mấy đứa thấy không? Chỉ cần định nghĩa các TweenSequenceItem với tween (giá trị bắt đầu và kết thúc của chặng đó) và weight (độ dài của chặng đó so với tổng thời gian), "phép thuật" sẽ tự động xảy ra! Anh đã thêm chain(CurveTween(curve: ...)) vào mỗi tween để mỗi chặng có một "cảm xúc" riêng đó." 3. Mẹo "hack não" và "chiến thuật" thực tế từ "Creyt" "Để mấy đứa "nâng tầm" kỹ năng animation của mình, "giảng viên Creyt" có vài "bí kíp" muốn "truyền thụ" đây: weight là "trọng số", không phải "thời gian tuyệt đối": Nhớ kỹ điều này nhé! weight là tỉ lệ phần trăm của tổng thời lượng animation. Tổng weight của tất cả TweenSequenceItem phải bằng 1.0. Nếu tổng lớn hơn hoặc nhỏ hơn 1.0, Flutter sẽ tự điều chỉnh để phù hợp. Ví dụ, nếu tổng là 0.5, thì 0.25 sẽ chiếm 50% của tổng thời gian animation. Mỗi Item một "cá tính" riêng: Đừng ngại "custom" mỗi TweenSequenceItem với một Curve khác nhau. Điều này giúp animation của mấy đứa trông "sống động" và "có hồn" hơn nhiều. Ví dụ, một chặng thì Curves.easeIn, chặng sau lại Curves.bounceOut để tạo hiệu ứng "nhảy bật". "Mix & Match" các loại Tween: Mấy đứa không chỉ giới hạn ở ColorTween đâu nhé. Có thể dùng SizeTween, RectTween, AlignmentTween, hay thậm chí Tween<double> để điều khiển bất cứ thứ gì có thể "tween" được. "Sáng tạo" lên! chain() là "cầu nối": Để thêm Curve vào một Tween trong TweenSequenceItem, mấy đứa sẽ dùng .chain(CurveTween(curve: yourCurve)). Nó giúp "kết nối" Curve với Tween một cách "mượt mà" nhất. 4. "Ứng dụng thực tế" – Khi "code" không chỉ là "code" "Mấy đứa nghĩ "animation" chỉ để "làm màu" thôi à? "Sai lầm" rồi đó! TweenSequenceItem được ứng dụng "rộng rãi" trong rất nhiều app "xịn xò" mà mấy đứa đang dùng hàng ngày: Màn hình Loading/Splash Screen: Các hiệu ứng logo "xuất hiện", "biến mất" hoặc "nhảy múa" theo nhiều giai đoạn khác nhau để giữ chân người dùng trong lúc chờ đợi. Onboarding App: Các hiệu ứng chuyển động của các thành phần UI khi giới thiệu tính năng mới, thường là các icon "bay lượn", text "xuất hiện" dần dần theo từng bước. Game UI/UX: Khi một vật phẩm "rơi xuống", "nảy lên" rồi "biến mất", hoặc các hiệu ứng khi nâng cấp đồ vật, mở khóa tính năng. Ứng dụng "e-commerce" (mua sắm online): Hiệu ứng khi thêm sản phẩm vào giỏ hàng, nút "thêm vào giỏ" có thể "nhảy" một cái, rồi "phóng to" ra, rồi "biến mất" vào giỏ hàng. Mạng xã hội (ví dụ TikTok, Instagram): Các hiệu ứng chuyển cảnh giữa các Story, hoặc khi tương tác với nút Like/Share có thể có nhiều trạng thái chuyển động. 5. "Thử nghiệm đã từng" và "nên dùng cho case nào" "Anh Creyt" đã từng "đau đầu" với việc tạo animation phức tạp bằng cách nối thủ công từng Tween một. Nó giống như việc "xây nhà" bằng cách "từng viên gạch" mà không có "bản thiết kế" vậy. Kết quả là "code" thì "rối như tơ vò", "maintain" thì "khó như lên trời", và "bug" thì "nhiều như quân Nguyên"! Nên dùng TweenSequenceItem khi: Mấy đứa cần một chuỗi hoạt ảnh có nhiều giai đoạn riêng biệt, mỗi giai đoạn có tốc độ, đường cong (curve) hoặc giá trị bắt đầu/kết thúc khác nhau. Mấy đứa muốn kiểm soát chặt chẽ tỉ lệ thời gian của từng phần trong tổng thể animation. Mấy đứa muốn tạo ra animation "liền mạch" và "mượt mà" qua nhiều trạng thái mà không cần phải quản lý nhiều AnimationController riêng lẻ. Không nên dùng TweenSequenceItem khi: Chỉ cần một animation đơn giản từ A đến B (ví dụ: một cái nút chỉ cần "phóng to" rồi "thu nhỏ"). Lúc này, dùng một Tween và AnimationController thông thường là đủ, không cần "đao to búa lớn". "Tóm lại, TweenSequenceItem là một công cụ "đắc lực" giúp mấy đứa "làm chủ" thế giới animation "đầy màu sắc" của Flutter. Hãy "thử nghiệm" và "sáng tạo" với nó nhé! Chúc mấy đứa "code" vui vẻ và "lên trình" vù vù!" Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

37 Đọc tiếp
TweenSequence: Nhạc Trưởng Hoạt Ảnh Flutter Của Bạn
22/03/2026

TweenSequence: Nhạc Trưởng Hoạt Ảnh Flutter Của Bạn

Chào các dân chơi hệ code GenZ! Anh Creyt đây, hôm nay chúng ta sẽ cùng nhau 'bung lụa' với một khái niệm nghe hơi hàn lâm nhưng thực ra lại cực kỳ 'high-tech' và 'cool ngầu' trong thế giới Flutter: TweenSequence. TweenSequence Là Gì Mà Nghe Có Vẻ 'Ghê Gớm' Vậy? Để dễ hình dung, các em cứ tưởng tượng thế này: Khi các em muốn làm một hoạt ảnh đơn giản, kiểu như một cái hộp nhấp nháy từ màu đỏ sang màu xanh, đó là một 'Tween' đơn lẻ. Giống như một nhạc công solo vậy. Nhưng đời đâu phải lúc nào cũng đơn giản, đúng không? Đôi khi, các em muốn cái hộp đó không chỉ đổi màu, mà sau đó còn phình to ra, rồi lại mờ dần đi, tất cả diễn ra theo một kịch bản đã định. Lúc này, TweenSequence chính là 'nhạc trưởng' mà các em cần! Nó không phải là một hoạt ảnh, mà là một công cụ để xâu chuỗi nhiều hoạt ảnh (tweens) lại với nhau thành một chuỗi liền mạch, có thứ tự và thời gian cụ thể. Giống như một đạo diễn tài ba, TweenSequence sẽ sắp xếp từng cảnh quay (từng tween) sao cho chúng diễn ra lần lượt, mượt mà và đúng thời điểm, tạo nên một bộ phim hoạt hình mini hoàn chỉnh. Nói cách khác, nó giúp bạn kể một câu chuyện bằng hoạt ảnh, từng bước một, thay vì chỉ là một hành động đơn lẻ. Tại Sao Chúng Ta Cần 'Nhạc Trưởng' Này? Đơn giản thôi! Trong thực tế, các hoạt ảnh trên app của chúng ta hiếm khi chỉ có một pha duy nhất. Hãy nghĩ đến hiệu ứng khi bạn nhấn nút 'Like' trên Facebook: nó có thể phình to ra, đổi màu, rồi nảy nhẹ một cái. Hoặc một màn hình loading phức tạp với nhiều đối tượng di chuyển theo nhiều giai đoạn. Nếu không có TweenSequence, các em sẽ phải 'cân' từng AnimationController riêng lẻ, tính toán thời gian thủ công, và rồi mọi thứ sẽ trở nên 'rối như canh hẹ'. TweenSequence giúp chúng ta quản lý sự phức tạp đó một cách thanh lịch và hiệu quả. Cách 'Nhạc Trưởng' TweenSequence Hoạt Động (Và Dàn Nhạc Của Nó) Để TweenSequence hoạt động, chúng ta cần vài thành phần chính: AnimationController: Đây là 'người cầm trịch' toàn bộ quá trình. Nó định nghĩa tổng thời gian của chuỗi hoạt ảnh và cung cấp giá trị từ 0.0 đến 1.0 theo thời gian. TweenSequence: Bản thân nó là một Tween<T> đặc biệt, nhận vào một danh sách các TweenSequenceItem. TweenSequenceItem: Đây là 'từng nốt nhạc' trong bản giao hưởng. Mỗi TweenSequenceItem bao gồm: tween: Một Tween cụ thể (ví dụ: ColorTween, SizeTween, CurveTween, IntTween, DoubleTween). Đây là hành động mà các em muốn thực hiện (đổi màu, thay đổi kích thước, v.v.). weight: Đây là 'thời lượng' hoặc 'trọng số' của tween đó trong tổng thời gian của toàn bộ TweenSequence. weight là một giá trị double và tổng weight của tất cả các TweenSequenceItem trong danh sách phải bằng 1.0. Ví dụ, nếu có 3 tween với weight là 0.2, 0.5, 0.3, thì tween đầu tiên sẽ chiếm 20% tổng thời gian, tween thứ hai 50%, và tween thứ ba 30%. Khi AnimationController chạy từ 0.0 đến 1.0, TweenSequence sẽ tính toán và áp dụng từng TweenSequenceItem theo đúng weight của nó, đảm bảo các hoạt ảnh diễn ra tuần tự. Code Ví Dụ Minh Họa: 'Cậu Bé Hộp' Kể Chuyện Giả sử chúng ta muốn một cái hộp: Đổi màu từ xanh lá sang đỏ (20% thời gian). Phóng to từ 50x50px lên 150x150px (50% thời gian). Mờ dần về 0 opacity (30% thời gian). Đây là cách chúng ta sẽ 'đạo diễn' nó: import 'package:flutter/material.dart'; class TweenSequenceDemo extends StatefulWidget { const TweenSequenceDemo({Key? key}) : super(key: key); @override State<TweenSequenceDemo> createState() => _TweenSequenceDemoState(); } class _TweenSequenceDemoState extends State<TweenSequenceDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Color?> _colorAnimation; late Animation<double?> _sizeAnimation; late Animation<double?> _opacityAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), // Tổng thời gian 3 giây ); // Định nghĩa chuỗi TweenSequence final colorTween = TweenSequence<Color?>([ TweenSequenceItem( tween: ColorTween(begin: Colors.green, end: Colors.red), weight: 0.2, // 20% thời gian (0.6 giây) ), TweenSequenceItem( tween: ColorTween(begin: Colors.red, end: Colors.red), // Giữ màu đỏ weight: 0.5, // 50% thời gian (1.5 giây) - không đổi màu trong giai đoạn này ), TweenSequenceItem( tween: ColorTween(begin: Colors.red, end: Colors.transparent), // Mờ dần weight: 0.3, // 30% thời gian (0.9 giây) ), ]); final sizeTween = TweenSequence<double?>([ TweenSequenceItem( tween: ConstantTween<double?>(50.0), // Giữ kích thước ban đầu weight: 0.2, ), TweenSequenceItem( tween: Tween<double>(begin: 50.0, end: 150.0), weight: 0.5, // Phóng to ), TweenSequenceItem( tween: Tween<double>(begin: 150.0, end: 150.0), // Giữ kích thước lớn weight: 0.3, ), ]); final opacityTween = TweenSequence<double?>([ TweenSequenceItem( tween: ConstantTween<double?>(1.0), // Giữ opacity 1.0 weight: 0.2, ), TweenSequenceItem( tween: ConstantTween<double?>(1.0), // Giữ opacity 1.0 weight: 0.5, ), TweenSequenceItem( tween: Tween<double>(begin: 1.0, end: 0.0), // Mờ dần weight: 0.3, ), ]); _colorAnimation = colorTween.animate(_controller); _sizeAnimation = sizeTween.animate(_controller); _opacityAnimation = opacityTween.animate(_controller); // Bắt đầu animation và lặp lại _controller.repeat(reverse: true); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('TweenSequence Demo')), body: Center( child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Opacity( opacity: _opacityAnimation.value ?? 1.0, child: Container( width: _sizeAnimation.value, height: _sizeAnimation.value, decoration: BoxDecoration( color: _colorAnimation.value, borderRadius: BorderRadius.circular(10.0), ), ), ); }, ), ), ); } } Trong ví dụ trên, anh đã tạo ba TweenSequence riêng biệt cho màu sắc, kích thước và độ mờ. Mỗi TweenSequence này chứa các TweenSequenceItem với weight tương ứng, đảm bảo các giai đoạn hoạt ảnh diễn ra đúng thứ tự và thời gian. ConstantTween được dùng để giữ nguyên giá trị trong các giai đoạn không muốn hoạt ảnh thay đổi. Mẹo Hay Từ Anh Creyt (Best Practices) Tính Toán weight Chuẩn Chỉ: Tổng weight của tất cả TweenSequenceItem trong một TweenSequence phải là 1.0. Nếu không, hoạt ảnh có thể không chạy đúng hoặc có những khoảng 'chết'. Đây là lỗi mà các em hay 'quên béng' nhất đấy! Chia Để Trị: Nếu chuỗi hoạt ảnh quá phức tạp với nhiều thuộc tính thay đổi cùng lúc, hãy tạo nhiều TweenSequence riêng biệt cho từng thuộc tính (như ví dụ trên với màu, kích thước, opacity) và cùng animate chúng với một AnimationController duy nhất. Điều này giúp code dễ đọc, dễ quản lý hơn rất nhiều. Sử Dụng Curve Trong Từng Tween: Đừng quên rằng mỗi Tween bên trong TweenSequenceItem vẫn có thể được animate với một Curve riêng biệt. Điều này cho phép bạn tinh chỉnh tốc độ chuyển động của từng giai đoạn hoạt ảnh (ví dụ: easeOut, bounceIn). Quản Lý AnimationController: Luôn dispose() AnimationController khi State bị hủy (dispose method). Nếu không, nó sẽ gây rò rỉ bộ nhớ, làm app của các em 'lag' như 'đồ cổ' vậy. ConstantTween Là Bạn Thân: Khi bạn muốn một thuộc tính giữ nguyên giá trị trong một phần của chuỗi hoạt ảnh, hãy dùng ConstantTween. Nó giúp bạn duy trì giá trị mà không cần phải 'nhảy múa' với các begin và end của Tween khác. Ứng Dụng Thực Tế: Ai Đang Dùng 'Nhạc Trưởng' Này? Màn hình chào mừng (Splash Screen) hoặc Onboarding: Các app như Netflix, Spotify thường có những màn hình giới thiệu với nhiều yếu tố UI xuất hiện và biến mất theo một trình tự đẹp mắt. Đó chính là đất diễn của TweenSequence. Hiệu ứng Loading phức tạp: Các animation loading không chỉ là một vòng quay đơn giản mà có thể là nhiều hình ảnh, chữ viết xuất hiện và biến mất theo từng pha. Slack hay Google Photos là ví dụ điển hình. Hiệu ứng tương tác UI: Khi bạn nhấn vào một nút, nó có thể không chỉ đổi màu mà còn nảy lên, sau đó rung nhẹ, rồi trở về trạng thái ban đầu. Hoặc hiệu ứng 'vỗ tay', 'thả tim' với nhiều giai đoạn hoạt ảnh. Game UI: Trong các game, khi một vật phẩm được nhặt, một kỹ năng được kích hoạt, thường có một chuỗi hoạt ảnh để báo hiệu cho người chơi. Thử Nghiệm Và Khi Nào Nên 'Triệu Hồi' TweenSequence? Anh Creyt đã từng 'vật lộn' với việc quản lý hàng tá AnimationController riêng lẻ cho từng pha hoạt ảnh phức tạp. Kết quả là code 'nát bét', khó debug, và hiệu suất thì 'rớt đài'. Từ khi 'kết thân' với TweenSequence, mọi thứ trở nên 'ngon lành cành đào' hơn hẳn. Nên dùng TweenSequence khi: Bạn cần một chuỗi hoạt ảnh mà các giai đoạn diễn ra tuần tự và có liên kết thời gian chặt chẽ với nhau. Bạn muốn kể một câu chuyện thông qua hoạt ảnh, nơi mỗi phần của câu chuyện là một TweenSequenceItem. Bạn có nhiều thay đổi thuộc tính (màu, kích thước, vị trí, độ mờ) cần xảy ra theo một kịch bản đã định trên cùng một đối tượng hoặc các đối tượng liên quan. Không nên dùng TweenSequence khi: Bạn chỉ cần một hoạt ảnh đơn giản, một pha duy nhất (kiểu như FadeTransition, ScaleTransition cơ bản). Đừng 'vác dao mổ trâu đi giết gà' nhé! Các hoạt ảnh không có mối quan hệ thời gian tuần tự mà diễn ra song song hoặc độc lập với nhau. Lúc đó, dùng nhiều Tween riêng lẻ hoặc ImplicitlyAnimatedWidget có thể phù hợp hơn. Nhớ nhé các GenZ, TweenSequence không chỉ là một công cụ, nó là một 'triết lý' giúp các em tổ chức và điều khiển các hoạt ảnh phức tạp một cách 'pro' hơn. Cứ thử nghiệm đi, rồi các em sẽ thấy nó 'đỉnh của chóp' như thế nào! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

37 Đọc tiếp
Tween thần tốc Flutter: Tạo hiệu ứng mượt mà như TikTok!
22/03/2026

Tween thần tốc Flutter: Tạo hiệu ứng mượt mà như TikTok!

Chào các bạn Gen Z mê code và mê cái đẹp UI! Hôm nay, anh Creyt sẽ bật mí cho các em một "phép thuật" trong Flutter giúp app của mình mượt mà, "slay" hơn bao giờ hết: đó chính là Tween. 1. Tween là gì mà "chill" vậy anh Creyt? "Tween" thực ra là viết tắt của "in-betweening" – tức là làm cái gì đó "ở giữa". Nghe hơi "lú" đúng không? Để anh giải thích bằng ngôn ngữ Gen Z cho dễ hiểu nhé: Em cứ hình dung thế này: Khi em xem một video TikTok chuyển cảnh "mượt như nhung", hay một nhân vật game di chuyển không phải kiểu "teleport" mà là lướt đi từ từ, đó chính là nhờ có Tween ở hậu trường. Nó không phải là người làm animation trực tiếp, mà nó là "kịch bản" hay "công thức" để tạo ra các giá trị trung gian giữa điểm bắt đầu (begin) và điểm kết thúc (end). Ví dụ, em muốn một widget thay đổi kích thước từ nhỏ (0.0) lên lớn (1.0). Tween sẽ không bảo nó "nhảy" thẳng từ 0.0 lên 1.0. Thay vào đó, nó sẽ tính toán các giá trị "ở giữa" như 0.1, 0.2, 0.3... cho đến 1.0 trong một khoảng thời gian nhất định. Giống như em có một hành trình, Tween là cái bản đồ chỉ đường cho em đi từng bước một, thay vì "dịch chuyển tức thời" vậy. 2. Tween để làm gì? Trong Flutter, Tween là trái tim của các animation "explicit" (animation tường minh). Nó giúp các em: Tạo hiệu ứng chuyển động: Di chuyển widget từ vị trí A sang B. Thay đổi kích thước: Phóng to, thu nhỏ widget. Thay đổi màu sắc: Đổi màu gradient "mượt mà" không bị "giật cục". Điều chỉnh độ mờ (opacity): Làm widget hiện lên (fade in) hoặc biến mất (fade out) "ảo diệu". Thay đổi góc xoay (rotation): Xoay widget "nghệ thuật". Tóm lại, Tween là "linh hồn" để biến một UI tĩnh thành một UI "sống động", "có hồn", khiến người dùng "mê mẩn" ngay từ cái chạm đầu tiên. 3. Code Ví Dụ: "Tween" một cái widget đơn giản Để các em dễ hình dung, anh Creyt sẽ hướng dẫn các em tạo một animation đơn giản: Một cái hộp sẽ phóng to/thu nhỏ và thay đổi độ mờ khi em nhấn nút. "Nghe là thấy mê rồi đúng không?" 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: 'Tween Demo by Creyt', theme: ThemeData(primarySwatch: Colors.blue), home: const TweenAnimationScreen(), ); } } class TweenAnimationScreen extends StatefulWidget { const TweenAnimationScreen({super.key}); @override State<TweenAnimationScreen> createState() => _TweenAnimationScreenState(); } class _TweenAnimationScreenState extends State<TweenAnimationScreen> with SingleTickerProviderStateMixin { late AnimationController _controller; // "Nhạc trưởng" điều khiển animation late Animation<double> _scaleAnimation; // Animation cho kích thước late Animation<double> _opacityAnimation; // Animation cho độ mờ @override void initState() { super.initState(); // 1. Khởi tạo AnimationController: "Nhạc trưởng" với thời lượng 1 giây _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, // Cần SingleTickerProviderStateMixin ); // 2. Định nghĩa Tween cho kích thước: Từ 0.5 (nhỏ) đến 1.5 (lớn) // Sau đó, áp dụng Tween này vào controller để tạo ra Animation _scaleAnimation = Tween<double>(begin: 0.5, end: 1.5).animate( CurvedAnimation(parent: _controller, curve: Curves.elasticOut), // Thêm hiệu ứng "nhún nhảy" ); // 3. Định nghĩa Tween cho độ mờ: Từ 0.2 (mờ) đến 1.0 (rõ nét) _opacityAnimation = Tween<double>(begin: 0.2, end: 1.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); // Lắng nghe trạng thái của controller để biết khi nào animation kết thúc _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); // Khi xong, đảo ngược lại } else if (status == AnimationStatus.dismissed) { _controller.forward(); // Khi về ban đầu, chạy tới } }); } @override void dispose() { _controller.dispose(); // "Giải phóng" nhạc trưởng khi không dùng nữa super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Tween Magic with Creyt')), body: Center( // AnimatedBuilder sẽ lắng nghe sự thay đổi của _controller // và chỉ rebuild phần con cần thiết, tối ưu hiệu suất child: AnimatedBuilder( animation: _controller, // Lắng nghe _controller builder: (context, child) { return Opacity( opacity: _opacityAnimation.value, // Áp dụng giá trị độ mờ từ animation child: Transform.scale( scale: _scaleAnimation.value, // Áp dụng giá trị kích thước từ animation child: Container( width: 100, // Kích thước cơ bản height: 100, color: Colors.deepPurple, child: const Center( child: Text( 'Creyt', style: TextStyle(color: Colors.white, fontSize: 20), ), ), ), ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Bắt đầu hoặc dừng animation if (_controller.isAnimating) { _controller.stop(); } else if (_controller.status == AnimationStatus.dismissed || _controller.status == AnimationStatus.reverse) { _controller.forward(); // Chạy tới } else if (_controller.status == AnimationStatus.completed || _controller.status == AnimationStatus.forward) { _controller.reverse(); // Chạy ngược } }, child: const Icon(Icons.play_arrow), ), ); } } Giải thích code: _controller = AnimationController(...): Đây là "nhạc trưởng" của chúng ta. Nó điều khiển thời gian, tốc độ, và trạng thái của toàn bộ animation. duration là thời lượng, vsync giúp đồng bộ animation với màn hình. Tween<double>(begin: 0.5, end: 1.5): Đây chính là Tween! Nó định nghĩa rằng chúng ta muốn các giá trị thay đổi từ 0.5 đến 1.5. Nó chỉ là một "công thức" thôi, chưa chạy đâu nhé. .animate(CurvedAnimation(parent: _controller, curve: Curves.elasticOut)): Chúng ta "kết nối" cái Tween này với "nhạc trưởng" _controller để nó biết "khi nào thì tính giá trị". CurvedAnimation cho phép chúng ta thêm các "đường cong" (curve) để animation trông tự nhiên hơn, ví dụ Curves.elasticOut sẽ tạo hiệu ứng "nhún nhảy" ở cuối. _scaleAnimation.value và _opacityAnimation.value: Đây là giá trị hiện tại mà Tween đã tính toán được, dựa trên trạng thái của _controller. Chúng ta dùng giá trị này để áp dụng vào các widget như Transform.scale và Opacity. AnimatedBuilder: Widget này rất quan trọng. Nó lắng nghe sự thay đổi của _controller và chỉ xây dựng lại (rebuild) phần con của nó khi giá trị animation thay đổi. Điều này giúp tối ưu hiệu suất, tránh rebuild toàn bộ cây widget. addStatusListener: Giúp chúng ta biết khi nào animation đã hoàn thành (completed) hay trở về trạng thái ban đầu (dismissed) để thực hiện hành động tiếp theo (ví dụ: chạy ngược lại). 4. Mẹo (Best Practices) từ anh Creyt để code "chất" hơn: "Đừng quên dispose": Giống như em đi ăn buffet xong phải trả đĩa vậy. Khi AnimationController không còn được dùng nữa (ví dụ: màn hình bị đóng), hãy gọi _controller.dispose() trong dispose() của StatefulWidget để tránh rò rỉ bộ nhớ. "Không dọn rác là dễ bị lag máy lắm đó!" "Chọn Curve phù hợp": Một animation có thể "đi thẳng" nhưng cũng có thể "đi dạo, đi lượn". CurvedAnimation với các Curves như easeInOut, bounceIn, elasticOut sẽ làm animation của em có "cảm xúc" hơn, "mượt mà" hơn. "Cứ thử nghiệm đi, mỗi curve là một vibe khác nhau đó!" "Kết hợp nhiều Tween": Đừng ngại "mix & match"! Em có thể dùng một AnimationController để điều khiển nhiều Tween khác nhau (ví dụ: vừa scale, vừa fade, vừa di chuyển) để tạo ra các animation phức tạp, "xịn xò" hơn. "Một nhạc trưởng, nhiều nhạc cụ, tạo nên bản giao hưởng UI!" "Dùng AnimatedBuilder khi có thể": Như anh đã nói ở trên, AnimatedBuilder giúp tối ưu hiệu suất cực tốt. Nó chỉ rebuild phần UI bị ảnh hưởng bởi animation, chứ không phải toàn bộ màn hình. "Code thông minh, app chạy mượt, user khen nức nở!" 5. Ví dụ thực tế các ứng dụng/website đã "quẩy" với Tween: Thực ra, các animation mà em thấy hàng ngày trên điện thoại hay web đều có bóng dáng của Tween (hoặc các cơ chế tương tự): TikTok/Instagram Reels: Các hiệu ứng chuyển cảnh siêu mượt khi em vuốt qua lại giữa các video. Nút "Like" trên Facebook/Instagram: Khi em nhấn "like", nút trái tim thường có hiệu ứng phóng to/thu nhỏ hoặc nảy lên một chút. Chuyển tab trong các ứng dụng: Thay vì nhảy "cộc cộc", các tab thường trượt sang ngang hoặc mờ dần/hiện ra. Hiệu ứng loading: Các vòng tròn quay, thanh tiến trình di chuyển, thường được tạo ra bằng cách animate các giá trị góc, vị trí. Game mobile đơn giản: Các nhân vật di chuyển, vật phẩm rơi, hay hiệu ứng nổ, tất cả đều cần tính toán các trạng thái "ở giữa" theo thời gian. 6. Thử nghiệm và hướng dẫn nên dùng cho case nào: Anh Creyt đã từng "quẩy" với Tween để tạo ra đủ thứ animation "điên rồ": Hiệu ứng "bùng nổ" khi hoàn thành nhiệm vụ: Khi người dùng đạt được một cột mốc, anh dùng Tween để phóng to một icon vinh danh, sau đó làm nó fade out và rơi xuống như pháo hoa. "Cảm giác thành tựu nó phải khác bọt chứ!" Animation "nhấp nháy" cho thông báo mới: Một icon chuông sẽ phập phồng to nhỏ, hoặc thay đổi màu sắc nhẹ nhàng để thu hút sự chú ý. "Không cần phải làm gì quá phức tạp, chỉ cần tinh tế là đủ." Vậy, khi nào thì nên "triển" Tween? Khi bạn muốn kiểm soát chi tiết animation: Nếu các ImplicitlyAnimatedWidget (như AnimatedContainer, AnimatedOpacity) không đủ tùy biến, Tween sẽ cho bạn toàn quyền điều khiển. Khi bạn cần tạo animation phức tạp: Kết hợp nhiều hiệu ứng (scale, move, fade) cùng lúc, hoặc tạo chuỗi animation liên tiếp. Khi bạn muốn đồng bộ nhiều animation: Dùng chung một AnimationController cho nhiều Tween để tất cả chuyển động "ăn khớp" với nhau. Khi bạn muốn animation có "cảm xúc" riêng: Với CurvedAnimation, bạn có thể tạo ra các hiệu ứng "nhún nhảy", "đàn hồi", "tăng tốc/giảm tốc" tùy ý. "Nhớ nhé các em, Tween không chỉ là code, nó là nghệ thuật! Hãy dùng nó để biến những ý tưởng "bay bổng" nhất của mình thành hiện thực trên màn hình di động. Giờ thì, về nhà code thử đi, có gì khúc mắc 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é!

46 Đọc tiếp
TransformLayer: Phù Thủy 3D Biến Hình UI Flutter Của Bạn!
22/03/2026

TransformLayer: Phù Thủy 3D Biến Hình UI Flutter Của Bạn!

Này mấy đứa, hôm nay chúng ta sẽ cùng giải mã một "siêu năng lực" ẩn mình trong Flutter mà ít ai dám động vào, đó chính là TransformLayer! Nghe cái tên đã thấy 'pro' rồi đúng không? Đừng lo, anh Creyt sẽ biến nó thành món ăn dễ nuốt nhất quả đất. 1. TransformLayer Là Gì Mà "Chill" Thế? Tưởng tượng thế này, app Flutter của mấy đứa là một sân khấu kịch hoành tráng. Bình thường, khi mấy đứa dùng Transform.rotate hay Transform.translate, đó là mấy đứa đang sai mấy anh công nhân sân khấu di chuyển thật cái đạo cụ (widget) của mấy đứa trên sàn diễn. Nó rõ ràng, dễ hiểu, nhưng đôi khi hơi "cồng kềnh" và tốn sức. Còn TransformLayer á? Nó giống như mấy đứa đang điều khiển máy quay phim vậy đó! Mấy đứa không hề di chuyển đạo cụ, đạo cụ vẫn đứng yên tại chỗ, nhưng mấy đứa lại thay đổi góc quay, thêm hiệu ứng phối cảnh, hay thậm chí là "bẻ cong" không gian nhìn thấy. Kết quả là khán giả (người dùng) thấy đạo cụ đó như đang xoay, đang bay, đang lùi sâu vào không gian ảo, trong khi thực tế nó vẫn "chôn chân" ở vị trí ban đầu trên sân khấu logic. Nói một cách hàn lâm hơn, TransformLayer là một widget cấp thấp (low-level) cho phép chúng ta áp dụng một ma trận biến đổi 3D (Matrix4) lên toàn bộ lớp vẽ (render layer) của con nó trước khi nó được vẽ ra màn hình. Điều này khác biệt hoàn toàn với widget Transform thông thường, vốn áp dụng biến đổi sau khi con nó đã được bố cục (layout) xong. Chính vì thế, TransformLayer cực kỳ mạnh mẽ cho các hiệu ứng 3D phức tạp, đòi hỏi phối cảnh (perspective) chân thực mà không làm ảnh hưởng đến bố cục logic của các widget con. Tóm lại: Transform: Di chuyển vật thể thật (ảnh hưởng layout). TransformLayer: Thay đổi góc nhìn camera (không ảnh hưởng layout, chỉ thay đổi cách vẽ). 2. Code Ví Dụ Minh Họa: Biến Hình Một Chiếc Card Để dễ hình dung, anh em mình thử tạo một hiệu ứng xoay 3D với phối cảnh nhé. Anh sẽ dùng GestureDetector để mấy đứa có thể "chạm và kéo" để xoay cái card, nhìn cho nó "ảo diệu" chút. import 'package:flutter/material.dart'; import 'package:vector_math/vector_math_64.dart' as vector; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TransformLayer Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const TransformLayerExample(), ); } } class TransformLayerExample extends StatefulWidget { const TransformLayerExample({super.key}); @override State<TransformLayerExample> createState() => _TransformLayerExampleState(); } class _TransformLayerExampleState extends State<TransformLayerExample> { double _rotationX = 0.0; double _rotationY = 0.0; @override Widget build(BuildContext context) { // Tạo ma trận biến đổi 3D // Bắt đầu với Matrix4.identity() là ma trận không biến đổi gì cả. Matrix4 transformMatrix = Matrix4.identity() ..setEntry(3, 2, 0.001) // Thêm phối cảnh (perspective) ..rotateX(vector.radians(_rotationX)) // Xoay quanh trục X ..rotateY(vector.radians(_rotationY)); // Xoay quanh trục Y return Scaffold( appBar: AppBar( title: const Text('TransformLayer Magic'), ), body: Center( child: GestureDetector( onPanUpdate: (details) { setState(() { _rotationY += details.delta.dx * 0.5; // Kéo ngang để xoay Y _rotationX -= details.delta.dy * 0.5; // Kéo dọc để xoay X }); }, child: TransformLayer( transform: transformMatrix, // Để thấy rõ hiệu ứng 3D, thường đặt child là một Container có màu sắc hoặc hình ảnh. child: Container( width: 200, height: 300, decoration: BoxDecoration( color: Colors.deepPurpleAccent, borderRadius: BorderRadius.circular(15), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 10, offset: Offset(5, 5), ), ], ), alignment: Alignment.center, child: const Text( 'Anh Creyt dạy TransformLayer', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), ), ), ), ); } } Giải thích code: Matrix4.identity(): Bắt đầu với một ma trận "trống trơn", không làm gì cả. setEntry(3, 2, 0.001): Đây là "chìa khóa" để tạo phối cảnh 3D. Giá trị 0.001 càng nhỏ thì phối cảnh càng mạnh (vật thể xa sẽ nhỏ đi nhanh hơn). Không có dòng này, mấy đứa sẽ chỉ thấy vật thể xoay 2D phẳng lì thôi. rotateX, rotateY: Áp dụng các phép xoay quanh trục X và Y. GestureDetector: Giúp chúng ta tương tác, kéo ngón tay để thay đổi góc xoay. 3. Mẹo Vặt "Hack Não" và Best Practices Hiểu Matrix4: Đây là "trái tim" của TransformLayer. Matrix4 là một ma trận 4x4 dùng để biểu diễn các phép biến đổi 3D (tịnh tiến, xoay, tỉ lệ, phối cảnh). Đừng sợ nó, cứ nghĩ nó như một "hộp công cụ" chứa các phép biến hình vậy. Flutter cung cấp sẵn các hàm tiện ích như rotateX, translate, scale để mấy đứa không cần phải "động tay" vào từng phần tử ma trận. TransformLayer vs Transform: Dùng Transform khi mấy đứa chỉ cần các biến đổi 2D đơn giản (xoay, tịnh tiến, tỉ lệ) và không quan tâm đến phối cảnh 3D sâu, hoặc khi muốn biến đổi ảnh hưởng đến bố cục của widget. Dùng TransformLayer khi mấy đứa cần hiệu ứng 3D chân thực (có phối cảnh), hiệu năng cao cho các animation phức tạp, hoặc khi muốn biến đổi trực quan mà không làm thay đổi vị trí logic của widget con. TransformLayer tạo ra một lớp vẽ mới, nên nó có thể đắt hơn một chút về bộ nhớ, nhưng lại siêu hiệu quả khi xử lý các phép biến đổi liên tục. Phối cảnh (setEntry(3, 2, value)): Luôn nhớ dòng này khi muốn có hiệu ứng 3D "sâu". Giá trị càng nhỏ (gần 0) thì hiệu ứng phối cảnh càng mạnh. Trục tọa độ: Trong Flutter, trục X hướng sang phải, Y hướng xuống dưới, Z hướng ra khỏi màn hình (về phía người xem). Debugging: Nếu thấy hiệu ứng không như ý, hãy thử tách nhỏ các phép biến đổi ra, hoặc dùng print để xem giá trị của Matrix4 sau mỗi lần biến đổi. 4. Ứng Dụng Thực Tế "Đỉnh Cao" TransformLayer (hoặc các kỹ thuật tương tự ở các nền tảng khác) được dùng ở rất nhiều nơi mấy đứa không ngờ tới: Hiệu ứng Parallax Scrolling: Khi cuộn trang, các lớp nội dung ở xa hơn sẽ di chuyển chậm hơn, tạo cảm giác chiều sâu. Mấy đứa có thể thấy cái này trên rất nhiều website hiện đại hay các app có giao diện "động". Card Flip/3D Cube Animations: Các hiệu ứng lật thẻ bài, xoay khối lập phương 3D trong các game, app học flashcard, hay giao diện album ảnh. AR (Augmented Reality) Overlays (simulated): Mặc dù Flutter không phải là nền tảng chính cho AR, nhưng với TransformLayer, mấy đứa có thể tạo ra các hiệu ứng giả lập AR, ví dụ như đặt một vật thể 3D ảo lên trên nền camera (nếu có tích hợp camera feed). Complex UI Transitions: Các hiệu ứng chuyển cảnh giữa các màn hình mà các phần tử UI như bay lượn, xoay tròn trong không gian 3D. Custom Shaders và Visual Effects: Kết hợp với CustomPainter hoặc các shader, TransformLayer có thể tạo ra các hiệu ứng hình ảnh độc đáo, biến đổi không gian vẽ một cách mạnh mẽ. 5. Thử Nghiệm và Khi Nào Nên Dùng Anh Creyt đã từng "đau đầu" với mấy cái hiệu ứng 3D phức tạp cho một dự án app bán hàng thời trang, muốn làm cho mấy cái sản phẩm nó "bay lượn" ra khỏi màn hình khi người dùng vuốt. Ban đầu cũng dùng Transform nhưng thấy nó cứ "cứng đơ" và không có chiều sâu. Đến khi chuyển sang TransformLayer và chịu khó "ngâm cứu" Matrix4 thì mọi thứ như "khai sáng" vậy. Các sản phẩm ảo như có hồn, lượn lờ trong không gian rất mượt mà và chân thực. Nên dùng TransformLayer khi: Cần phối cảnh 3D: Khi mấy đứa muốn vật thể trông xa hơn khi nó lùi vào, hoặc gần hơn khi nó tiến ra. Hiệu năng là ưu tiên hàng đầu cho animation 3D: Khi có nhiều phép biến đổi liên tục và phức tạp, đặc biệt là trên các thiết bị yếu hơn. Không muốn biến đổi làm ảnh hưởng layout: Khi mấy đứa chỉ muốn thay đổi cách hiển thị mà không làm thay đổi kích thước hay vị trí bố cục của widget. Không nên dùng TransformLayer khi: Chỉ cần biến đổi 2D đơn giản: Nếu chỉ cần xoay 90 độ, di chuyển sang trái 10px, hay scale to gấp đôi, dùng Transform là đủ và dễ hiểu hơn nhiều. "Đại pháo bắn muỗi" làm gì cho mệt! Mới bắt đầu với Flutter và chưa vững kiến thức cơ bản: Hãy nắm chắc các widget cơ bản và Transform trước khi "nhảy" vào TransformLayer để tránh bị "ngộp". Tóm lại, TransformLayer là một công cụ mạnh mẽ, là "át chủ bài" để mấy đứa nâng cấp visual game của app Flutter lên một tầm cao mới. Đừng ngại thử nghiệm, cứ coi Matrix4 như một trò chơi xếp hình 3D và từ từ khám phá nhé! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

38 Đọc tiếp
TooltipTheme: Nâng tầm 'Lời thì thầm' của ứng dụng Flutter
22/03/2026

TooltipTheme: Nâng tầm 'Lời thì thầm' của ứng dụng Flutter

Chào các gen Z! Hôm nay, anh Creyt sẽ cùng các bạn "mổ xẻ" một khái niệm nghe thì có vẻ nhỏ bé nhưng lại là "phù thủy" tạo nên sự tinh tế cho ứng dụng của chúng ta: TooltipTheme trong Flutter. TooltipTheme là gì và để làm gì? (Genz-friendly style) Các bạn cứ hình dung thế này: trong một buổi tiệc đông người, khi bạn muốn chỉ cho ai đó một chi tiết nhỏ trên bức tranh mà không muốn hét toáng lên, bạn sẽ khẽ ghé tai thì thầm đúng không? Cái "lời thì thầm" đó chính là Tooltip trong lập trình. Tooltip là một đoạn văn bản nhỏ hiển thị khi người dùng di chuột (trên web/desktop) hoặc nhấn giữ (trên mobile) vào một thành phần UI nào đó. Nó dùng để cung cấp thêm thông tin giải thích cho icon, nút bấm, hoặc bất kỳ widget nào mà không làm rối giao diện chính. Còn TooltipTheme? À, nó chính là "người tạo mẫu" cho tất cả những lời thì thầm đó trong ứng dụng của bạn. Thay vì phải tự tay thiết kế từng lời thì thầm một (kiểu chữ, màu sắc, kích thước hộp thoại), TooltipTheme cho phép bạn định nghĩa một "phong cách" chung, một "bộ đồng phục" cho tất cả các tooltip của mình. Như vậy, ứng dụng của bạn sẽ trông "ngầu" hơn, chuyên nghiệp hơn và nhất quán hơn rất nhiều. Nói tóm lại, TooltipTheme giúp bạn: Đồng bộ hóa giao diện: Tất cả các tooltip trên ứng dụng của bạn sẽ có cùng một "vibe", cùng một "brand identity". Tiết kiệm thời gian: Không cần chỉnh sửa từng tooltip riêng lẻ. Nâng cao trải nghiệm người dùng (UX): Một giao diện đồng nhất luôn dễ chịu và dễ sử dụng hơn. Code Ví Dụ Minh Họa: Từ Cơ Bản Đến Nâng Cao Đầu tiên, hãy xem một Tooltip cơ bản trông như thế nà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: 'Tooltip Demo', theme: ThemeData(primarySwatch: Colors.blue), home: Scaffold( appBar: AppBar(title: const Text('Tooltip Cơ Bản')), body: Center( child: Tooltip( message: 'Đây là một nút bấm quan trọng!', child: ElevatedButton( onPressed: () {}, child: const Text('Nhấn tôi'), ), ), ), ), ); } } Giờ, chúng ta sẽ áp dụng TooltipTheme để thay đổi diện mạo của nó. Bạn có thể định nghĩa TooltipTheme ở cấp độ MaterialApp (để áp dụng toàn bộ ứng dụng) hoặc ở một Theme widget cụ thể (để áp dụng cho một phần của cây widget). Ví dụ áp dụng TooltipTheme toàn cục (Global): 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: 'TooltipTheme Demo', theme: ThemeData( primarySwatch: Colors.deepPurple, // Đây rồi, "người tạo mẫu" của chúng ta! tooltipTheme: TooltipThemeData( decoration: BoxDecoration( color: Colors.deepPurpleAccent.shade700, // Màu nền của tooltip borderRadius: BorderRadius.circular(8), // Bo góc border: Border.all(color: Colors.white, width: 1.5), // Viền ), textStyle: const TextStyle( color: Colors.white, // Màu chữ fontSize: 14, // Kích thước chữ fontWeight: FontWeight.bold, // Chữ in đậm ), height: 36, // Chiều cao của tooltip padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), // Đệm bên trong margin: const EdgeInsets.symmetric(horizontal: 16), // Khoảng cách với cạnh màn hình verticalOffset: 48, // Dịch chuyển tooltip theo chiều dọc so với widget gốc preferTooltipsBelow: false, // Ưu tiên hiển thị tooltip phía trên widget waitDuration: const Duration(milliseconds: 500), // Thời gian chờ trước khi hiển thị showDuration: const Duration(seconds: 3), // Thời gian hiển thị ), ), home: Scaffold( appBar: AppBar(title: const Text('TooltipTheme Global')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Tooltip( message: 'Nút này có phong cách mới nè!', child: ElevatedButton( onPressed: () {}, child: const Text('Nút 1'), ), ), const SizedBox(height: 30), Tooltip( message: 'Và nút này cũng vậy luôn!', child: IconButton( icon: const Icon(Icons.info_outline, size: 30), onPressed: () {}, ), ), ], ), ), ), ); } } Thấy chưa? Chỉ với một lần khai báo tooltipTheme trong ThemeData, tất cả các Tooltip trong ứng dụng của bạn sẽ tự động khoác lên mình bộ cánh mới mà bạn đã định nghĩa. Ngầu chưa! Override (ghi đè) TooltipTheme cho từng Tooltip cụ thể: Đôi khi, bạn muốn một tooltip nào đó có phong cách riêng biệt, phá cách một chút. Đơn giản thôi, bạn chỉ cần định nghĩa các thuộc tính trực tiếp trên widget Tooltip đó. Các thuộc tính này sẽ ghi đè lên cài đặt từ TooltipThemeData. // ... (phần MaterialApp và ThemeData giống ví dụ trên) class MyOverrideTooltipScreen extends StatelessWidget { const MyOverrideTooltipScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Override TooltipTheme')), body: Center( child: Tooltip( message: 'Tôi là tooltip đặc biệt!', decoration: BoxDecoration( color: Colors.amber, // Màu nền riêng biệt borderRadius: BorderRadius.circular(15), ), textStyle: const TextStyle( color: Colors.black, // Màu chữ riêng biệt fontSize: 16, fontStyle: FontStyle.italic, ), preferTooltipsBelow: true, // Ưu tiên hiển thị phía dưới child: FloatingActionButton( onPressed: () {}, child: const Icon(Icons.add), ), ), ), ); } } Mẹo hay (Best Practices) từ anh Creyt "Nhất quán là Vua": Giống như việc bạn mặc đồ có phong cách riêng, ứng dụng của bạn cũng cần một phong cách nhất quán. Hãy dùng TooltipTheme ở cấp độ MaterialApp để đảm bảo sự đồng bộ. Chỉ override khi thật sự cần thiết cho một mục đích đặc biệt (ví dụ: tooltip cảnh báo). Đọc được là trên hết: Đừng vì "nghệ thuật" mà chọn màu chữ và màu nền tooltip khó đọc. Đảm bảo độ tương phản cao (ví dụ: chữ trắng trên nền tối, hoặc ngược lại) và kích thước chữ vừa phải. Không ai thích phải nheo mắt đọc "lời thì thầm" cả. "Timing is Everything": Các thuộc tính waitDuration (thời gian chờ trước khi hiển thị) và showDuration (thời gian hiển thị) rất quan trọng. Đặt waitDuration quá ngắn sẽ khiến tooltip xuất hiện "nhảy nhót" gây khó chịu. Quá dài thì người dùng sẽ không biết có tooltip. showDuration quá ngắn thì người dùng chưa kịp đọc, quá dài thì lại che mất nội dung khác. Hãy tìm "điểm vàng" khoảng 500ms cho waitDuration và 1.5s - 3s cho showDuration. Accessibility: Luôn nghĩ đến người dùng có nhu cầu đặc biệt. Đảm bảo kích thước tooltip không quá nhỏ, và cung cấp đủ thông tin mà không làm phiền trải nghiệm của họ. "Lời thì thầm, không phải tiếng hét": Tooltip dùng để bổ sung thông tin, không phải để hướng dẫn chính. Nếu người dùng cần đọc tooltip để hiểu một nút bấm, có lẽ bạn nên xem lại thiết kế icon hoặc nhãn của nút đó. Ứng dụng thực tế: Ai đã dùng "lời thì thầm" này? Bạn có thể thấy tooltip ở khắp mọi nơi, dù đôi khi bạn không để ý: Figma, Photoshop, Google Docs: Di chuột qua các biểu tượng trên thanh công cụ, bạn sẽ thấy một "lời thì thầm" giải thích chức năng của biểu tượng đó (ví dụ: "Undo", "Redo", "Bold"). Các trang thương mại điện tử (Shopee, Lazada): Khi bạn di chuột qua các icon như "Thêm vào giỏ hàng", "Yêu thích", thường sẽ có tooltip hiện ra để xác nhận hành động đó. Hệ điều hành (Windows, macOS): Di chuột qua các icon trên thanh tác vụ/dock, bạn sẽ thấy tên của ứng dụng. Trong các ứng dụng Flutter, đặc biệt là các ứng dụng dành cho desktop hoặc web, TooltipTheme là cực kỳ hữu ích để duy trì sự chuyên nghiệp và đồng nhất cho các thông báo nhỏ này. Thử nghiệm và Nên dùng cho case nào? Anh Creyt đã từng "đau đầu" với việc các tooltip trong ứng dụng trông mỗi nơi một kiểu. Lúc thì màu xanh, lúc thì màu đỏ, font chữ thì lúc to lúc nhỏ, nhìn rất "chợ". Sau đó, khi "khai sáng" ra TooltipTheme, mọi thứ trở nên dễ dàng hơn bao giờ hết. Nên dùng TooltipTheme cho các trường hợp sau: Xây dựng thư viện UI/Component (Design System): Nếu bạn đang xây dựng một bộ component dùng chung cho nhiều dự án, việc định nghĩa TooltipTheme là bắt buộc để đảm bảo các component luôn hiển thị nhất quán. Ứng dụng có số lượng tooltip lớn: Thay vì chỉnh sửa thủ công, TooltipTheme là cứu cánh. Đặc biệt hữu ích cho các ứng dụng quản lý, dashboard, nơi có nhiều biểu tượng và nút cần giải thích. Đảm bảo nhận diện thương hiệu (Branding): Muốn tooltip của bạn có màu sắc và font chữ đúng với brand guideline của công ty? TooltipTheme là câu trả lời. Tăng cường khả năng tiếp cận (Accessibility): Bạn có thể tạo ra một TooltipTheme riêng biệt với kích thước chữ lớn hơn, độ tương phản cao hơn để phục vụ người dùng có thị lực kém. TooltipTheme không chỉ là một công cụ để làm đẹp, mà còn là một phần quan trọng trong việc xây dựng một trải nghiệm người dùng liền mạch và chuyên nghiệp. Hãy tận dụng nó thật hiệu quả nhé các 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é!

43 Đọc tiếp
ToggleButtons Flutter: Chọn Đúng Team, Chill Đúng Vibe!
22/03/2026

ToggleButtons Flutter: Chọn Đúng Team, Chill Đúng Vibe!

Chào các bạn gen Z năng động, lại là anh Creyt đây! Hôm nay, chúng ta sẽ "bóc tách" một em widget cực kỳ thú vị trong Flutter, giúp app của tụi mình flex được nhiều tính năng hơn mà không cần drama: đó là ToggleButtons. Cứ tưởng tượng thế này: Bạn đang ở trong một quán cà phê "chill" hết nấc, và muốn chọn loại sữa cho ly trà sữa của mình. Bình thường thì phải bấm từng cái nút chọn: 'Sữa tươi', 'Sữa đặc', 'Không sữa'… rồi lại phải 'unselect' cái cũ nếu muốn đổi. Mất vibe kinh khủng! ToggleButtons sinh ra để 'auto-chill' vụ này. Nó giống như một nhóm bạn thân, mỗi đứa đại diện cho một lựa chọn. Bạn có thể chọn một đứa, hai đứa, hoặc cả lũ tùy theo rules. Cứ bấm là nó 'toggle' trạng thái: đang chọn thì bỏ chọn, đang không chọn thì chọn. Đơn giản, tiện lợi, và quan trọng là… nhìn nó 'pro' hơn hẳn mấy cái nút bấm đơn lẻ. Tóm lại, ToggleButtons là một widget trong Flutter cho phép bạn hiển thị một nhóm các nút có thể được bật hoặc tắt (toggle). Nó cực kỳ hữu ích khi bạn muốn người dùng chọn một hoặc nhiều tùy chọn từ một danh sách cố định mà các tùy chọn đó có liên quan mật thiết với nhau. ToggleButtons trong Flutter: "Chìa Khóa" Quyết Định Vibe Trong Flutter, ToggleButtons là một widget được thiết kế để hiển thị một hàng các nút liên quan. Mỗi nút có thể được chọn (selected) hoặc không được chọn (unselected). Điểm đặc biệt của nó là bạn phải tự quản lý trạng thái chọn cho từng nút. Các thuộc tính quan trọng nhất mà bạn cần nắm để 'flex' em nó: children: Một list các Widget (thường là Text hoặc Icon) sẽ hiển thị bên trong mỗi nút. Đây chính là 'tụi bạn thân' mà anh Creyt nói đó. isSelected: Một list các bool có độ dài tương ứng với children. Mỗi bool sẽ cho Flutter biết nút tương ứng có đang được chọn hay không. Đây là 'trạng thái' của từng đứa bạn. onPressed: Một callback function được gọi khi một nút được nhấn. Trong hàm này, bạn sẽ cập nhật trạng thái isSelected của mình. Đây là 'hành động' khi bạn 'chạm' vào đứa bạn đó. color, selectedColor, fillColor, splashColor, borderColor, selectedBorderColor, borderRadius: Các thuộc tính để 'tút tát' cho em nó đẹp trai, đẹp gái hơn. Code Ví Dụ Minh Họa: Chọn Phong Cách Âm Nhạc Giờ thì, lý thuyết suông mãi cũng chán. Chúng ta sẽ cùng nhau viết một ví dụ 'sương sương' để thấy ToggleButtons hoạt động như thế nào trong thực tế. Chúng ta sẽ tạo một nhóm nút chọn 'Phong cách âm nhạc yêu thích' 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 ToggleButtons Demo', theme: ThemeData( primarySwatch: Colors.blueGrey, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // Đây là "trạng thái" của từng nút. // Mặc định, tất cả đều false (không được chọn). List<bool> _selections = List.generate(3, (_) => false); final List<String> _musicStyles = ['Pop', 'Rock', 'EDM']; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Chọn Phong Cách Âm Nhạc (ToggleButtons)'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Chọn phong cách âm nhạc yêu thích của bạn:', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), ToggleButtons( // List các widget con (thường là Text hoặc Icon) children: _musicStyles.map((style) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text(style), )).toList(), // Trạng thái hiện tại của từng nút isSelected: _selections, // Hàm được gọi khi một nút được nhấn onPressed: (int index) { // Rất quan trọng: Phải gọi setState để cập nhật UI setState(() { // Đảo ngược trạng thái của nút được nhấn _selections[index] = !_selections[index]; }); // In ra các lựa chọn hiện tại để debug/kiểm tra print('Các lựa chọn hiện tại: ${ _selections.map((e) => e ? 'Selected' : 'Unselected').toList()}'); }, // Tùy chỉnh giao diện (styling) color: Colors.grey[600], // Màu chữ/icon khi không chọn selectedColor: Colors.white, // Màu chữ/icon khi được chọn fillColor: Colors.blueGrey, // Màu nền khi được chọn borderColor: Colors.blueGrey.shade200, // Màu viền selectedBorderColor: Colors.blueGrey.shade800, // Màu viền khi được chọn borderRadius: BorderRadius.circular(8), // Bo góc borderWidth: 2, ), const SizedBox(height: 30), Text( 'Bạn đã chọn: ${ _musicStyles .asMap() .entries .where((entry) => _selections[entry.key]) .map((entry) => entry.value) .join(', ') }', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), ), ); } } Mẹo Hay và Best Practices từ Giảng Viên Creyt Thấy chưa, code Flutter nó 'flex' dễ hiểu vậy đó. Giờ thì, nghe anh Creyt 'deep dive' thêm vài mẹo để tụi mình không bị 'nghiệp quật' khi dùng ToggleButtons nhé: Quản lý State là linh hồn: ToggleButtons là một widget "stateful" (có trạng thái). Điều này có nghĩa là bạn PHẢI quản lý trạng thái isSelected của nó bên ngoài widget, thường là trong một StatefulWidget và cập nhật nó bằng setState(). Nếu không, nút sẽ không thay đổi trạng thái khi bạn nhấn vào đâu. Nó giống như bạn bấm nút mà máy không nhận lệnh vậy, 'phèn' lắm! Độ dài isSelected và children phải khớp: Đây là lỗi sơ đẳng mà nhiều bạn hay mắc phải. Số lượng bool trong isSelected PHẢI BẰNG số lượng Widget trong children. Nếu không, Flutter sẽ "giận dỗi" và ném lỗi ngay. Cứ tưởng tượng bạn có 3 đứa bạn mà chỉ có 2 cái ghế để ngồi vậy. Styling đồng bộ: Dùng các thuộc tính như color, selectedColor, fillColor một cách nhất quán để tạo ra một UI "hợp gu", dễ nhìn. Đừng để mỗi nút một màu, nhìn nó 'ô dề' lắm. Accessibility (Khả năng tiếp cận): Luôn đảm bảo các nút của bạn có đủ tương phản màu sắc và kích thước dễ bấm. Người dùng có thị lực kém hoặc gặp khó khăn về vận động cũng cần được 'chill' khi dùng app của bạn chứ. Khi nào thì chọn 1, khi nào thì chọn nhiều? Chọn 1 (Single Selection): Nếu chỉ muốn người dùng chọn DUY NHẤT một tùy chọn (ví dụ: chọn giới tính, chọn đơn vị tiền tệ chính), thì trong hàm onPressed, bạn phải reset tất cả các giá trị trong _selections về false, rồi mới set _selections[index] thành true. Chọn nhiều (Multiple Selection): Như ví dụ trên, chỉ cần đảo ngược trạng thái của nút được nhấn (_selections[index] = !_selections[index]). Ghi nhớ: Cứ nhớ ToggleButtons là "nhóm bạn thân" nhiều lựa chọn. Mỗi đứa bạn có một "trạng thái" (isSelected) và khi bạn "tương tác" (onPressed) với đứa nào thì đứa đó sẽ "thay đổi mood" (setState). Ứng Dụng Thực Tế: "Flex" Khắp Nơi! Vậy thì, ngoài việc chọn nhạc, ToggleButtons còn được các app 'xịn xò' dùng ở đâu nữa? App chỉnh sửa ảnh/video: Chọn các bộ lọc (filters) khác nhau (ví dụ: "Vintage", "B&W", "Sepia"). Bạn có thể chọn nhiều bộ lọc để kết hợp. Ứng dụng thời tiết: Chọn đơn vị nhiệt độ (C/F), đơn vị gió (km/h, m/s). Thường là chọn 1. App mua sắm/tìm kiếm: Bộ lọc sản phẩm (ví dụ: "Size S", "Màu Đỏ", "Còn hàng"). Người dùng có thể chọn nhiều tiêu chí. Trình soạn thảo văn bản: Các nút định dạng văn bản như B (Bold), I (Italic), U (Underline), căn lề (Trái, Giữa, Phải). Đây là một ví dụ kinh điển của ToggleButtons, mỗi nút có thể bật/tắt độc lập. Cài đặt: Bật/tắt các tùy chọn riêng lẻ hoặc nhóm các tùy chọn liên quan. Nên Dùng Khi Nào và "Né" Khi Nào? Anh Creyt đã từng 'thử nghiệm' nhiều với ToggleButtons và nhận ra nó thực sự là 'cứu cánh' trong các trường hợp sau: Khi bạn có một nhóm lựa chọn nhỏ (khoảng 2-5 tùy chọn) và các tùy chọn đó có liên quan chặt chẽ đến nhau. Ví dụ: chọn chế độ xem (Grid/List), chọn đơn vị đo lường, chọn ngôn ngữ hiển thị (nếu chỉ có 2-3 ngôn ngữ chính). Khi bạn muốn người dùng dễ dàng nhìn thấy tất cả các tùy chọn cùng một lúc mà không cần mở một menu dropdown. Nó giúp giảm số lần click và tăng trải nghiệm người dùng. Khi bạn cần một UI rõ ràng, trực quan cho các tùy chọn bật/tắt. Không nên dùng khi nào? Khi có quá nhiều tùy chọn (hơn 5-6): Lúc này ToggleButtons sẽ chiếm quá nhiều không gian trên màn hình và trông rất 'rối'. Hãy nghĩ đến DropdownButton, RadioListTile (nếu chọn 1) hoặc CheckboxListTile (nếu chọn nhiều) thay thế. Khi các tùy chọn không liên quan đến nhau: Mỗi tùy chọn nên là một Switch hoặc Checkbox riêng lẻ. Khi bạn cần chọn từ một danh sách động: ToggleButtons hoạt động tốt nhất với danh sách cố định. Vậy đó, các bạn gen Z! Hi vọng qua bài giảng 'sương sương' này, tụi mình đã 'nắm vibe' được ToggleButtons trong Flutter rồi nhé. Cứ thực hành nhiều vào, có gì 'bí' thì cứ 'ới' anh Creyt. Chúc các bạn code 'mượt' như lụa! 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é!

36 Đọc tiếp