Chuyên mục

Flutter

Flutter tutolrial

47 bài viết
GestureRecognizer: Khi Ứng Dụng Của Bạn Biết Đọc Suy Nghĩ Của Người Dùng
18/03/2026

GestureRecognizer: Khi Ứng Dụng Của Bạn Biết Đọc Suy Nghĩ Của Người Dùng

Chào các lập trình viên tương lai, đây là Creyt. Hôm nay, chúng ta sẽ lặn sâu vào một khái niệm tuy cơ bản mà lại cực kỳ mạnh mẽ trong Flutter, đó là GestureRecognizer. Hãy hình dung thế này: bạn đang ở một nhà hàng sang trọng, và ứng dụng của bạn chính là anh phục vụ tận tâm. Khách hàng (người dùng) không phải lúc nào cũng nói to "Tôi muốn món A" hay "Tôi muốn đi đến trang B". Đôi khi, họ chỉ vẫy tay nhẹ, gật đầu, hoặc thậm chí là một cái nháy mắt tinh quái. GestureRecognizer chính là đôi mắt tinh tường và bộ não phân tích của anh phục vụ đó, giúp ứng dụng của bạn "đọc hiểu" những tín hiệu phi ngôn ngữ từ người dùng. 1. GestureRecognizer Là Gì và Để Làm Gì? Trong thế giới Flutter, các widget của chúng ta thường rất "ngoan hiền", chúng chỉ làm những gì được bảo. Một Text thì hiển thị chữ, một Image thì hiển thị hình ảnh. Nhưng để biến một giao diện tĩnh thành một trải nghiệm tương tác sống động, chúng ta cần một cơ chế để phát hiện và phản ứng lại các cử chỉ của người dùng: từ những cú chạm nhẹ, vuốt ngang dọc, kéo thả, cho đến những cái chụm hai ngón tay để phóng to. GestureRecognizer là một lớp trừu tượng (abstract class) trong Flutter, đóng vai trò như một bộ phân tích các sự kiện con trỏ (pointer events) thô từ hệ điều hành và biến chúng thành các "cử chỉ" có ý nghĩa. Nghe có vẻ phức tạp, nhưng may mắn thay, Flutter đã cung cấp cho chúng ta một widget "bao bọc" cực kỳ tiện lợi để sử dụng hầu hết các GestureRecognizer phổ biến: đó là GestureDetector. GestureDetector giống như một "vệ sĩ" chuyên nghiệp đứng canh gác một khu vực trên màn hình của bạn. Bất cứ khi nào có một cử chỉ được thực hiện trong khu vực đó, GestureDetector sẽ bắt lấy, phân tích và kích hoạt các hành động bạn đã định nghĩa. Nó giúp chúng ta lắng nghe đủ loại "ngôn ngữ cơ thể" của người dùng: Taps: onTap, onDoubleTap, onLongPress (chạm, chạm đúp, giữ lâu) Drags: onHorizontalDragStart, onVerticalDragUpdate, onPanEnd (kéo ngang, cập nhật kéo dọc, kết thúc kéo nói chung) Scales: onScaleStart, onScaleUpdate, onScaleEnd (bắt đầu, cập nhật, kết thúc phóng to/thu nhỏ) Và rất nhiều loại cử chỉ khác nữa! 2. Code Ví Dụ Minh Hoạ: Khi Chiếc Hộp Biết Phản Ứng Hãy cùng xem một ví dụ đơn giản nhưng đầy đủ để hiểu cách GestureDetector hoạt động. Chúng ta sẽ tạo một cái hộp nhỏ, và nó sẽ thay đổi màu sắc khi bạn chạm vào, in ra thông báo khi bạn giữ lâu, và di chuyển khi bạn kéo 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: 'GestureRecognizer Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const GestureDemoScreen(), ); } } class GestureDemoScreen extends StatefulWidget { const GestureDemoScreen({super.key}); @override State<GestureDemoScreen> createState() => _GestureDemoScreenState(); } class _GestureDemoScreenState extends State<GestureDemoScreen> { Color _boxColor = Colors.blue; String _message = 'Chạm, giữ hoặc kéo tôi!'; Offset _offset = const Offset(0.0, 0.0); // Vị trí của hộp @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('GestureDetector Tuyệt Vời'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _message, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 30), // Sử dụng Transform.translate để di chuyển hộp Transform.translate( offset: _offset, child: GestureDetector( // Khi chạm (tap) onTap: () { setState(() { _boxColor = _boxColor == Colors.blue ? Colors.red : Colors.blue; _message = 'Bạn vừa chạm tôi!'; }); print('Hộp đã được chạm!'); }, // Khi giữ lâu (long press) onLongPress: () { setState(() { _message = 'Bạn đã giữ tôi lâu quá!'; }); print('Hộp đã được giữ lâu!'); }, // Khi bắt đầu kéo (pan start) onPanStart: (details) { print('Bắt đầu kéo tại: ${details.localPosition}'); setState(() { _message = 'Bạn đang kéo tôi!'; }); }, // Khi đang kéo (pan update) onPanUpdate: (details) { setState(() { _offset += details.delta; // Cập nhật vị trí theo sự thay đổi của con trỏ }); print('Đang kéo, vị trí hiện tại: $_offset'); }, // Khi kết thúc kéo (pan end) onPanEnd: (details) { print('Kết thúc kéo.'); setState(() { _message = 'Bạn vừa kéo tôi xong!'; }); }, child: Container( width: 150, height: 150, color: _boxColor, alignment: Alignment.center, child: const Text( 'Chạm Tôi', style: TextStyle(color: Colors.white, fontSize: 18), ), ), ), ), ], ), ), ); } } Trong ví dụ này: Chúng ta bọc một Container bằng GestureDetector. onTap thay đổi màu sắc của hộp. onLongPress cập nhật thông báo. onPanStart, onPanUpdate, onPanEnd cùng nhau tạo hiệu ứng kéo thả cho hộp bằng cách cập nhật _offset và sử dụng Transform.translate. details.delta là sự thay đổi vị trí của con trỏ kể từ lần cập nhật gần nhất – một công cụ tuyệt vời để tạo hiệu ứng kéo mượt mà. 3. Mẹo Hay (Best Practices) Từ Creyt Để trở thành một "thầy phù thủy" điều khiển cử chỉ, hãy ghi nhớ vài lời khuyên này: Rõ ràng là Vua (Specificity is King): Đừng cố gắng "nhận diện" một cú vuốt bằng onTap. Mỗi GestureRecognizer được thiết kế để bắt một loại cử chỉ cụ thể. Sử dụng đúng công cụ cho đúng việc sẽ giúp mã của bạn sạch sẽ hơn và tránh các lỗi hành vi không mong muốn. Phản hồi là Bạn (Provide Feedback): Người dùng cần biết rằng hành động của họ đã được ứng dụng ghi nhận. Khi một cử chỉ được phát hiện, hãy cung cấp phản hồi trực quan ngay lập tức: đổi màu, phóng to, rung nhẹ, hoặc một animation tinh tế. Điều này giống như khi bạn gật đầu xác nhận với khách hàng rằng bạn đã nghe thấy yêu cầu của họ vậy. Cẩn thận với Hệ thống Phân cấp (Beware of Hierarchy): GestureDetector có thể được lồng vào nhau. Nếu một widget con có GestureDetector và widget cha cũng có, thì thường cử chỉ sẽ được xử lý bởi widget con trước. Nếu bạn muốn xử lý các sự kiện con trỏ thô (ví dụ, để chặn sự kiện lan truyền lên cha), hãy tìm hiểu về Listener widget, nó là một cấp độ thấp hơn GestureDetector. Không lạm dụng (Don't Overdo It): Không phải mọi widget đều cần một GestureDetector. Chỉ sử dụng khi bạn thực sự cần tương tác phức tạp. Việc đặt quá nhiều GestureDetector có thể gây ra hiệu suất không cần thiết và đôi khi là xung đột cử chỉ. Tạo cử chỉ Tùy chỉnh (Custom Recognizers - Nâng cao): Đối với những cử chỉ thực sự độc đáo, bạn hoàn toàn có thể tự tạo GestureRecognizer của riêng mình bằng cách kế thừa từ OneSequenceGestureRecognizer hoặc MultiDragGestureRecognizer. Nhưng đó là câu chuyện của một buổi học nâng cao hơn, khi bạn đã là một "phù thủy" cử chỉ thực thụ rồi! 4. Ứng Dụng Thực Tế: GestureRecognizer Ở Khắp Mọi Nơi Bạn có thể không nhận ra, nhưng GestureRecognizer đang hoạt động miệt mài trong hầu hết các ứng dụng di động bạn sử dụng hàng ngày: Mạng xã hội (Facebook, Instagram, TikTok): Vuốt lên/xuống để xem bài đăng mới, vuốt ngang để xem Stories, chạm đúp để "thả tim" (like), kéo để làm mới (pull-to-refresh). Tất cả đều là nhờ GestureRecognizer hoặc các widget được xây dựng trên đó. Ứng dụng bản đồ (Google Maps, Apple Maps): Chụm hai ngón tay để phóng to/thu nhỏ (pinch-to-zoom), kéo để di chuyển bản đồ (pan), xoay bản đồ bằng hai ngón tay. Đây là những ví dụ điển hình của các cử chỉ phức tạp. Thư viện ảnh: Vuốt ngang để chuyển ảnh, chụm để phóng to/thu nhỏ ảnh. Game di động: Nhiều game sử dụng cử chỉ kéo thả, chạm giữ hoặc các chuỗi cử chỉ phức tạp để điều khiển nhân vật hay tương tác với vật phẩm. Tóm lại, GestureRecognizer không chỉ là một công cụ, nó là "giọng nói" của ứng dụng, giúp ứng dụng không chỉ hiển thị mà còn "lắng nghe" và "phản hồi" lại người dùng một cách thông minh và tinh tế. Hãy làm chủ nó, và bạn sẽ mở ra một thế giới mới của trải nghiệm người dùng tuyệt vời! 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é!

0 Đọc tiếp
FractionallySizedBox: Kỹ Thuật 'Đo Vải' Tinh Tế Cho UI Flutter
18/03/2026

FractionallySizedBox: Kỹ Thuật 'Đo Vải' Tinh Tế Cho UI Flutter

Chào các chiến hữu lập trình! Hôm nay, chúng ta sẽ lặn sâu vào một viên ngọc ẩn của Flutter, thứ mà nhiều bạn thường bỏ qua nhưng lại cực kỳ quyền năng trong việc xây dựng giao diện người dùng linh hoạt, đó là FractionallySizedBox. Cứ hình dung thế này: trong thế giới lập trình UI, đôi khi bạn cần một widget không phải to đúng 'X pixels' hay 'Y pixels' cố định. Mà bạn lại muốn nó to bằng 'một nửa không gian cha nó' hay 'một phần ba chiều cao của cái màn hình ấy'. Nó giống như bạn đi may quần áo vậy, thay vì nói 'cái ống quần này rộng 20cm', bạn nói 'nó rộng bằng 30% vòng đùi của tôi'. Đấy, cái '30%' ấy chính là linh hồn của FractionallySizedBox! Vậy, FractionallySizedBox làm gì? Đơn giản là nó cho phép bạn định nghĩa kích thước của widget con (child) DỰA TRÊN tỷ lệ phần trăm của kích thước widget cha (parent) có sẵn. Nó không tự tạo ra không gian, mà nó 'thò tay' vào cái không gian mà thằng cha nó đã cấp cho nó, rồi 'cắt' ra một phần theo đúng tỷ lệ bạn muốn cho thằng con. Nó có hai thuộc tính chính, như hai cái kéo sắc bén để bạn cắt vải vậy: widthFactor: Cái này quyết định chiều rộng của widget con sẽ bằng bao nhiêu phần của chiều rộng widget cha. Giá trị từ 0.0 (rộng 0%) đến 1.0 (rộng 100%). heightFactor: Tương tự, nhưng là cho chiều cao. Từ 0.0 đến 1.0. Nếu bạn chỉ định một trong hai (hoặc cả hai), thằng con sẽ được co giãn theo tỷ lệ đó. Nếu bạn không chỉ định, nó sẽ mặc định là null, tức là không ảnh hưởng đến kích thước đó, để thằng con tự quyết hoặc để thằng cha quyết định. Nói nhiều không bằng làm một phát ăn ngay! Hãy xem ví dụ này để thấy nó hoạt động như thế nào trong thực tế: import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'FractionallySizedBox Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('FractionallySizedBox của Creyt'), ), body: Center( child: Container( color: Colors.grey[300], // Màu nền của widget cha để dễ hình dung width: 300, // Chiều rộng cố định của cha height: 300, // Chiều cao cố định của cha child: FractionallySizedBox( widthFactor: 0.75, // Con chiếm 75% chiều rộng của cha heightFactor: 0.5, // Con chiếm 50% chiều cao của cha child: Container( color: Colors.deepPurple, // Màu của widget con child: const Center( child: Text( 'Tôi là con, tôi chiếm 75% rộng và 50% cao của cha!', textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 16), ), ), ), ), ), ), ); } } Trong ví dụ trên, cái Container màu tím sẽ chiếm 75% chiều rộng và 50% chiều cao của cái Container màu xám. Mặc dù thằng cha màu xám có kích thước cố định, nhưng thằng con màu tím lại 'nhìn' vào kích thước đó và tự điều chỉnh theo tỷ lệ. Ngon lành cành đào! Rồi, giờ là vài chiêu thức 'phòng the' để các bạn dùng FractionallySizedBox cho nó pro: Hiểu rõ 'Cha' của bạn: FractionallySizedBox cần một widget cha có ràng buộc kích thước (constraints) rõ ràng. Nếu cha nó là một cái Column hay Row (mà không có Expanded hay Flexible đi kèm), hoặc một ListView không giới hạn kích thước, thì FractionallySizedBox sẽ không biết '100%' là bao nhiêu mà tính toán. Nó sẽ 'bối rối' và có thể ném lỗi hoặc không hoạt động như ý. Luôn đảm bảo cha nó cung cấp một không gian hữu hạn để nó 'cắt'. Kết hợp với Align hoặc Center: FractionallySizedBox chỉ lo chuyện kích thước, nó không quan tâm đến vị trí của thằng con. Nếu bạn muốn thằng con nằm giữa cái không gian mà FractionallySizedBox đã 'cắt' ra, hãy bọc nó trong Center hoặc dùng alignment của FractionallySizedBox (mặc định là Alignment.center). Dùng cho Responsive Design: Đây chính là 'sân nhà' của nó! Khi bạn muốn một thành phần UI tự động co giãn theo kích thước màn hình (mà kích thước màn hình là cha của mọi thứ), FractionallySizedBox là một lựa chọn tuyệt vời. Ví dụ, một banner chiếm 80% chiều rộng màn hình, bất kể màn hình to hay nhỏ. Không phải lúc nào cũng là giải pháp: Đừng lạm dụng nó. Đôi khi Expanded, Flexible, hoặc đơn giản là SizedBox với kích thước cố định lại là lựa chọn tốt hơn, tùy vào ngữ cảnh. FractionallySizedBox là cho các trường hợp bạn cần sizing theo TỶ LỆ. Giờ thì, ứng dụng thực tế nó ở đâu? Không phải chỉ trên sách vở đâu nha: Bảng điều khiển (Dashboards): Tưởng tượng một dashboard với các card thông tin. Bạn muốn mỗi card chiếm 30% chiều rộng của hàng, hoặc một biểu đồ chiếm 60% chiều cao của khu vực hiển thị. FractionallySizedBox là 'tay chơi' chính ở đây. Thanh tiến độ (Progress Bars): Một thanh tiến độ thường có phần 'đã hoàn thành' chiếm một tỷ lệ nhất định của tổng chiều dài thanh. Dễ dàng dùng FractionallySizedBox để điều khiển chiều rộng của phần 'đã hoàn thành' theo một value từ 0.0 đến 1.0. Layout lưới ảnh (Image Grids): Bạn muốn mỗi ảnh trong một hàng chiếm 1/3 chiều rộng màn hình (trừ padding)? Dùng FractionallySizedBox kết hợp với GridView hoặc Row là ra ngay. Các thành phần UI đáp ứng (Responsive UI Components): Bất cứ khi nào bạn có một component mà kích thước của nó cần thay đổi tỷ lệ thuận với kích thước của parent (mà parent có thể là toàn bộ màn hình), FractionallySizedBox là một công cụ cực kỳ hữu ích. Ví dụ, một nút bấm chiếm 70% chiều rộng của một card. Tóm lại, FractionallySizedBox là một công cụ mạnh mẽ trong bộ đồ nghề của lập trình viên Flutter, giúp bạn tạo ra những giao diện linh hoạt, thích ứng tốt với mọi kích thước màn hình. Nắm vững nó, bạn sẽ có thêm một 'vũ khí' lợi hại để chinh phục thế giới UI/UX đấy! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

1 Đọc tiếp
FractionalTranslation: Dịch Chuyển Linh Hoạt Trong Flutter
18/03/2026

FractionalTranslation: Dịch Chuyển Linh Hoạt Trong Flutter

Chào các "thợ code" tương lai, hôm nay chúng ta sẽ mổ xẻ một công cụ khá hay ho trong hộp đồ nghề Flutter mà nhiều khi các bạn bỏ qua: FractionalTranslation. Nghe tên có vẻ học thuật, nhưng thực ra nó là một "chiếc đòn bẩy" cực kỳ linh hoạt để dịch chuyển các widget của chúng ta. FractionalTranslation là gì và để làm gì? Thầy Creyt hay ví von thế này: Bạn có một bức tranh treo tường. Bình thường, bạn sẽ nói "đẩy bức tranh sang phải 10cm" đúng không? Đó là cách chúng ta dùng Transform.translate hoặc Positioned với các giá trị tuyệt đối (pixel). Nhưng nếu bạn muốn nói "đẩy bức tranh sang phải một nửa chiều rộng của chính nó", hoặc "kéo nó lên trên một phần tư chiều cao của nó" thì sao? Đó chính là lúc FractionalTranslation tỏa sáng! FractionalTranslation là một widget trong Flutter cho phép bạn dịch chuyển con của nó (child widget) tương đối so với kích thước của chính con đó. Thay vì dùng pixel, bạn dùng các giá trị phân số (fraction) từ 0.0 đến 1.0 (hoặc hơn) để định vị. Để làm gì ư? Nó cực kỳ hữu ích khi bạn muốn tạo ra các hiệu ứng UI động, responsive, hay các animation mà vị trí dịch chuyển cần phải tự động điều chỉnh theo kích thước của widget. Ví dụ, một menu trượt vào từ cạnh màn hình, một thành phần UI tự động căn chỉnh khi kích thước màn hình thay đổi, hoặc hiệu ứng parallax tinh tế. Thuộc tính chính của nó là translation, nhận một đối tượng Offset. Offset(0.5, 0): Dịch sang phải 50% chiều rộng của child. Offset(-0.25, 0): Dịch sang trái 25% chiều rộng của child. Offset(0, 1.0): Dịch xuống dưới 100% chiều cao của child. Offset(0.5, 0.5): Dịch sang phải 50% chiều rộng VÀ xuống dưới 50% chiều cao của child. Code Ví Dụ Minh Họa: Một Widget "Lướt" Vào Mượt Mà Để các bạn dễ hình dung, chúng ta sẽ tạo một widget đơn giản, khi bấm nút thì nó sẽ "lướt" từ bên ngoài vào giữa màn hình, rồi lướt ra khi bấm lại. Toàn bộ quá trình dịch chuyển sẽ dựa trên kích thước của chính 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: 'FractionalTranslation Demo', theme: ThemeData(primarySwatch: Colors.blueGrey), home: const FractionalTranslationScreen(), ); } } class FractionalTranslationScreen extends StatefulWidget { const FractionalTranslationScreen({super.key}); @override State<FractionalTranslationScreen> createState() => _FractionalTranslationScreenState(); } class _FractionalTranslationScreenState extends State<FractionalTranslationScreen> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Offset> _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); // Animation starts from Offscreen (1.0 means 100% of its width to the right) // and ends at its original position (0.0). _animation = Tween<Offset>( begin: const Offset(1.0, 0.0), // Start 100% of its width to the right end: Offset.zero, // End at its original position ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleAnimation() { if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('FractionalTranslation Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Sử dụng AnimatedBuilder để rebuild widget khi animation thay đổi giá trị AnimatedBuilder( animation: _animation, builder: (context, child) { return FractionalTranslation( translation: _animation.value, // Giá trị offset thay đổi theo animation child: Material( elevation: 8.0, borderRadius: BorderRadius.circular(12.0), child: Container( width: 200, // Kích thước cố định để dễ hình dung height: 100, padding: const EdgeInsets.all(16.0), alignment: Alignment.center, decoration: BoxDecoration( color: Colors.teal.shade300, borderRadius: BorderRadius.circular(12.0), ), child: const Text( 'Xin chào, tôi lướt đây!', style: TextStyle(color: Colors.white, fontSize: 16), ), ), ), ); }, ), const SizedBox(height: 40), ElevatedButton( onPressed: _toggleAnimation, child: const Text('Bật/Tắt Hiệu Ứng Lướt'), ), ], ), ), ); } } Giải thích: Chúng ta dùng AnimationController để điều khiển tiến trình animation. Tween<Offset> được tạo với begin: const Offset(1.0, 0.0) và end: Offset.zero. Điều này có nghĩa là widget sẽ bắt đầu dịch chuyển từ vị trí 100% chiều rộng của nó sang phải (ngoài màn hình, nếu nó nằm trong một Row hoặc Stack lớn hơn) và kết thúc ở vị trí ban đầu của nó (không dịch chuyển). AnimatedBuilder lắng nghe sự thay đổi của _animation và rebuild FractionalTranslation với giá trị translation mới. FractionalTranslation nhận _animation.value làm thuộc tính translation, khiến Container con của nó dịch chuyển mượt mà. Mẹo Hay và Best Practices từ Thầy Creyt Nghĩ theo tỷ lệ, không phải pixel: Đây là "bí kíp" lớn nhất. Khi bạn cần một widget dịch chuyển một cách tương đối với chính nó hoặc với một container lớn hơn, hãy nghĩ ngay đến FractionalTranslation. Nó giúp code của bạn linh hoạt và dễ bảo trì hơn rất nhiều khi giao diện thay đổi kích thước. Kết hợp với Animation: FractionalTranslation "sinh ra" là để làm bạn với các animation. Sử dụng AnimatedBuilder hoặc TweenAnimationBuilder để tạo ra các hiệu ứng dịch chuyển mượt mà, tự nhiên. Cẩn thận với Clipping: Đôi khi, khi dịch chuyển một widget ra khỏi giới hạn của cha nó, bạn có thể thấy nó bị cắt (clipped). Nếu muốn nó vẫn hiển thị đầy đủ, hãy đảm bảo widget cha không có thuộc tính clipBehavior là Clip.hardEdge hoặc bạn có thể bọc nó trong OverflowBox nếu cần hiển thị ngoài giới hạn. So sánh với Transform.translate: Transform.translate cũng dịch chuyển widget, nhưng nó dùng giá trị pixel tuyệt đối. FractionalTranslation dùng giá trị phân số. Chọn cái nào tùy thuộc vào yêu cầu: dịch chuyển cố định một lượng pixel hay dịch chuyển tương đối theo kích thước. Ứng Dụng Thực Tế FractionalTranslation (hoặc các kỹ thuật tương tự dựa trên dịch chuyển tương đối) được ứng dụng rất nhiều trong các sản phẩm thực tế: Hiệu ứng Parallax Scrolling: Trong các trang web hoặc ứng dụng có hiệu ứng cuộn parallax, các lớp nội dung khác nhau sẽ di chuyển với tốc độ (tỷ lệ) khác nhau khi người dùng cuộn. FractionalTranslation có thể giúp mô phỏng điều này bằng cách dịch chuyển các lớp dựa trên vị trí cuộn và kích thước của chúng. Slide-in Menus/Drawers: Mặc dù Flutter có Drawer widget riêng, nhưng để tạo các menu tùy chỉnh trượt vào từ cạnh màn hình (như các ứng dụng tin tức, mạng xã hội) với hiệu ứng tinh tế, FractionalTranslation có thể được dùng để kiểm soát vị trí trượt dựa trên chiều rộng của menu. Onboarding Screens/Walkthroughs: Khi bạn thấy các phần tử UI dịch chuyển vào/ra màn hình một cách mượt mà trong các màn hình giới thiệu ứng dụng lần đầu, đó thường là sự kết hợp của animation và các kỹ thuật dịch chuyển tương đối. Responsive UI Elements: Trong một bố cục responsive, bạn có thể muốn một nút bấm hoặc một banner quảng cáo dịch chuyển một khoảng nhất định tính theo tỷ lệ của màn hình hoặc của chính nó, thay vì một giá trị pixel cố định có thể bị lệch trên các thiết bị khác nhau. Nhớ nhé, lập trình không chỉ là viết code, mà là "điêu khắc" logic và giao diện. FractionalTranslation là một trong những "dụng cụ" tinh xảo giúp bạn làm điều đó. Chúc các bạn code vui vẻ! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

35 Đọc tiếp
FormState trong Flutter: Bếp Trưởng Quyền Lực Quản Lý Đơn Hàng Dữ Liệu!
18/03/2026

FormState trong Flutter: Bếp Trưởng Quyền Lực Quản Lý Đơn Hàng Dữ Liệu!

Chào mừng các bạn đến với buổi học đầy năng lượng hôm nay! Tôi là Creyt, và hôm nay chúng ta sẽ cùng khám phá một khái niệm cực kỳ quan trọng trong Flutter khi bạn làm việc với các biểu mẫu: FormState. FormState Là Gì? Để Làm Gì? Để dễ hình dung, các bạn hãy tưởng tượng thế này nhé: Một ứng dụng di động giống như một nhà hàng lớn, và mỗi khi người dùng cần nhập thông tin – từ đăng nhập, đăng ký, điền địa chỉ giao hàng, hay thậm chí là cài đặt tùy chỉnh – đó chính là một đơn đặt hàng (một cái Form). Trong cái nhà hàng này, mỗi món ăn (mỗi trường nhập liệu như email, mật khẩu) cần phải được chế biến đúng cách, tuân thủ các quy tắc an toàn vệ sinh thực phẩm (validation). Và ai là người quản lý tất cả các đơn hàng, đảm bảo chúng được chuẩn bị đúng, đầy đủ, và sẵn sàng để phục vụ khách hàng (gửi đi) một cách trơn tru nhất? Đó chính là Bếp Trưởng FormState của chúng ta! Nói một cách kỹ thuật hơn, FormState là một lớp (class) trong Flutter chịu trách nhiệm quản lý trạng thái của một widget Form. Nó cung cấp các phương thức để: Xác thực đồng bộ (Validate): Kiểm tra xem TẤT CẢ các trường nhập liệu con trong Form có hợp lệ hay không. Giống như Bếp trưởng kiểm tra từng nguyên liệu, từng món ăn nhỏ trước khi món chính được hoàn thành. Lưu dữ liệu (Save): Thu thập dữ liệu từ tất cả các trường nhập liệu con đã được xác thực. Sau khi món ăn đạt chuẩn, Bếp trưởng sẽ tổng hợp lại để đưa ra cho phục vụ. Thiết lập lại (Reset): Xóa trắng hoặc đưa các trường nhập liệu về trạng thái ban đầu. Đơn giản là dọn dẹp quầy bếp sau khi phục vụ xong một đơn hàng. Code Ví Dụ Minh Họa: Form Đăng Nhập Của Chúng Ta Để thấy rõ sức mạnh của Bếp Trưởng FormState, chúng ta hãy xây dựng một form đăng nhập đơn giản với hai trường: Email và Mật khẩu. import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'FormState Demo by Creyt', theme: ThemeData(primarySwatch: Colors.blue), home: const LoginForm(), ); } } class LoginForm extends StatefulWidget { const LoginForm({super.key}); @override State<LoginForm> createState() => _LoginFormState(); } class _LoginFormState extends State<LoginForm> { // Bước 1: Tạo một GlobalKey để truy cập FormState. // Đây chính là 'chiếc bảng kẹp giấy' của Bếp trưởng! final _formKey = GlobalKey<FormState>(); String _email = ''; String _password = ''; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Đăng nhập cùng Creyt')), body: Padding( padding: const EdgeInsets.all(16.0), child: // Bước 2: Bọc các trường nhập liệu trong widget Form. // Đây là 'khu vực bếp' nơi các món ăn được chuẩn bị. Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ TextFormField( decoration: const InputDecoration( labelText: 'Email', hintText: 'Nhập email của bạn', border: OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, // Bước 3: Định nghĩa hàm validator cho từng trường. // Đây là 'kiểm tra chất lượng' cho từng nguyên liệu. validator: (value) { if (value == null || value.isEmpty) { return 'Email không được để trống!'; } if (!value.contains('@')) { return 'Email không hợp lệ!'; } return null; // Trả về null nếu hợp lệ }, // Bước 4: Định nghĩa hàm onSaved để lưu giá trị. // Sau khi nguyên liệu đạt chuẩn, Bếp trưởng ghi lại. onSaved: (value) { _email = value!; }, ), const SizedBox(height: 16.0), TextFormField( decoration: const InputDecoration( labelText: 'Mật khẩu', hintText: 'Nhập mật khẩu của bạn', border: OutlineInputBorder(), ), obscureText: true, validator: (value) { if (value == null || value.isEmpty) { return 'Mật khẩu không được để trống!'; } if (value.length < 6) { return 'Mật khẩu phải ít nhất 6 ký tự!'; } return null; }, onSaved: (value) { _password = value!; }, ), const SizedBox(height: 24.0), ElevatedButton( onPressed: () { // Bước 5: Sử dụng _formKey để truy cập FormState và xác thực. // Bếp trưởng ra lệnh: 'Kiểm tra tất cả các món ăn!' if (_formKey.currentState!.validate()) { // Nếu tất cả hợp lệ, thì tiến hành lưu dữ liệu. // 'Các món đã đạt chuẩn, ghi lại đơn hàng và phục vụ!' _formKey.currentState!.save(); // Ở đây, bạn có thể gửi _email và _password lên server ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Đăng nhập với: $_email / $_password')), ); print('Email: $_email, Mật khẩu: $_password'); } }, child: const Text('Đăng Nhập'), ), ], ), ), ), ); } } Giải thích chi tiết: GlobalKey<FormState> _formKey = GlobalKey<FormState>();: Đây là chìa khóa vạn năng để bạn có thể truy cập và tương tác với trạng thái của Form từ bất kỳ đâu trong widget tree. Giống như chiếc điều khiển từ xa của Bếp trưởng vậy. Form(key: _formKey, ...): Chúng ta bọc tất cả các TextFormField trong một widget Form. Điều này cho Flutter biết rằng tất cả các trường nhập liệu bên trong Form này đều thuộc về một biểu mẫu logic duy nhất và sẽ được quản lý bởi cùng một FormState (thông qua _formKey). TextFormField(validator: (value) { ... }, onSaved: (value) { ... }): Mỗi TextFormField có hai callback quan trọng: validator: Hàm này sẽ được gọi khi bạn gọi _formKey.currentState!.validate(). Nó nhận giá trị hiện tại của trường và trả về một chuỗi lỗi nếu không hợp lệ, hoặc null nếu hợp lệ. Đây là quy trình kiểm tra chất lượng cho từng món ăn nhỏ. onSaved: Hàm này được gọi khi bạn gọi _formKey.currentState!.save(). Nó dùng để lưu giá trị đã được xác thực vào biến trạng thái của bạn (_email, _password). Bếp trưởng ghi lại kết quả sau khi kiểm tra xong. if (_formKey.currentState!.validate()) { ... }: Đây là khoảnh khắc quyết định! Khi người dùng nhấn nút 'Đăng Nhập', chúng ta gọi validate(). Nếu tất cả các validator của TextFormField con đều trả về null (tức là hợp lệ), thì validate() sẽ trả về true. Lúc này, chúng ta mới an tâm gọi save() để thu thập dữ liệu và xử lý tiếp. Mẹo Hay (Best Practices) Từ Giảng Viên Creyt Luôn dùng GlobalKey<FormState>: Đây là cách chuẩn để tương tác với FormState. Đừng bao giờ cố gắng 'hack' hay tìm cách khác, nó sẽ làm bạn đau đầu đấy. autovalidateMode - Phản hồi tức thì: Ban đầu, Form không tự động xác thực cho đến khi bạn gọi validate(). Để cải thiện trải nghiệm người dùng, bạn có thể thêm autovalidateMode: AutovalidateMode.onUserInteraction vào widget Form. Điều này sẽ khiến các trường tự động xác thực ngay khi người dùng tương tác với chúng (ví dụ: gõ xong một ký tự và thoát khỏi trường đó). Giống như việc Bếp trưởng có một camera giám sát tự động kiểm tra món ăn ngay khi đầu bếp vừa chạm tay vào vậy. Xử lý onSaved cẩn thận: Đảm bảo rằng bạn lưu giá trị vào các biến trạng thái phù hợp. Giá trị từ onSaved là String?, nên nhớ xử lý null nếu có thể (thường thì validator đã đảm bảo nó không null rồi). Chia nhỏ Form lớn: Nếu form của bạn quá phức tạp với hàng chục trường, hãy cân nhắc chia nó thành nhiều Form nhỏ hơn (mỗi Form có GlobalKey riêng) hoặc dùng các thư viện quản lý form như flutter_form_builder để đơn giản hóa code. Bếp trưởng có giỏi đến mấy cũng không thể ôm đồm hàng trăm đơn cùng lúc được, phải chia ra các tổ trưởng chứ! Thông báo cho người dùng: Luôn hiển thị thông báo lỗi rõ ràng và thân thiện khi validation thất bại. Điều này giúp người dùng dễ dàng sửa lỗi và hoàn thành form. Ứng Dụng Thực Tế FormState không phải là lý thuyết suông, nó là xương sống của rất nhiều ứng dụng bạn dùng hàng ngày: Facebook, Google, Instagram: Mọi form đăng nhập, đăng ký, đổi mật khẩu đều sử dụng các cơ chế tương tự FormState để xác thực thông tin tài khoản. Shopee, Tiki, Lazada: Khi bạn điền địa chỉ giao hàng, thông tin thanh toán, mã giảm giá, đó đều là các form lớn được quản lý và xác thực cẩn thận bằng FormState (hoặc các framework tương tự). Các ứng dụng ngân hàng (TPBank, Vietcombank): Việc chuyển tiền, thay đổi thông tin cá nhân yêu cầu độ chính xác cao. FormState đảm bảo các số tài khoản, số tiền, OTP được nhập đúng định dạng trước khi gửi đi. Các ứng dụng ghi chú, quản lý công việc: Khi bạn tạo một task mới, nhập tiêu đề, mô tả, ngày hết hạn, FormState sẽ giúp kiểm tra xem bạn đã điền đủ thông tin cần thiết chưa. FormState chính là người hùng thầm lặng, đảm bảo mọi dữ liệu bạn nhập vào ứng dụng đều 'sạch sẽ' và đáng tin cậy. Nắm vững nó, bạn sẽ tự tin xây dựng những ứng dụng với trải nghiệm người dùng mượt mà và an toàn hơn rất nhiều. Chúc các bạn học tốt và hẹn gặp lại trong buổi học tiếp theo! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

23 Đọc tiếp
FlowMenu Flutter: Khi Menu Bay Bổng, UX Thăng Hoa
18/03/2026

FlowMenu Flutter: Khi Menu Bay Bổng, UX Thăng Hoa

Chào mừng các "đệ tử" đến với bài giảng hôm nay của lão Creyt! Hôm nay, chúng ta sẽ "mổ xẻ" một khái niệm nghe có vẻ lạ mà lại quen vô cùng trong thế giới UI/UX: FlowMenu. Nghe tên thì hoành tráng, nhưng thực chất nó là một ý tưởng thiết kế và một kỹ thuật triển khai tinh tế trong Flutter, chứ không phải là một widget "mì ăn liền" như PopupMenuButton đâu nhé. FlowMenu Là Gì? Để Làm Gì? Cứ hình dung thế này, cái điện thoại của bạn là một căn phòng nhỏ. Mọi thứ bạn cần dùng mà cứ bày la liệt ra sàn nhà thì vừa chật, vừa rối mắt, phải không? FlowMenu chính là "cái tủ thần kỳ" của Doraemon, nơi bạn có thể cất gọn những món đồ (các hành động, chức năng) quan trọng, liên quan mật thiết với nhau. Khi cần, chỉ cần "mở tủ", chúng sẽ "bung lụa" ra một cách duyên dáng, có thể là hình quạt, hình tròn, hay một đường thẳng tắp, rồi lại thu gọn lại khi không dùng đến. Mục đích cốt lõi của FlowMenu: Tiết kiệm không gian: Thay vì rải rác 3-4 nút hành động quan trọng chiếm chỗ, ta gói gọn chúng vào một nút duy nhất. Tăng tính thẩm mỹ: Các hiệu ứng chuyển động mượt mà, "bung nở" của FlowMenu tạo cảm giác hiện đại, chuyên nghiệp cho ứng dụng. Cải thiện trải nghiệm người dùng (UX): Gom nhóm các hành động liên quan giúp người dùng dễ dàng tìm thấy và thực hiện các tác vụ theo ngữ cảnh, giảm thiểu sự lộn xộn. Nói tóm lại, FlowMenu là cách chúng ta biến cái "đống lộn xộn" thành một "vũ điệu" UI uyển chuyển, hiệu quả. "Dòng Chảy" Của FlowMenu Trong Flutter: Widget Flow Trong Flutter, để tạo ra "dòng chảy" (flow) của các widget con một cách tùy biến, chúng ta có một "ông trùm" chuyên trị việc này: Widget Flow. Đừng nhầm lẫn nó với Column hay Row nhé. Flow giống như một sân khấu riêng, nơi bạn là đạo diễn, toàn quyền quyết định vị trí (position) và kích thước (size) của từng "diễn viên" (widget con) theo từng "khung hình" (animation tick). Điểm khác biệt lớn nhất của Flow so với Stack hay các layout widget khác là nó không tự động tính toán vị trí cho con. Thay vào đó, nó ủy quyền hoàn toàn việc này cho một FlowDelegate. Cái FlowDelegate này chính là "kịch bản" của bạn, nơi bạn viết ra cách mỗi widget con sẽ di chuyển, xuất hiện ở đâu khi menu mở ra hay đóng lại. Các bước triển khai cơ bản: AnimationController: "Nhạc trưởng" điều khiển tốc độ và trạng thái (mở/đóng) của animation. Tween: Định nghĩa khoảng giá trị mà animation sẽ chạy, ví dụ từ 0 đến 1. Flow Widget: "Sân khấu" chứa các nút hành động con và nút chính. FlowDelegate tùy chỉnh (CustomFlowDelegate): "Kịch bản" để tính toán vị trí của từng widget con dựa trên giá trị animation hiện tại. Code Ví Dụ Minh Hoạ: Một FlowMenu Hình Quạt (Radial FlowMenu) Để dễ hình dung, chúng ta sẽ xây dựng một FlowMenu đơn giản với nút chính ở góc dưới bên phải, khi nhấn vào sẽ "bung" ra ba nút con theo hình quạt. Chuẩn bị giấy bút (à quên, bàn phím) nào! import 'package:flutter/material.dart'; import 'dart:math' as math; // --- Custom Flow Delegate cho FlowMenu hình quạt --- class RadialFlowDelegate extends FlowDelegate { final Animation<double> animation; RadialFlowDelegate({required this.animation}) : super(repaint: animation); @override void paintChildren(FlowPaintingContext context) { final double xStart = context.size.width - 50.0; // Vị trí nút chính (x) final double yStart = context.size.height - 50.0; // Vị trí nút chính (y) for (int i = 0; i < context.childCount; i++) { // Góc bắt đầu (ví dụ: 180 độ = pi radian) và phân bố đều // Nút chính (child 0) luôn ở vị trí gốc if (i == 0) { context.paintChild(i, transform: Matrix4.translationValues(xStart, yStart, 0.0)); } else { final double radius = 100.0 * animation.value; // Bán kính bung ra final double angle = ((i - 1) * math.pi / 4) + math.pi; // Góc phân bố (từ 180 độ về phía trên trái) final double x = xStart + (radius * math.cos(angle)); final double y = yStart + (radius * math.sin(angle)); context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0)); } } } @override bool shouldRepaint(covariant RadialFlowDelegate oldDelegate) { return animation != oldDelegate.animation; } @override Size getSize(BoxConstraints constraints) { return constraints.biggest; // Chiếm toàn bộ không gian có thể } } // --- Widget chính của FlowMenu --- class FlowMenuExample extends StatefulWidget { const FlowMenuExample({super.key}); @override State<FlowMenuExample> createState() => _FlowMenuExampleState(); } class _FlowMenuExampleState extends State<FlowMenuExample> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleMenu() { if (_controller.isDismissed) { _controller.forward(); // Mở menu } else { _controller.reverse(); // Đóng menu } } Widget _buildFab(IconData icon, VoidCallback onPressed) { return FloatingActionButton( heroTag: null, // Tránh lỗi heroTag trùng lặp nếu có nhiều FAB mini: true, onPressed: onPressed, child: Icon(icon), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('FlowMenu của Thầy Creyt')), body: Flow( delegate: RadialFlowDelegate(animation: _controller), children: <Widget>[ // Child 0: Nút chính để mở/đóng menu _buildFab( Icons.menu, _toggleMenu, ), // Child 1: Nút hành động 1 _buildFab( Icons.add, () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Thêm mới!')) ), ), // Child 2: Nút hành động 2 _buildFab( Icons.edit, () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Chỉnh sửa!')) ), ), // Child 3: Nút hành động 3 _buildFab( Icons.share, () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Chia sẻ!')) ), ), ], ), ); } } // --- Cách chạy ví dụ này trong main.dart --- // void main() { // runApp(const MyApp()); // } // class MyApp extends StatelessWidget { // const MyApp({super.key}); // @override // Widget build(BuildContext context) { // return MaterialApp( // title: 'FlowMenu Demo', // theme: ThemeData(primarySwatch: Colors.blue), // home: const FlowMenuExample(), // ); // } // } Giải thích sơ bộ về đoạn code: RadialFlowDelegate: Đây là "linh hồn" của FlowMenu. Trong phương thức paintChildren, chúng ta tính toán vị trí x và y cho từng nút con dựa trên giá trị animation.value và góc angle. Nút chính (child 0) thì giữ nguyên, các nút con còn lại (child 1, 2, 3...) sẽ "bung" ra từ nút chính theo hình quạt. animation.value từ 0 đến 1 sẽ điều khiển bán kính radius từ 0 đến 100. FlowMenuExample: Là StatefulWidget để quản lý AnimationController. Khi _toggleMenu được gọi, _controller sẽ chạy forward() hoặc reverse() để mở/đóng menu. Flow Widget: Nhận RadialFlowDelegate của chúng ta và danh sách các widget con. Điều kỳ diệu sẽ xảy ra ở đây! Mẹo & Best Practices (Mẹo của lão Creyt) Hiệu suất là Vàng: Flow widget được thiết kế khá tối ưu cho các layout động, phức tạp. Nó tránh việc tái xây dựng toàn bộ cây widget khi animation chạy, chỉ tập trung vào việc sơn lại (repaint) các con. Tuy nhiên, đừng quá lạm dụng với hàng trăm nút con, điều gì quá cũng không tốt! Trải nghiệm Người dùng là Thượng đế: Phản hồi rõ ràng: Khi người dùng chạm vào nút chính, hãy đảm bảo có hiệu ứng để họ biết menu sắp mở ra hoặc đóng lại. Dễ dàng đóng: Ngoài việc chạm vào nút chính, có thể cân nhắc thêm chức năng đóng menu khi chạm ra ngoài khu vực menu (sử dụng GestureDetector bao quanh Flow). Số lượng vừa phải: FlowMenu đẹp nhất khi chứa 3-5 hành động quan trọng. Nhiều quá sẽ làm rối mắt và khó chọn. Accessibility (Khả năng tiếp cận): Đừng quên người dùng khiếm thị hoặc dùng bàn phím. Đảm bảo các nút con có semanticsLabel rõ ràng và có thể focus được qua phím Tab (nếu là ứng dụng desktop/web). Flutter đã hỗ trợ khá tốt cho điều này, nhưng bạn cần kiểm tra lại. Tùy biến không giới hạn: FlowDelegate là "sân chơi" của bạn. Muốn menu bung ra hình xoắn ốc? Hình zigzag? Hay theo một đường cong Bézier? Cứ thoải mái "vẽ" trong paintChildren! Ngữ cảnh là Chìa khóa: Chỉ sử dụng FlowMenu khi các hành động thực sự liên quan đến nhau và có thể nhóm lại một cách logic. Đừng biến nó thành "cái kho chứa đồ lặt vặt"! Ứng dụng Thực tế: "Những Ông Lớn" Đã Dùng FlowMenu? Tuy không phải là một widget có tên gọi "FlowMenu" cụ thể, nhưng ý tưởng và cơ chế hoạt động của nó đã và đang được rất nhiều ứng dụng lớn áp dụng dưới các hình thức khác nhau, thường được gọi là "Speed Dial" hoặc "Radial Menu": Ứng dụng Chỉnh sửa Ảnh (ví dụ: Adobe Lightroom Mobile, Snapseed): Thường có các menu tròn hoặc bán nguyệt để chọn nhanh các công cụ (cắt, xoay, bộ lọc, v.v.). Khi bạn chọn một công cụ, các biểu tượng khác sẽ ẩn đi. Ứng dụng Ghi chú (ví dụ: Google Keep, Evernote): Nút "+" thường bung ra các tùy chọn như "Thêm ghi chú", "Thêm danh sách", "Thêm ảnh", "Thêm bản vẽ". Ứng dụng Mạng xã hội/Chat (ví dụ: Facebook Messenger, Telegram): Nút đính kèm trong khung chat thường bung ra các tùy chọn như "Ảnh", "Video", "Tệp", "Vị trí", "Liên hệ". Ứng dụng Quản lý Tác vụ/Dự án: Nhiều ứng dụng sử dụng kiểu menu này để thêm nhanh các loại tác vụ khác nhau (task, event, note). FlowMenu không chỉ là một kỹ thuật lập trình, nó là một tư duy thiết kế để làm cho ứng dụng của bạn không chỉ hoạt động tốt mà còn "đẹp mắt" và "thông minh" hơn. Hãy thử nghiệm và sáng tạo với nó, các đệ tử nhé! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

33 Đọc tiếp
FlowDelegate: Nghệ Thuật Sắp Đặt Widget Ngoạn Mục trong Flutter
18/03/2026

FlowDelegate: Nghệ Thuật Sắp Đặt Widget Ngoạn Mục trong Flutter

Chào các đồng chí lập trình viên, anh Creyt lại lên sóng đây! Hôm nay, chúng ta sẽ cùng nhau 'mổ xẻ' một gã khá 'lạnh lùng' nhưng lại cực kỳ quyền năng trong thế giới Flutter: FlowDelegate. Nghe tên có vẻ khô khan, nhưng tin anh đi, nó chính là tay biên đạo múa bậc thầy cho các widget của bạn đấy! FlowDelegate là gì và tại sao chúng ta cần đến nó? Cứ hình dung thế này, trong Flutter, chúng ta thường dùng Row, Column, Stack, Wrap để sắp xếp các widget. Chúng nó giống như những 'đội trưởng' chỉ đạo đội hình vậy: ông A đứng đây, bà B đứng cạnh ông A, đứa C nằm chồng lên ông A... Rất tiện lợi, đúng không? Nhưng đời không như mơ, đôi khi bạn cần một màn trình diễn phức tạp hơn, nơi các widget không chỉ đứng yên một chỗ mà còn phải 'nhảy nhót', 'xoay vòng', hay 'tụm năm tụm ba' theo một quy luật rất riêng, và quan trọng nhất là phải mượt mà như bơ dù có hàng trăm 'vũ công' trên sân khấu. Đó chính là lúc Flow và người cộng sự đắc lực của nó, FlowDelegate, bước ra ánh sáng. Flow là một widget cấp thấp, sinh ra để xử lý các bố cục phức tạp, đặc biệt là khi các phần tử con cần được sắp xếp theo một logic tùy chỉnh cao độ và có thể thay đổi vị trí một cách linh hoạt mà không cần phải 'đập đi xây lại' toàn bộ cây widget. Còn FlowDelegate ư? Nó chính là bản thiết kế chi tiết của màn trình diễn đó. Nó định nghĩa chính xác cách thức các widget con của Flow được vẽ lên màn hình, từ vị trí, góc xoay cho đến kích thước. FlowDelegate cho phép bạn kiểm soát từng pixel mà không phải trả giá bằng hiệu năng, bởi vì nó chỉ tập trung vào việc vẽ lại vị trí của các widget con, chứ không phải xây lại chúng. Cơ chế hoạt động: Khi bạn là "Tổng Đạo Diễn" Khi sử dụng Flow, bạn sẽ cần cung cấp một FlowDelegate tùy chỉnh. Về cơ bản, bạn sẽ phải 'chấp bút' cho ba phương thức chính trong FlowDelegate: paintChildren(FlowPaintingContext context): Đây là trái tim của FlowDelegate. Nó giống như bạn đang đứng trên sân khấu và chỉ đạo từng vũ công một: "Anh A, ra giữa sân khấu, xoay 45 độ. Chị B, lùi về phía sau một chút, cao hơn anh A 10 pixel." Bạn sẽ dùng context.paintChild(index) và áp dụng các Matrix4 để di chuyển, xoay, scale từng widget con. Đây là nơi bạn định nghĩa toàn bộ logic bố cục. getSize(BoxConstraints constraints): Phương thức này trả về kích thước tổng thể của Flow widget. Nó giống như bạn nói với nhà sản xuất: "Sân khấu của tôi cần rộng chừng này, cao chừng kia để chứa hết các vũ công." Nó sẽ cho Flow biết nó nên chiếm bao nhiêu không gian trên màn hình. shouldRepaint(covariant FlowDelegate oldDelegate): Đây là "người gác cổng hiệu năng". Nó quyết định liệu Flow có cần phải vẽ lại các widget con của nó hay không khi FlowDelegate thay đổi. Nếu bạn thay đổi một thuộc tính nào đó trong FlowDelegate (ví dụ: góc xoay, khoảng cách), phương thức này sẽ kiểm tra xem sự thay đổi đó có đủ lớn để yêu cầu vẽ lại không. Trả về true nếu cần vẽ lại, false nếu không. Đây là chìa khóa để giữ cho ứng dụng của bạn mượt mà. Code Ví Dụ: Tạo một Radial Menu "siêu ngầu" Chúng ta hãy cùng tạo một menu hình tròn (Radial Menu) đơn giản. Khi bạn nhấn vào nút trung tâm, các nút chức năng khác sẽ 'bung' ra xung quanh nó như những cánh hoa. Đầu tiên, chúng ta cần một FlowDelegate để định nghĩa cách các nút con sẽ được sắp xếp: import 'dart:math' as math; import 'package:flutter/material.dart'; class RadialMenuDelegate extends FlowDelegate { final Animation<double> animation; RadialMenuDelegate({required this.animation}) : super(repaint: animation); @override void paintChildren(FlowPaintingContext context) { // Kích thước của widget con đầu tiên (nút trung tâm) final double buttonSize = context.getChildSize(0)!.width; // Bán kính đường tròn các nút con sẽ bung ra final double radius = buttonSize * 1.5; // Vẽ nút trung tâm context.paintChild(0, transform: Matrix4.translationValues( (context.size.width - buttonSize) / 2, (context.size.height - buttonSize) / 2, 0, )); // Vẽ các nút con còn lại for (int i = 1; i < context.childCount; i++) { final double theta = i * (math.pi / (context.childCount - 2)) * animation.value; // Góc xoay final double x = (context.size.width / 2) - (buttonSize / 2) + (radius * math.cos(theta)); final double y = (context.size.height / 2) - (buttonSize / 2) + (radius * math.sin(theta)); context.paintChild(i, transform: Matrix4.translationValues(x, y, 0)); } } @override Size getSize(BoxConstraints constraints) { // Đảm bảo Flow có đủ không gian cho menu bung ra return Size.square(constraints.maxWidth); } @override bool shouldRepaint(covariant RadialMenuDelegate oldDelegate) { return animation != oldDelegate.animation; } } Và đây là cách chúng ta sử dụng RadialMenuDelegate với một Flow widget: import 'package:flutter/material.dart'; // Import RadialMenuDelegate từ file trên class RadialMenu extends StatefulWidget { const RadialMenu({super.key}); @override State<RadialMenu> createState() => _RadialMenuState(); } class _RadialMenuState extends State<RadialMenu> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleMenu() { if (_controller.isDismissed) { _controller.forward(); } else { _controller.reverse(); } } Widget _buildFab(IconData icon, VoidCallback onPressed) { return RawMaterialButton( onPressed: onPressed, shape: const CircleBorder(), padding: const EdgeInsets.all(16.0), fillColor: Colors.blue, child: Icon(icon, color: Colors.white), ); } @override Widget build(BuildContext context) { return Flow( delegate: RadialMenuDelegate(animation: _controller), children: <Widget>[ // Nút trung tâm _buildFab(Icons.menu, _toggleMenu), // Các nút con _buildFab(Icons.edit, () => print('Edit')), _buildFab(Icons.share, () => print('Share')), _buildFab(Icons.add, () => print('Add')), _buildFab(Icons.delete, () => print('Delete')), ], ); } } // Để chạy thử, bạn có thể đặt RadialMenu vào Scaffold: /* void main() { runApp(MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('FlowDelegate Example')), body: Center( child: SizedBox( width: 300, height: 300, child: RadialMenu(), ), ), ), )); } */ Trong ví dụ trên, RadialMenuDelegate dùng animation.value để tính toán góc xoay cho từng nút con, tạo hiệu ứng 'bung' ra hoặc 'thu vào' mượt mà. Matrix4.translationValues là công cụ để di chuyển widget con đến vị trí mong muốn. Mẹo và Best Practices từ "Lão Làng" Creyt Khi nào thì dùng, khi nào thì "thôi đi ông"? Dùng khi: Bạn cần bố cục tùy chỉnh cao độ, đặc biệt là các bố cục động, hoạt ảnh mà vị trí các phần tử thay đổi liên tục nhưng bản thân các phần tử không thay đổi cấu trúc bên trong. Flow tối ưu cho hiệu năng trong những trường hợp này vì nó chỉ vẽ lại, không xây lại. Ví dụ: menu hình tròn, tag cloud phức tạp, hiệu ứng xếp chồng card động. Thôi đi ông khi: Các bố cục đơn giản, tĩnh, hoặc chỉ cần Row, Column, Stack, Wrap là đủ. Đừng "vác dao mổ trâu đi giết gà" nhé. Flow phức tạp hơn để debug và duy trì. Hiệu năng là vàng: Nhớ kỹ, ưu điểm lớn nhất của Flow là hiệu năng. Nó tránh được việc tái tạo (rebuild) toàn bộ cây widget con khi chỉ vị trí của chúng thay đổi. Hãy tận dụng shouldRepaint một cách thông minh để chỉ vẽ lại khi thực sự cần thiết. Matrix4 là "người bạn thân": Hầu hết các phép biến đổi trong paintChildren sẽ liên quan đến Matrix4. Hãy làm quen với các phương thức như translationValues, rotationZ, scale để điều khiển vị trí, xoay, và kích thước của các widget con. Debugging Flow có thể "lú": Vì Flow hoạt động ở cấp độ thấp, nó không cung cấp các cơ chế ràng buộc bố cục tự động như Row hay Column. Khi có lỗi về vị trí, bạn sẽ phải tự tính toán và kiểm tra các giá trị x, y, theta của mình. Hãy dùng print hoặc debug mode để xem các giá trị tính toán được. Caching calculations: Nếu logic tính toán vị trí của bạn phức tạp, hãy cân nhắc cache các giá trị trung gian để tránh tính toán lại không cần thiết trong mỗi frame. Ứng dụng thực tế: "Cuộc sống là một sân khấu lớn" FlowDelegate không phải là một ngôi sao thường xuyên xuất hiện trên các ứng dụng phổ thông, nhưng nó là một "ngôi sao thầm lặng" trong các trường hợp đặc biệt cần đến sự tinh tế và hiệu năng: Radial Action Buttons: Giống như ví dụ chúng ta vừa làm, nhiều ứng dụng có một nút hành động nổi (FAB) ở góc màn hình, khi nhấn vào, nó bung ra một loạt các tùy chọn nhỏ hơn theo hình quạt. Đây chính là mảnh đất màu mỡ cho FlowDelegate. Tag Clouds/Dynamic Tag Layouts: Trong các ứng dụng có nhiều thẻ (tag) cần hiển thị một cách linh hoạt, có thể chồng chéo hoặc sắp xếp ngẫu nhiên nhưng vẫn đảm bảo tính thẩm mỹ và hiệu năng cao. Custom Loading Animations: Các hiệu ứng loading phức tạp, nơi các phần tử nhỏ di chuyển theo quỹ đạo đặc biệt (ví dụ: xoay quanh một điểm, sắp xếp lại theo hình dạng động). Interactive Galleries/Image Viewers: Trong một số trường hợp đặc biệt, khi bạn muốn tạo hiệu ứng xem ảnh độc đáo, nơi các ảnh con có thể xoay, phóng to, thu nhỏ và di chuyển theo cử chỉ người dùng, FlowDelegate có thể là một công cụ mạnh mẽ. Nhớ nhé, FlowDelegate không phải là công cụ bạn dùng hàng ngày, nhưng khi bạn cần một màn trình diễn bố cục "đỉnh cao", mượt mà và hiệu quả, nó chính là bí kíp cuối cùng trong túi đồ nghề của bạn. Hãy luyện tập và làm chủ nó để nâng tầm kỹ năng Flutter của mình lên một đẳng cấp mới! Chúc các bạn code vui vẻ! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

24 Đọc tiếp
FlexibleSpaceBar: Bí kíp tạo AppBar biến hình trong Flutter
18/03/2026

FlexibleSpaceBar: Bí kíp tạo AppBar biến hình trong Flutter

Chào các lập trình viên tương lai, hoặc những người đã "lăn lộn" với code đủ để hiểu rằng UI/UX không chỉ là màu mè mà là linh hồn của ứng dụng! Tôi là Creyt, và hôm nay chúng ta sẽ mổ xẻ một "phù thủy" trong thế giới Flutter: FlexibleSpaceBar. 1. FlexibleSpaceBar là gì và tại sao chúng ta cần nó? Hãy hình dung thế này: Bạn có một chiếc xe thể thao siêu ngầu, nhưng đôi khi bạn muốn nó biến hình thành một chiếc SUV đa dụng để chở đồ, rồi lại thu gọn thành xe đua khi cần tốc độ. Trong thế giới Flutter, FlexibleSpaceBar chính là cái "bộ phận biến hình" đó cho AppBar của bạn. Nói một cách hàn lâm hơn, FlexibleSpaceBar là một widget được thiết kế để hoạt động bên trong SliverAppBar. Nhiệm vụ chính của nó là tạo ra một vùng không gian linh hoạt ở phần đầu ứng dụng, cho phép AppBar của bạn co giãn một cách mượt mà khi người dùng cuộn (scroll). Khi bạn cuộn xuống, FlexibleSpaceBar có thể mở rộng ra, tiết lộ thêm nội dung như một hình ảnh nền lớn, một tiêu đề ấn tượng, hoặc bất kỳ widget nào bạn muốn. Ngược lại, khi bạn cuộn lên, nó sẽ thu gọn lại, để lại một AppBar nhỏ gọn, tinh tế. Tại sao chúng ta cần nó ư? Đơn giản thôi. Trong thời đại mà người dùng đòi hỏi trải nghiệm mượt mà, trực quan, thì một AppBar tĩnh như "tượng đài" là không đủ. FlexibleSpaceBar giúp ứng dụng của bạn trở nên sống động, tương tác hơn, tạo hiệu ứng parallax (hiệu ứng thị sai) cực kỳ hút mắt, khiến người dùng cảm thấy như đang lướt trên một trang web cao cấp chứ không phải một ứng dụng di động thông thường. Nó là chìa khóa để biến những giao diện "đơn thuần" thành "đẳng cấp". 2. Code Ví Dụ Minh Họa: Biến hình cùng FlexibleSpaceBar Để FlexibleSpaceBar phát huy sức mạnh, chúng ta cần đặt nó vào đúng "khung sườn" của nó, đó chính là SliverAppBar, và tất cả sẽ nằm trong một CustomScrollView. Nghe có vẻ phức tạp, nhưng hãy xem ví dụ này, nó đơn giản hơn bạn nghĩ nhiều: import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'FlexibleSpaceBar Demo', theme: ThemeData( primarySwatch: Colors.blue, appBarTheme: const AppBarTheme( backgroundColor: Colors.deepPurple, // Màu nền mặc định cho AppBar ), ), home: const FlexibleSpaceBarExample(), ); } } class FlexibleSpaceBarExample extends StatelessWidget { const FlexibleSpaceBarExample({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverAppBar( expandedHeight: 250.0, // Chiều cao tối đa khi App Bar mở rộng floating: false, // Không trôi nổi khi cuộn xuống một chút pinned: true, // Luôn ghim App Bar ở trên cùng khi thu gọn flexibleSpace: FlexibleSpaceBar( centerTitle: true, // Căn giữa tiêu đề khi App Bar thu gọn title: const Text( 'Lớp Học Flutter Của Thầy Creyt', style: TextStyle( color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold, ), ), background: Image.network( 'https://picsum.photos/id/1043/800/400', // Hình nền sẽ co giãn và tạo hiệu ứng parallax fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, errorBuilder: (context, error, stackTrace) => const Center(child: Icon(Icons.error)), ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( color: index.isOdd ? Colors.white : Colors.grey[100], height: 120.0, child: Center( child: Text( 'Bài học số ${index + 1}: Khái niệm Flutter nâng cao', style: const TextStyle(fontSize: 18, color: Colors.black87), ), ), ); }, childCount: 25, // Tạo 25 mục để có thể cuộn ), ), ], ), ); } } Trong ví dụ trên: CustomScrollView: Là "con đường" chứa tất cả các Sliver (các phần có thể cuộn được). SliverAppBar: Là "chiếc xe" của chúng ta. expandedHeight: Xác định chiều cao tối đa của AppBar khi nó được mở rộng hoàn toàn. Hãy coi nó là "chiều cao khi xe biến hình thành SUV". pinned: true: Đảm bảo rằng khi bạn cuộn lên, AppBar sẽ thu gọn lại và "ghim" ở đầu màn hình, không biến mất hoàn toàn. flexibleSpace: FlexibleSpaceBar(...): Đây chính là "bộ phận biến hình" mà chúng ta đang nói đến. title: Widget này (thường là Text) sẽ xuất hiện khi AppBar thu gọn và sẽ di chuyển, mờ dần/hiện ra khi AppBar co giãn. background: Đây là nơi bạn đặt hình ảnh hoặc bất kỳ widget nào mà bạn muốn nó xuất hiện ở phần nền của AppBar khi nó mở rộng. Nó sẽ tự động tạo hiệu ứng parallax khi cuộn. centerTitle: true: Khi AppBar thu gọn, tiêu đề sẽ được căn giữa. 3. Mẹo Vặt & Best Practices Từ Lão Làng Creyt Để dùng FlexibleSpaceBar một cách "thượng thừa", đây là vài mẹo nhỏ mà tôi đã đúc kết được sau bao năm "xông pha trận mạc": Hiệu ứng Parallax đỉnh cao: Hãy luôn đặt một Image.network hoặc Image.asset vào thuộc tính background của FlexibleSpaceBar. Flutter sẽ tự động xử lý hiệu ứng parallax (hình nền di chuyển chậm hơn nội dung) một cách mượt mà, tạo cảm giác chiều sâu rất ấn tượng. Kết hợp SliverAppBar: pinned: true: Gần như luôn luôn nên dùng true. Nó giữ cho AppBar thu gọn ở trên cùng, mang lại trải nghiệm người dùng quen thuộc và tiện lợi. floating: true và snap: true: Nếu bạn muốn AppBar tự động hiện lại ngay lập tức khi người dùng cuộn xuống một chút (thay vì phải cuộn hết lên đầu), hãy dùng floating: true. Kết hợp với snap: true, nó sẽ tự động "bật" ra hoặc "thu" lại hoàn toàn thay vì dừng ở lưng chừng. expandedHeight: Đừng quá ham hố chiều cao lớn. Hãy chọn một giá trị vừa phải (ví dụ: 150-250px) để không chiếm quá nhiều không gian màn hình trên các thiết bị nhỏ. Nội dung title: Tiêu đề trong FlexibleSpaceBar sẽ tự động thay đổi kích thước và vị trí. Hãy giữ nó ngắn gọn, súc tích để dễ đọc khi AppBar thu gọn. Nếu cần nhiều thông tin hơn, hãy cân nhắc dùng một Stack trong background để chồng các widget lên nhau. Tránh "nhồi nhét": Mặc dù bạn có thể đặt bất kỳ widget nào vào background, đừng nhồi nhét quá nhiều logic phức tạp hoặc widget nặng nề vào đó. Mục đích chính là tạo hiệu ứng thị giác, không phải là nơi chứa đựng toàn bộ giao diện. 4. Ứng Dụng Thực Tế: Ai đang dùng "phép thuật" này? Bạn có thể không nhận ra, nhưng hiệu ứng của FlexibleSpaceBar đã và đang được rất nhiều ứng dụng lớn sử dụng để nâng tầm trải nghiệm người dùng: Ứng dụng Mạng xã hội: Hãy mở profile của bạn trên LinkedIn, Facebook, hoặc Instagram. Bạn sẽ thấy hình ảnh bìa (cover photo) thường chiếm một diện tích lớn ở trên cùng, và khi bạn cuộn xuống xem các bài đăng, hình ảnh đó sẽ thu nhỏ dần, hoặc biến mất, để lại một thanh tiêu đề nhỏ gọn. Đó chính là hiệu ứng tương tự mà FlexibleSpaceBar mang lại. Ứng dụng Đọc tin tức/Blog: Khi bạn đọc một bài báo trên các ứng dụng như Google News hay các trang blog lớn, phần tiêu đề hoặc hình ảnh đại diện của bài viết thường rất lớn ở đầu trang, sau đó co lại khi bạn cuộn xuống nội dung. Trang sản phẩm E-commerce: Các ứng dụng mua sắm như Shopee, Lazada (hoặc các phiên bản quốc tế như Amazon) thường có trang chi tiết sản phẩm với một carousel hình ảnh lớn ở trên cùng. Khi cuộn, phần này cũng sẽ thu gọn lại. Ứng dụng Âm nhạc/Video: Các trang thông tin về album, nghệ sĩ trên Spotify, YouTube Music hay trang kênh trên YouTube cũng thường dùng hiệu ứng này để hiển thị ảnh bìa album/kênh và thông tin, sau đó thu gọn khi người dùng cuộn qua danh sách bài hát/video. Tóm lại, FlexibleSpaceBar không chỉ là một widget đẹp mắt, mà còn là một công cụ mạnh mẽ để tạo ra các giao diện động, tương tác và chuyên nghiệp trong ứng dụng Flutter của bạn. Hãy thử nghiệm và sáng tạo với nó 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é!

23 Đọc tiếp
Mở Một Lần Một Thôi! Flutter ExpansionPanelListRadio
18/03/2026

Mở Một Lần Một Thôi! Flutter ExpansionPanelListRadio

ExpansionPanelListRadio: Ông Chủ Của Sự Ngăn Nắp Chào các bạn, lại là Creyt đây! Hôm nay chúng ta sẽ "giải mã" một widget mà thoạt nghe có vẻ phức tạp nhưng thực ra lại là "trợ thủ đắc lực" cho sự gọn gàng và tập trung trong giao diện người dùng của chúng ta: ExpansionPanelListRadio. Bạn cứ hình dung thế này: trong thế giới lập trình, đôi khi chúng ta cần hiển thị một danh sách các lựa chọn hoặc thông tin chi tiết, nhưng nếu cứ "phanh phui" tất cả ra cùng lúc thì màn hình của bạn sẽ trông như một bãi chiến trường vậy. ExpansionPanelListRadio sinh ra để giải quyết vấn đề đó. Nó giống như một cái tủ quần áo thần kỳ của Doraemon, bạn có nhiều ngăn kéo (các panel), nhưng tại một thời điểm, chỉ được phép mở một ngăn duy nhất để lấy đồ thôi. Rất tiện lợi, phải không? Về cơ bản, nó là gì và để làm gì? ExpansionPanelListRadio là một widget trong Flutter cho phép bạn tạo một danh sách các bảng điều khiển (panels) có thể mở rộng. Điều đặc biệt ở đây, như cái tên "Radio" đã gợi ý, là nó sẽ tự động đảm bảo rằng chỉ một panel duy nhất có thể được mở rộng tại bất kỳ thời điểm nào. Khi bạn mở một panel khác, panel đang mở trước đó sẽ tự động đóng lại. Nó cực kỳ hữu ích trong các tình huống sau: Các câu hỏi thường gặp (FAQ): Người dùng chỉ cần mở câu trả lời cho câu hỏi họ quan tâm, tránh việc phải cuộn qua một danh sách dài các câu trả lời. Lựa chọn cấu hình sản phẩm: Ví dụ, khi bạn chọn "Màu sắc", panel chọn màu sẽ mở ra, và khi bạn chọn "Kích cỡ", panel màu sẽ đóng lại và panel kích cỡ mở ra. Hướng dẫn từng bước: Chỉ hiển thị chi tiết cho bước hiện tại. Các bộ lọc (filters) trong ứng dụng thương mại điện tử: Khi bạn chọn một danh mục lọc, các tùy chọn chi tiết của danh mục đó hiện ra, và khi bạn chọn danh mục khác, danh mục cũ sẽ ẩn đi. Code Ví Dụ Minh Họa: "Tủ Đồ Thông Minh" Để ExpansionPanelListRadio hoạt động, chúng ta cần một StatefulWidget để quản lý trạng thái của panel đang mở. Hãy xem ví dụ dưới đây: import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'ExpansionPanelListRadio 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> { // Biến để lưu trữ giá trị của panel đang mở. // Null nghĩa là không có panel nào mở. // Đây là "chìa khóa" để ExpansionPanelListRadio biết panel nào đang active. Object? _currentOpenPanelValue; final List<Item> _data = generateItems(5); // Tạo 5 item mẫu @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Tủ Đồ Thông Minh Của Creyt'), ), body: SingleChildScrollView( child: ExpansionPanelList.radio( // Giá trị của panel được mở ban đầu. // Nếu không set, mặc định sẽ không có panel nào mở. initialOpenPanelValue: _currentOpenPanelValue, // Callback khi trạng thái mở/đóng của panel thay đổi. // `value` là giá trị của panel vừa được mở/đóng. // `isExpanded` là trạng thái mới của panel đó. onExpansionChanged: (Object value, bool isExpanded) { setState(() { // Nếu panel được mở, lưu giá trị của nó. // Nếu panel đóng (do người dùng click lại hoặc mở panel khác), // thì _currentOpenPanelValue sẽ được set thành null hoặc giá trị của panel mới. _currentOpenPanelValue = isExpanded ? value : null; }); print('Panel with value $value is now expanded: $isExpanded'); }, // Danh sách các ExpansionPanelRadio con. children: _data.map<ExpansionPanelRadio>((Item item) { return ExpansionPanelRadio( value: item.id, // Giá trị duy nhất cho mỗi panel. RẤT QUAN TRỌNG! headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( title: Text(item.headerValue), leading: Icon(isExpanded ? Icons.folder_open : Icons.folder), ); }, body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.expandedValue), const SizedBox(height: 10), ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Bạn vừa chọn: ${item.headerValue}')), ); }, child: const Text('Chọn mục này'), ), ], ), ), // Cho phép người dùng nhấn vào header để mở/đóng panel. canTapOnHeader: true, ); }).toList(), ), ), ); } } // Lớp mẫu để chứa dữ liệu cho mỗi panel class Item { Item({ required this.id, required this.headerValue, required this.expandedValue, }); Object id; // Dùng Object để linh hoạt, thường là int hoặc String String headerValue; String expandedValue; } // Hàm tạo dữ liệu mẫu List<Item> generateItems(int numberOfItems) { return List<Item>.generate(numberOfItems, (int index) { return Item( id: index, headerValue: 'Ngăn Kéo Số ${index + 1}', expandedValue: 'Đây là nội dung chi tiết của Ngăn Kéo Số ${index + 1}. Bạn có thể đặt bất cứ widget nào vào đây.', ); }); } Trong ví dụ trên: Chúng ta tạo một List<Item> để mô phỏng dữ liệu cho các panel. _currentOpenPanelValue là biến Object? quản lý panel nào đang được mở. Khi onExpansionChanged được gọi, chúng ta cập nhật biến này để ExpansionPanelListRadio biết trạng thái mới. Mỗi ExpansionPanelRadio cần một value duy nhất. Đây là "định danh" để widget biết panel nào đang được thao tác. headerBuilder xây dựng phần tiêu đề của panel, và body là nội dung sẽ hiển thị khi panel được mở. canTapOnHeader: true là một chi tiết nhỏ nhưng quan trọng, giúp người dùng có thể chạm vào tiêu đề để mở/đóng, thay vì chỉ mũi tên. Mẹo Hay Từ Giảng Viên Creyt (Best Practices) Luôn dùng StatefulWidget: ExpansionPanelListRadio cần một biến trạng thái để theo dõi panel nào đang mở (initialOpenPanelValue). Nếu bạn dùng StatelessWidget mà không có cơ chế quản lý trạng thái bên ngoài (như Provider, BLoC, Riverpod), nó sẽ không hoạt động như mong đợi. value phải DUY NHẤT: Đây là "chìa khóa" để ExpansionPanelListRadio xác định các panel. Nếu các value bị trùng lặp, hành vi của widget sẽ không đúng. Tốt nhất là dùng int hoặc String làm ID duy nhất. Quản lý initialOpenPanelValue: Biến này không chỉ dùng để thiết lập panel mở ban đầu mà còn được ExpansionPanelListRadio sử dụng nội bộ để biết panel nào đang mở. Luôn cập nhật nó trong onExpansionChanged để đồng bộ trạng thái. canTapOnHeader là bạn của người dùng: Mặc định, chỉ có mũi tên nhỏ ở cuối header mới có thể mở/đóng panel. Bật canTapOnHeader: true sẽ giúp trải nghiệm người dùng mượt mà hơn rất nhiều. Nội dung body linh hoạt: Bạn có thể đặt bất kỳ widget phức tạp nào vào phần body của ExpansionPanelRadio, từ Column, Row, Form cho đến các ListView lồng nhau. Hãy tận dụng sự linh hoạt này! Ứng Dụng Thực Tế: "Ai Đã Dùng Nó?" Bạn có thể thấy ExpansionPanelListRadio (hoặc các biến thể của nó) được sử dụng rộng rãi trong nhiều ứng dụng và trang web hàng ngày: Các ứng dụng ngân hàng/tài chính: Phần FAQ, hoặc mục "Hỗ trợ" nơi bạn có thể mở các câu hỏi về tài khoản, thẻ tín dụng, v.v., nhưng chỉ một câu trả lời hiện ra mỗi lần. Ứng dụng mua sắm (E-commerce): Trong phần bộ lọc sản phẩm, bạn có thể thấy các mục như "Thương hiệu", "Kích cỡ", "Màu sắc". Khi bạn mở "Thương hiệu", các lựa chọn về thương hiệu hiện ra, và nếu bạn mở "Kích cỡ", phần thương hiệu sẽ tự động đóng lại. Ứng dụng học tập/khóa học online: Các mục lục bài giảng hoặc FAQ về khóa học. Các trang cài đặt (Settings): Đôi khi các cài đặt được nhóm lại thành các panel, và bạn chỉ có thể mở một nhóm cài đặt tại một thời điểm để điều chỉnh. Tóm lại, ExpansionPanelListRadio không chỉ là một widget đẹp mắt mà còn là một công cụ mạnh mẽ để tạo ra giao diện người dùng gọn gàng, có tổ chức và tập trung. Hãy thử nghiệm và biến nó thành "đồ nghề" của bạn 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é!

14 Đọc tiếp
Flutter ExpansionPanel: Mở khóa UI linh hoạt, tối ưu trải nghiệm
18/03/2026

Flutter ExpansionPanel: Mở khóa UI linh hoạt, tối ưu trải nghiệm

Chào mừng các bạn đến với buổi học hôm nay cùng Giảng viên Creyt! Hôm nay, chúng ta sẽ cùng nhau "bóc tách" một widget cực kỳ hữu ích trong Flutter, đó là ExpansionPanel. ExpansionPanel là gì? Để làm gì? Để dễ hình dung, các bạn cứ tưởng tượng thế này: ExpansionPanel giống như một "chiếc rèm sân khấu mini" trên ứng dụng của bạn vậy. Ban đầu, nó chỉ là một cái tiêu đề nhỏ gọn, kín đáo – cái "rèm" đang buông xuống. Nhưng khi người dùng "kéo rèm lên" (tức là chạm vào tiêu đề), "vở diễn" (nội dung chi tiết) bên trong sẽ từ từ hiện ra, bung nở đầy đủ. Khi không cần nữa, "rèm" lại buông xuống, trả lại không gian gọn gàng cho sân khấu màn hình. Vậy nó để làm gì? Đơn giản là để giải quyết bài toán không gian và sự tập trung của người dùng. Trong một thế giới di động mà diện tích màn hình là vàng, việc nhồi nhét mọi thứ vào cùng một lúc sẽ khiến người dùng "ngộp thở". ExpansionPanel giúp bạn: Tiết kiệm không gian: Chỉ hiển thị những gì cần thiết ngay lập tức (tiêu đề), giữ cho giao diện luôn thoáng đãng. Tổ chức nội dung: Nhóm các thông tin liên quan lại với nhau một cách logic, dễ quản lý. Cải thiện trải nghiệm người dùng (UX): Giảm tải nhận thức (cognitive load). Người dùng chỉ cần tập trung vào thông tin họ muốn xem, và có thể "ẩn" nó đi khi không cần nữa. Đây là một nguyên tắc thiết kế UI/UX cốt lõi, giúp người dùng cảm thấy kiểm soát được ứng dụng. Nói theo kiểu Harvard một chút, ExpansionPanel là một ví dụ điển hình của "progressive disclosure" (tiết lộ dần dần) – một kỹ thuật thiết kế giao diện giúp giảm độ phức tạp bằng cách chỉ hiển thị thông tin khi người dùng yêu cầu. Nó giúp duy trì sự đơn giản ở cấp độ bề mặt, nhưng vẫn cung cấp chiều sâu khi cần thiết. Code Ví Dụ Minh Hoạ Rõ Ràng Để sử dụng ExpansionPanel, chúng ta thường dùng nó trong một danh sách gọi là ExpansionPanelList. Widget này sẽ quản lý nhiều ExpansionPanel con. Chúng ta cần một StatefulWidget để quản lý trạng thái mở/đóng của từng panel. Đây là một ví dụ đơn giả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: 'ExpansionPanel Demo của Creyt', theme: ThemeData(primarySwatch: Colors.blueGrey), home: const ExpansionPanelScreen(), ); } } // Lớp dữ liệu cho mỗi item trong ExpansionPanelList class Item { Item({ required this.expandedValue, required this.headerValue, this.isExpanded = false, }); String expandedValue; String headerValue; bool isExpanded; } List<Item> generateItems(int numberOfItems) { return List<Item>.generate(numberOfItems, (int index) { return Item( headerValue: 'Tiêu đề Panel ${index + 1}', expandedValue: 'Đây là nội dung chi tiết của Panel ${index + 1}. Bạn có thể đặt bất kỳ widget nào vào đây, từ văn bản đến hình ảnh hay các form nhập liệu phức tạp.', ); }); } class ExpansionPanelScreen extends StatefulWidget { const ExpansionPanelScreen({super.key}); @override State<ExpansionPanelScreen> createState() => _ExpansionPanelScreenState(); } class _ExpansionPanelScreenState extends State<ExpansionPanelScreen> { final List<Item> _data = generateItems(3); // Tạo 3 panel mẫu @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ExpansionPanel của Creyt'), ), body: SingleChildScrollView( child: ExpansionPanelList( expansionCallback: (int index, bool isExpanded) { setState(() { _data[index].isExpanded = !isExpanded; }); }, children: _data.map<ExpansionPanel>((Item item) { return ExpansionPanel( headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( title: Text(item.headerValue), leading: Icon(isExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), ); }, body: Padding( padding: const EdgeInsets.all(16.0), child: Text(item.expandedValue), ), isExpanded: item.isExpanded, ); }).toList(), ), ), ); } } Trong đoạn code trên: Chúng ta tạo một lớp Item để quản lý dữ liệu cho mỗi panel, bao gồm tiêu đề (headerValue), nội dung (expandedValue) và trạng thái mở/đóng (isExpanded). ExpansionPanelList là widget chính chứa các ExpansionPanel. expansionCallback là hàm được gọi khi người dùng chạm vào tiêu đề của một panel. Tại đây, chúng ta setState để cập nhật trạng thái isExpanded của item tương ứng, khiến panel đóng/mở. Mỗi ExpansionPanel có headerBuilder (xây dựng phần tiêu đề) và body (nội dung khi mở). isExpanded là thuộc tính quan trọng để điều khiển trạng thái mở hay đóng của panel. Mẹo (Best Practices) từ Giảng viên Creyt Quản lý trạng thái là chìa khóa: Đừng bao giờ quên isExpanded! Nó là "tay lái" điều khiển "chiếc rèm sân khấu" của bạn. Luôn setState đúng cách khi trạng thái thay đổi để UI được cập nhật. Đừng lạm dụng: Mặc dù ExpansionPanel rất tiện lợi, nhưng nếu bạn có quá nhiều panel với nội dung cực kỳ dài hoặc phức tạp, hãy cân nhắc giải pháp khác như chuyển sang một màn hình riêng hoặc sử dụng tab. Performance có thể bị ảnh hưởng nếu bạn render quá nhiều widget ẩn. Nội dung header phải rõ ràng: Tiêu đề của mỗi panel phải đủ súc tích và mô tả được nội dung bên trong, để người dùng không cần mở ra cũng biết đại khái nó nói về cái gì. "Quy tắc 3 giây" – người dùng nên hiểu ngay lập tức. Tùy biến linh hoạt: headerBuilder và body đều nhận vào Widget, nghĩa là bạn có thể đặt bất cứ thứ gì vào đó – từ text đơn giản đến các form phức tạp, hình ảnh, hay thậm chí là một ListView con. Hãy sáng tạo! ExpansionPanelList.radio: Nếu bạn chỉ muốn một panel được mở tại một thời điểm (kiểu "radio button"), hãy dùng ExpansionPanelList.radio thay vì ExpansionPanelList thông thường. Nó tự động quản lý việc đóng các panel khác khi một panel mới được mở. Ví Dụ Thực Tế Các Ứng Dụng/Website Đã Ứng Dụng ExpansionPanel, hay các biến thể của nó (còn gọi là Accordion trong web design), được sử dụng rộng rãi đến mức bạn có thể thấy nó ở khắp mọi nơi: Trang FAQ (Câu hỏi thường gặp): Đây là ứng dụng kinh điển nhất. Một danh sách các câu hỏi, mỗi câu khi click vào sẽ hiện câu trả lời chi tiết. Tiết kiệm không gian cực hiệu quả. Trang Cài đặt (Settings/Preferences): Trong các ứng dụng di động, các nhóm cài đặt thường được gom lại thành các panel có thể mở rộng. Ví dụ: "Cài đặt tài khoản", "Cài đặt thông báo", "Cài đặt bảo mật". Trang Chi tiết sản phẩm trên E-commerce: Các phần như "Thông số kỹ thuật", "Mô tả sản phẩm", "Đánh giá" thường được đặt trong các panel có thể mở rộng để trang sản phẩm không quá dài. Các bước hướng dẫn (Tutorials/Onboarding): Khi bạn cần hướng dẫn người dùng qua nhiều bước, mỗi bước có thể là một panel, mở ra từng bước một. Menu điều hướng phức tạp: Trong một số trường hợp, menu có nhiều cấp con cũng có thể sử dụng ExpansionPanel để tổ chức. Như vậy, ExpansionPanel không chỉ là một widget đơn thuần, mà nó là một công cụ mạnh mẽ giúp bạn thiết kế giao diện thông minh, thân thiện và hiệu quả. Hãy vận dụng nó thật khéo léo vào các dự án của mình nhé! Hẹn gặp lại trong bài học tiếp theo! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

23 Đọc tiếp
EdgeInsetsDirectional: Định Hướng Khoảng Trắng Chuẩn Flutter
18/03/2026

EdgeInsetsDirectional: Định Hướng Khoảng Trắng Chuẩn Flutter

Chào các lập trình viên tương lai! Anh Creyt đây, và hôm nay chúng ta sẽ cùng mổ xẻ một khái niệm tưởng chừng đơn giản nhưng lại cực kỳ quan trọng trong việc xây dựng giao diện người dùng (UI) chuyên nghiệp và toàn cầu hóa với Flutter: EdgeInsetsDirectional. 1. EdgeInsetsDirectional là gì và để làm gì? Bạn thấy đấy, trong thế giới lập trình, đôi khi những chi tiết nhỏ lại là những người hùng thầm lặng, và EdgeInsetsDirectional chính là một trong số đó. Hãy hình dung bạn là một kiến trúc sư tài ba, thiết kế một ngôi nhà. Bạn không chỉ đặt gạch mà còn phải tính toán khoảng cách, lối đi để ngôi nhà có không gian thở, đúng không? Trong Flutter, Padding và Margin là những 'khoảng thở' đó, và chúng ta thường dùng EdgeInsets để định nghĩa chúng. Nhưng có một vấn đề: thế giới không chỉ có tiếng Anh! Có những ngôn ngữ đọc từ trái sang phải (Left-to-Right - LTR) như tiếng Việt, tiếng Anh, nhưng cũng có những ngôn ngữ đọc từ phải sang trái (Right-to-Left - RTL) như tiếng Ả Rập, tiếng Do Thái. Nếu bạn cứ cứng nhắc dùng EdgeInsets.only(left: 10.0, right: 20.0), thì khi giao diện của bạn chuyển sang chế độ RTL, cái 'padding bên trái' vẫn nằm nguyên bên trái, trong khi lẽ ra nó phải chuyển sang bên phải để phù hợp với hướng đọc mới. UI của bạn sẽ trông như bị 'lật ngược' một cách ngớ ngẩn. Đó là lúc EdgeInsetsDirectional bước ra ánh sáng! Nó là một phiên bản thông minh hơn của EdgeInsets, được thiết kế để tự động thích nghi với hướng văn bản hiện tại của ứng dụng. Thay vì dùng left và right, bạn sẽ dùng start và end. start: Tương ứng với left khi hướng văn bản là LTR, và right khi hướng văn bản là RTL. end: Tương ứng với right khi hướng văn bản là LTR, và left khi hướng văn bản là RTL. Nói cách khác, EdgeInsetsDirectional giúp UI của bạn 'biết đọc xuôi hay đọc ngược', đảm bảo khoảng cách luôn được áp dụng đúng vị trí, bất kể ngôn ngữ nào. Nó là nền tảng cho việc quốc tế hóa (Internationalization - i18n) một cách mượt mà. 2. Code Ví Dụ Minh Họa Rõ Ràng Để bạn thấy rõ sự khác biệt, anh Creyt sẽ trình diễn một ví dụ nhỏ. Chúng ta sẽ tạo ra hai chiếc Card, một chiếc trong môi trường LTR và một chiếc trong môi trường RTL, và xem EdgeInsetsDirectional hoạt độ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: 'EdgeInsetsDirectional Demo', theme: ThemeData(primarySwatch: Colors.blue), home: Scaffold( appBar: AppBar(title: const Text('EdgeInsetsDirectional Demo')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Ví dụ LTR (Left-to-Right) Directionality( textDirection: TextDirection.ltr, // Đặt hướng văn bản là LTR child: MyDirectionalCard(title: 'LTR Card (Start=30, End=10)', color: Colors.blue.shade100), ), const SizedBox(height: 20), // Ví dụ RTL (Right-to-Left) Directionality( textDirection: TextDirection.rtl, // Đặt hướng văn bản là RTL child: MyDirectionalCard(title: 'RTL Card (Start=30, End=10)', color: Colors.green.shade100), ), const SizedBox(height: 20), // Ví dụ với EdgeInsets.only (để so sánh) Card( color: Colors.red.shade100, margin: const EdgeInsets.only(left: 30.0, right: 10.0, top: 10.0, bottom: 10.0), // Cố định left/right child: const Padding( padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0), child: Text( 'Fixed Left/Right Padding', style: TextStyle(fontSize: 16), ), ), ), ], ), ), ), ); } } class MyDirectionalCard extends StatelessWidget { final String title; final Color color; const MyDirectionalCard({Key? key, required this.title, required this.color}) : super(key: key); @override Widget build(BuildContext context) { return Card( color: color, // Đây là lúc EdgeInsetsDirectional thể hiện sức mạnh! // 'start' sẽ là bên trái trong LTR, và bên phải trong RTL. // 'end' sẽ là bên phải trong LTR, và bên trái trong RTL. margin: const EdgeInsetsDirectional.only(start: 30.0, end: 10.0, top: 10.0, bottom: 10.0), child: Padding( padding: const EdgeInsetsDirectional.symmetric(horizontal: 20.0, vertical: 15.0), child: Text( title, style: const TextStyle(fontSize: 16), ), ), ); } } Trong ví dụ trên, hãy chú ý cách margin của MyDirectionalCard được định nghĩa bằng EdgeInsetsDirectional.only(start: 30.0, end: 10.0). Khi bạn chạy ứng dụng: Với textDirection: TextDirection.ltr, Card sẽ có 30.0 padding ở bên trái (start) và 10.0 ở bên phải (end). Với textDirection: TextDirection.rtl, Card sẽ có 30.0 padding ở bên phải (start) và 10.0 ở bên trái (end). Còn chiếc Card cuối cùng sử dụng EdgeInsets.only(left: 30.0, right: 10.0) thì sao? Nó sẽ luôn có padding 30.0 ở bên trái vật lý và 10.0 ở bên phải vật lý, bất kể hướng văn bản là gì. Bạn sẽ thấy ngay sự khác biệt về trực quan! 3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế Luôn ưu tiên EdgeInsetsDirectional cho khoảng cách ngang: Bất cứ khi nào bạn cần định nghĩa padding hoặc margin theo chiều ngang (trái/phải), hãy nghĩ ngay đến EdgeInsetsDirectional. Nó là lựa chọn an toàn và linh hoạt nhất cho các ứng dụng đa ngôn ngữ. start và end, không phải left và right: Hãy tập thói quen dùng start (nơi văn bản bắt đầu) và end (nơi văn bản kết thúc). Đây là tư duy quan trọng cho UI quốc tế hóa. top và bottom là bất biến: Đối với khoảng cách theo chiều dọc (top, bottom), bạn vẫn có thể an tâm sử dụng EdgeInsets.only(top: ..., bottom: ...) vì chúng không bị ảnh hưởng bởi hướng văn bản. Kiểm tra với Directionality: Khi phát triển, hãy chủ động dùng Directionality widget để kiểm tra giao diện của bạn trong cả hai chế độ LTR và RTL. Đừng đợi đến khi deploy mới phát hiện lỗi. Hiểu rõ ngữ cảnh: EdgeInsetsDirectional hoạt động dựa trên TextDirection của BuildContext hiện tại. Thường thì MaterialApp sẽ cung cấp TextDirection mặc định dựa trên ngôn ngữ thiết bị, nhưng bạn có thể ghi đè bằng Directionality. 4. Ứng dụng/Website đã ứng dụng Thực tế, hầu hết các ứng dụng và website lớn, có phạm vi toàn cầu đều phải sử dụng các cơ chế tương tự EdgeInsetsDirectional để đảm bảo trải nghiệm người dùng liền mạch. Bạn có thể thấy điều này ở: Facebook, Twitter, Instagram: Các ứng dụng mạng xã hội này phục vụ hàng tỷ người dùng trên toàn thế giới với vô số ngôn ngữ, bao gồm cả tiếng Ả Rập và tiếng Do Thái. Các phần tử UI như avatar, nút like, comment, hay các biểu tượng điều hướng đều phải 'lật' vị trí một cách thông minh để phù hợp với hướng đọc của người dùng. Google Maps, Google Search: Các sản phẩm của Google nổi tiếng về khả năng quốc tế hóa. Các thanh tìm kiếm, kết quả hiển thị, hay các chi tiết trên bản đồ đều điều chỉnh khoảng cách và vị trí để phù hợp với ngữ cảnh ngôn ngữ. WhatsApp, Telegram: Ứng dụng nhắn tin cũng là một ví dụ điển hình. Các bong bóng chat, hình ảnh đại diện, hay các biểu tượng trạng thái tin nhắn đều cần phải có padding/margin linh hoạt để hiển thị đúng trong cả LTR và RTL. Tóm lại, EdgeInsetsDirectional không chỉ là một công cụ tiện lợi, mà nó còn là một tư duy, một triết lý thiết kế UI hướng tới sự toàn cầu hóa và trải nghiệm người dùng tối ưu. Hãy biến nó thành một phần không thể thiếu trong bộ công cụ của bạn 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é!

24 Đọc tiếp
Giải Mã EditableTextState: Trái Tim Bí Ẩn Của Nhập Liệu Flutter
18/03/2026

Giải Mã EditableTextState: Trái Tim Bí Ẩn Của Nhập Liệu Flutter

Chào mừng các "đệ tử" đến với buổi học hôm nay! Anh Creyt sẽ "mổ xẻ" một khái niệm mà nhiều bạn có thể đã dùng hàng ngày mà không hay biết, đó là EditableTextState trong Flutter. Nghe cái tên có vẻ "học thuật" và "khó nhằn" đúng không? Đừng lo, anh sẽ biến nó thành "món khai vị" dễ nuốt nhất.### 1. EditableTextState là gì và để làm gì?Hãy hình dung thế này: Khi bạn gõ phím vào một ô văn bản trên màn hình điện thoại, đó không chỉ là việc các ký tự hiện lên "phép thuật" đâu. Phía sau cánh gà là cả một "nhà hát" đang vận hành. Trong Flutter, EditableText chính là cái sân khấu trần trụi nhất, nơi các ký tự của bạn sẽ "biểu diễn". Nó là nền tảng cơ bản nhất cho mọi widget nhập liệu, từ TextField quen thuộc cho đến TextFormField "tinh vi" hơn.Vậy còn EditableTextState? À ha, đây chính là "người quản lý hậu trường" tài ba của cái sân khấu EditableText đó. Nó không phải là một widget mà bạn trực tiếp nhìn thấy hay tương tác, mà là một đối tượng State nội bộ, "nắm giữ vận mệnh" của mọi thứ liên quan đến việc nhập liệu:Giá trị văn bản hiện tại: Nó biết bạn đang gõ cái gì.Vị trí con trỏ (cursor): Nó luôn theo dõi "ngón tay chỉ huy" đang ở đâu.Phạm vi chọn (selection): Nó biết bạn đang "khoanh vùng" đoạn văn bản nào.Trạng thái focus: Nó quyết định khi nào thì sân khấu sáng đèn (trường nhập liệu được focus) và khi nào thì tắt đèn.Xử lý input: Nó lắng nghe từng nhịp gõ phím, từng cử chỉ vuốt chạm để cập nhật nội dung.Tóm lại, nếu EditableText là "khung xương" của một trường nhập liệu, thì EditableTextState chính là "hệ thần kinh" điều khiển mọi hoạt động của nó. Mặc dù bạn thường dùng TextField (một widget "cao cấp" hơn đã "đóng gói" sẵn EditableText và quản lý EditableTextState giúp bạn), việc hiểu về EditableTextState sẽ giúp bạn "can thiệp sâu" hơn khi cần tạo ra những trải nghiệm nhập liệu "độc nhất vô nhị" mà TextField không thể đáp ứng.### 2. Code Ví Dụ Minh Họa Rõ RàngNhư anh đã nói, EditableTextState là internal, nên ta không trực tiếp tạo ra nó. Thay vào đó, ta sẽ làm việc với EditableText và các "công cụ" đi kèm để thấy nó vận hành. Hãy xem một ví dụ cơ bản nhất của EditableText:dart<br>import 'package:flutter/material.dart';<br><br>class EditableTextDemo extends StatefulWidget {<br> const EditableTextDemo({Key? key}) : super(key: key);<br><br> @override<br> State<EditableTextDemo> createState() => _EditableTextDemoState();<br>}<br><br>class _EditableTextDemoState extends State<EditableTextDemo> {<br> // 1. TextEditingController: "Tay điều khiển" chính của nội dung văn bản.<br> final TextEditingController _textController = TextEditingController();<br><br> // 2. FocusNode: "Bộ não" quản lý trạng thái focus của trường nhập liệu.<br> final FocusNode _focusNode = FocusNode();<br><br> @override<br> void initState() {<br> super.initState();<br> // Lắng nghe sự thay đổi của văn bản thông qua controller<br> _textController.addListener(() {<br> print('Text changed: ${_textController.text}');<br> });<br> // Lắng nghe sự thay đổi của focus<br> _focusNode.addListener(() {<br> print('Focus changed: ${_focusNode.hasFocus}');<br> });<br> }<br><br> @override<br> void dispose() {<br> // Rất quan trọng: Luôn "giải phóng" controller và focus node để tránh rò rỉ bộ nhớ.<br> _textController.dispose();<br> _focusNode.dispose();<br> super.dispose();<br> }<br><br> @override<br> Widget build(BuildContext context) {<br> return Scaffold(<br> appBar: AppBar(<br> title: const Text('EditableText Demo by Creyt'),<br> ),<br> body: Center(<br> child: Padding(<br> padding: const EdgeInsets.all(16.0),<br> child: Column(<br> mainAxisAlignment: MainAxisAlignment.center,<br> children: [<br> Container(<br> padding: const EdgeInsets.all(8.0),<br> decoration: BoxDecoration(<br> border: Border.all(color: Colors.blueAccent),<br> borderRadius: BorderRadius.circular(5.0),<br> ),<br> child: EditableText(<br> controller: _textController, // Gắn controller vào EditableText<br> focusNode: _focusNode, // Gắn focus node vào EditableText<br> style: const TextStyle(<br> color: Colors.black,<br> fontSize: 18.0,<br> ),<br> cursorColor: Colors.blue, // Màu con trỏ<br> backgroundCursorColor: Colors.grey, // Màu nền con trỏ khi không focus (thường không thấy rõ)<br> autofocus: true, // Tự động focus khi widget được tạo<br> maxLines: 1, // Cho phép nhập một dòng<br> keyboardType: TextInputType.text, // Loại bàn phím<br> onChanged: (text) {<br> // Callback khi văn bản thay đổi (cũng có thể dùng listener của controller)<br> print('OnChanged: $text');<br> },<br> onSubmitted: (text) {<br> // Callback khi người dùng nhấn Enter/Done<br> print('OnSubmitted: $text');<br> _focusNode.unfocus(); // Hủy focus sau khi submit<br> },<br> ),<br> ),<br> const SizedBox(height: 20),<br> ElevatedButton(<br> onPressed: () {<br> // Đặt text programmatically thông qua controller<br> _textController.text = 'Hello Creyt!';<br> // Đặt con trỏ về cuối<br> _textController.selection = TextSelection.fromPosition(<br> TextPosition(offset: _textController.text.length),<br> );<br> _focusNode.requestFocus(); // Yêu cầu focus lại<br> },<br> child: const Text('Set Text & Focus'),<br> ),<br> ElevatedButton(<br> onPressed: () {<br> _focusNode.unfocus(); // Hủy focus<br> },<br> child: const Text('Unfocus'),<br> ),<br> ],<br> ),<br> ),<br> ),<br> );<br> }<br>}<br>Trong ví dụ trên, _textController và _focusNode chính là "cánh tay nối dài" của bạn để điều khiển EditableTextState một cách gián tiếp. Mọi thao tác như gõ chữ, chọn văn bản, di chuyển con trỏ đều được EditableTextState xử lý bên trong, và bạn "giao tiếp" với nó thông qua các đối tượng này.### 3. Mẹo (Best Practices) từ "Lão Làng" Creyt"Đường tắt" hay "Đường mòn": Hầu hết thời gian, bạn nên dùng TextField hoặc TextFormField. Chúng là những "con đường cao tốc" đã được "trải nhựa" sẵn, cung cấp đủ tính năng và xử lý hầu hết các trường hợp thông thường. Chỉ khi nào bạn cần "đi rừng", tức là cần tùy chỉnh cực kỳ sâu mà TextField không cho phép, thì mới "lội suối" dùng EditableText.TextEditingController là "Đại Sứ": Luôn coi TextEditingController là "đại sứ" của bạn trong việc giao tiếp với nội dung của trường nhập liệu. Muốn đọc, muốn ghi, muốn thay đổi con trỏ hay vùng chọn? Cứ "gọi điện" cho "đại sứ" này.FocusNode là "Người Gác Cổng": FocusNode giúp bạn kiểm soát ai được "vào cửa" (trường nhập liệu nào được focus) và ai phải "ra ngoài". Luôn dùng nó để quản lý luồng focus trong ứng dụng, đặc biệt khi có nhiều trường nhập liệu."Dọn dẹp" sau khi dùng: Đây là "kim chỉ nam" của người lập trình chuyên nghiệp. Luôn nhớ dispose() TextEditingController và FocusNode trong phương thức dispose() của StatefulWidget để tránh rò rỉ bộ nhớ. Coi như "tắt đèn, đóng cửa" sau khi rời đi vậy.Khi nào thì dùng EditableText? Khi bạn đang xây dựng một "siêu phẩm" như trình soạn thảo mã nguồn (code editor), trình soạn thảo văn bản phong phú (rich text editor) với nhiều định dạng, hoặc một trường nhập liệu có giao diện và hành vi hoàn toàn khác biệt so với mặc định. Lúc đó, EditableText sẽ là "bãi đất trống" để bạn thỏa sức "xây dựng lâu đài" của riêng mình.### 4. Ứng dụng Thực TếNghe có vẻ "hàn lâm" nhưng EditableText và EditableTextState thực sự là xương sống của nhiều thứ bạn dùng hàng ngày:Trình soạn thảo mã nguồn (Code Editors) trên di động: Các ứng dụng như "Dcoder", "AIDE" hay các trình soạn thảo Markdown trên di động. Để có thể tô màu cú pháp (syntax highlighting), hiển thị số dòng, xử lý các phím tắt phức tạp, họ phải xây dựng trên nền tảng thấp như EditableText để kiểm soát từng pixel và từng sự kiện nhập liệu.Trình soạn thảo văn bản phong phú (Rich Text Editors): Các ứng dụng ghi chú như "Notion", "Google Keep" hoặc các trình soạn thảo email. Khi bạn muốn in đậm, in nghiêng, chèn hình ảnh vào giữa đoạn văn, đó là lúc EditableText được tùy chỉnh để hỗ trợ nhiều kiểu định dạng khác nhau.Các trường nhập liệu đặc biệt: Ví dụ, một trường nhập liệu cho công thức toán học, nơi bạn gõ ký hiệu và nó tự động hiển thị dưới dạng công thức đẹp mắt. Hoặc một trường nhập liệu "tag" nơi mỗi tag là một "viên thuốc" độc lập có thể xóa riêng lẻ.Vậy đó, "đệ tử" thấy không? Ngay cả những khái niệm tưởng chừng phức tạp nhất cũng có thể được "mổ xẻ" và hiểu rõ. EditableTextState không phải là "quái vật" mà là "người thợ xây" thầm lặng, tạo nên những trải nghiệm nhập liệu mượt mà mà chúng ta vẫn thường thưởng thức. Hãy nắm vững nó để "nâng tầm" khả năng lập trình của mình nhé! Hẹn gặp lại trong bài học tiếp theo! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

48 Đọc tiếp
EditableText: "Cái ruột" của mọi khung nhập liệu trong Flutter
18/03/2026

EditableText: "Cái ruột" của mọi khung nhập liệu trong Flutter

Chào mừng các "đệ tử" đến với bài học hôm nay! Giảng viên Creyt sẽ cùng các bạn "mổ xẻ" một khái niệm mà nhiều khi chúng ta bỏ qua, nhưng nó lại là "trái tim" của mọi thứ liên quan đến nhập liệu văn bản trong Flutter: EditableText. 1. EditableText là gì và để làm gì? – "Cái ruột" trần trụi Các bạn cứ hình dung thế này: Nếu TextField là một chiếc xe hơi đã hoàn thiện, bóng loáng, có đầy đủ ghế da, điều hòa mát lạnh, thì EditableText chính là cái khung sườn (chassis) trần trụi, khối động cơ và hệ thống lái cơ bản của chiếc xe đó. Nó là widget cấp thấp nhất trong Flutter chịu trách nhiệm xử lý việc nhập liệu, chọn văn bản, và di chuyển con trỏ mà không hề có bất kỳ trang trí (decoration) hay hiệu ứng hình ảnh mặc định nào. Mục đích sinh ra của nó? Đơn giản là để bạn có toàn quyền kiểm soát! Khi bạn cần một trường nhập liệu có giao diện "độc lạ Bình Dương", hoặc một hành vi tương tác mà TextField không thể đáp ứng được (ví dụ, một trình soạn thảo code, một editor rich-text với đủ thứ định dạng), thì EditableText chính là "công cụ" bạn cần để "đẽo gọt" từ đầu. 2. Tại sao không dùng TextField luôn cho rồi? Câu hỏi hay! TextField là "người anh em" phổ biến hơn nhiều, và trong 99% trường hợp, bạn nên dùng TextField. Nó đã "đóng gói" sẵn EditableText bên trong và thêm vào hàng tá tiện ích như InputDecoration (viền, label, hint text, icon), errorText, padding, scrollPhysics... Nó giống như việc bạn mua một căn nhà đã xây sẵn, đầy đủ tiện nghi, chỉ việc dọn vào ở. Nhưng đôi khi, bạn không muốn căn nhà xây sẵn đó. Bạn muốn tự mình "thiết kế kiến trúc" từng viên gạch, từng đường dây điện để tạo ra một "kiệt tác" có một không hai. Khi đó, EditableText là "viên gạch" cơ bản nhất để bạn bắt đầu xây dựng. 3. Code Ví Dụ Minh Hoạ – "Mổ xẻ" cái ruột Để các bạn dễ hình dung, chúng ta hãy cùng xem EditableText hoạt động như thế nào. Các bạn sẽ thấy nó "trần trụi" đến mức 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: 'EditableText Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const EditableTextScreen(), ); } } class EditableTextScreen extends StatefulWidget { const EditableTextScreen({super.key}); @override State<EditableTextScreen> createState() => _EditableTextScreenState(); } class _EditableTextScreenState extends State<EditableTextScreen> { // 1. Controller: "Người quản lý" nội dung văn bản late final TextEditingController _textController; // 2. FocusNode: "Người gác cổng" cho trạng thái tập trung (focus) late final FocusNode _focusNode; @override void initState() { super.initState(); _textController = TextEditingController(text: 'Hello Giảng viên Creyt!'); _focusNode = FocusNode(); } @override void dispose() { _textController.dispose(); // Luôn nhớ "dọn dẹp" controller _focusNode.dispose(); // Luôn nhớ "dọn dẹp" focus node super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('EditableText Demo của Creyt'), ), body: Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Container( padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( color: Colors.grey[200], border: Border.all(color: Colors.blueAccent, width: 2), borderRadius: BorderRadius.circular(8.0), ), child: EditableText( controller: _textController, focusNode: _focusNode, style: const TextStyle( fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold, ), cursorColor: Colors.red, // Màu con trỏ backgroundCursorColor: Colors.blue, // Màu con trỏ khi không focus (ít dùng) selectionColor: Colors.lightBlue.withOpacity(0.5), // Màu vùng chọn readOnly: false, // Có cho phép sửa đổi không? maxLines: 1, // Số dòng tối đa keyboardType: TextInputType.text, autofocus: true, // Tự động focus khi widget được tạo onChanged: (text) { // Bất cứ khi nào văn bản thay đổi print('Văn bản đã thay đổi: $text'); }, onSubmitted: (text) { // Khi người dùng nhấn Enter/Done print('Người dùng đã submit: $text'); _focusNode.unfocus(); // Bỏ focus sau khi submit }, ), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Thao tác với văn bản từ bên ngoài _textController.text = 'Creyt đã thay đổi nội dung!'; _focusNode.requestFocus(); // Yêu cầu focus lại }, child: const Icon(Icons.edit), ), ); } } Trong ví dụ trên, các bạn thấy EditableText chỉ cung cấp những thứ cốt lõi nhất: controller, focusNode, style, cursorColor, selectionColor và các callback như onChanged, onSubmitted. Mọi thứ về "khung viền", "nền", "padding" đều phải do bạn tự "đắp" bên ngoài bằng các widget như Container, Padding, BoxDecoration. 4. Mẹo (Best Practices) từ Giảng viên Creyt – "Bí kíp" để không "đi vào vết xe đổ" "Đừng đụng vào nó nếu không cần!": Đây là quy tắc vàng! Luôn bắt đầu với TextField. Chỉ khi nào bạn gặp phải một yêu cầu UI/UX quá đặc biệt mà TextField không thể đáp ứng, hoặc bạn cần tối ưu hiệu năng cực đoan cho một lượng lớn input, thì mới nghĩ đến EditableText. Nó giống như việc bạn chỉ nên tự xây nhà khi bạn là kiến trúc sư và thợ xây lành nghề, chứ không phải chỉ vì muốn "thử cho biết". Quản lý TextEditingController và FocusNode: Hai "anh bạn" này cực kỳ quan trọng và hay bị quên. Luôn nhớ khai báo chúng bằng late final hoặc khởi tạo trong initState và phải dispose() chúng trong phương thức dispose() của StatefulWidget. Nếu không, chúng sẽ gây ra rò rỉ bộ nhớ (memory leak), làm ứng dụng của bạn "nặng nề" và "chậm chạp" dần theo thời gian. Đây là "nghiệp vụ" cơ bản mà một lập trình viên "có tâm" phải làm. Tùy biến "đến tận chân răng": EditableText cho phép bạn kiểm soát màu con trỏ (cursorColor), màu vùng chọn (selectionColor), và thậm chí cả màu con trỏ khi không focus (backgroundCursorColor). Tận dụng điều này để tạo ra những trải nghiệm nhập liệu độc đáo, phù hợp với branding của ứng dụng bạn. Hiệu năng (Performance): Vì EditableText ít "phụ kiện" hơn TextField, trong những trường hợp cực đoan (ví dụ: một màn hình có hàng trăm ô nhập liệu nhỏ), nó có thể mang lại hiệu năng tốt hơn một chút. Tuy nhiên, đừng "mù quáng" mà hãy luôn dùng công cụ Profile của Flutter để kiểm tra trước khi quyết định "hy sinh" sự tiện lợi của TextField để đổi lấy EditableText. 5. Ứng dụng thực tế – "EditableText" đang ở đâu ngoài kia? "Thầy ơi, có ai dùng cái này không hay chỉ mình em học?" – Chắc chắn rồi! EditableText là nền tảng cho nhiều ứng dụng phức tạp mà bạn thấy hàng ngày: Trình soạn thảo mã nguồn (Code Editors): Các ứng dụng như VS Code (phiên bản web), hoặc các editor trên di động thường cần hiển thị cú pháp highlight, đánh số dòng, và các tính năng chỉnh sửa phức tạp. EditableText cung cấp cơ chế nhập liệu cơ bản, sau đó các lớp logic khác sẽ "vẽ" thêm các hiệu ứng đó lên trên. Trình soạn thảo văn bản đa định dạng (Rich Text Editors): Tưởng tượng các ứng dụng như Google Docs, Notion, hay thậm chí là phần soạn thảo tin nhắn trên các mạng xã hội cho phép bạn in đậm, in nghiêng, chèn link... EditableText xử lý phần nhập liệu thô, còn việc áp dụng các định dạng là do các lớp cao hơn quản lý. Các thanh tìm kiếm tùy chỉnh (Custom Search Bars): Đôi khi, một thanh tìm kiếm không chỉ đơn thuần là nhập text. Nó có thể có gợi ý đặc biệt, hiệu ứng chuyển động riêng, hoặc tích hợp trực tiếp vào một phần của UI game. EditableText là "viên gạch" lý tưởng để xây dựng những thanh tìm kiếm như vậy từ đầu. Ứng dụng game hoặc tương tác cao: Trong một số game, bạn có thể cần một ô nhập tên người chơi hoặc mật khẩu mà nó phải hòa quyện hoàn hảo vào phong cách đồ họa của game, không hề có một chút "mùi" của widget hệ thống. EditableText là lựa chọn tuyệt vời cho các tình huống này. Vậy là các bạn đã "thấm" được phần nào về EditableText rồi chứ? Hãy nhớ, nó là một "công cụ" mạnh mẽ, nhưng hãy sử dụng nó một cách thông minh và có mục đích. Đừng bao giờ ngại "mổ xẻ" các khái niệm cơ bản để hiểu sâu hơn về cách Flutter vận hành nhé! Hẹn gặp lại trong bài học tiếp theo! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

22 Đọc tiếp