Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
TimePickerDialog: Đặt Giờ Chuẩn Gen Z trong Flutter!
22/03/2026

TimePickerDialog: Đặt Giờ Chuẩn Gen Z trong Flutter!

Chào các homies Gen Z mê code! Hôm nay, anh Creyt sẽ dẫn mấy đứa đi khám phá một cái “đồng hồ báo thức” cực xịn trong Flutter, đó là TimePickerDialog. Nghe tên thì hơi học thuật nhưng thực ra nó là ông hoàng của việc chọn giờ trong app, giúp app mình trông chuyên nghiệp và dễ dùng hơn rất nhiều. TimePickerDialog là gì mà "chill" thế? Thực ra, TimePickerDialog nó như một cái bảng điều khiển thời gian mini, bật lên cái là cho người dùng chọn giờ và phút một cách trực quan, nhanh gọn lẹ. Thay vì phải gõ tay từng số, từng chữ số 0, hay loay hoay với format 12h/24h, thì anh bạn này sẽ show ra một giao diện đẹp đẽ, chuẩn Material Design để người dùng chỉ việc "chạm và chọn". Để làm gì ư? Đơn giản là để app của mấy đứa có thể hỏi người dùng "Mấy giờ bạn muốn đặt lịch?", "Mấy giờ bạn muốn hẹn giờ báo thức?", hay "Mấy giờ ship đồ ăn đến nhà?". Nó là mảnh ghép không thể thiếu cho các ứng dụng có yếu tố thời gian, giúp trải nghiệm người dùng mượt mà như lướt TikTok vậy. Code Ví Dụ: Gọi "Thần Đèn" TimePickerDialog ra sao? Để triệu hồi TimePickerDialog, chúng ta sẽ dùng hàm showTimePicker. Nó là một Future, nên kết quả trả về sẽ là một TimeOfDay? (có thể là null nếu người dùng hủy bỏ). Cứ hình dung thế này: mấy đứa bấm nút "Chọn Giờ", Flutter sẽ hỏi "Mấy giờ?". Người dùng chọn xong, Flutter sẽ trả lại cái giờ đó cho mình xử lý. Easy peasy! 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: 'TimePicker Demo của Creyt', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TimePickerScreen(), ); } } class TimePickerScreen extends StatefulWidget { const TimePickerScreen({super.key}); @override State<TimePickerScreen> createState() => _TimePickerScreenState(); } class _TimePickerScreenState extends State<TimePickerScreen> { TimeOfDay? _selectedTime; // Biến để lưu giờ đã chọn // Hàm bất đồng bộ để hiển thị TimePickerDialog Future<void> _selectTime(BuildContext context) async { // Gọi showTimePicker và chờ kết quả final TimeOfDay? pickedTime = await showTimePicker( context: context, // Context cần thiết để hiển thị dialog initialTime: _selectedTime ?? TimeOfDay.now(), // Giờ khởi tạo (nếu chưa chọn thì lấy giờ hiện tại) builder: (BuildContext context, Widget? child) { // Đây là chỗ để tùy chỉnh theme cho dialog, cho nó 'tone-sur-tone' với app mình return Theme( data: ThemeData.light().copyWith( primaryColor: Colors.teal, // Màu chủ đạo của dialog (phần header) colorScheme: const ColorScheme.light(primary: Colors.teal, onPrimary: Colors.white), // Màu sắc cho các thành phần chính buttonTheme: const ButtonThemeData(textTheme: ButtonTextTheme.primary), // Màu chữ nút ), child: child!, // Đừng quên trả về child! ); }, ); // Kiểm tra xem người dùng có chọn giờ không (không phải null) và có khác giờ cũ không if (pickedTime != null && pickedTime != _selectedTime) { setState(() { _selectedTime = pickedTime; // Cập nhật lại giờ đã chọn và render lại UI }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Chọn Giờ Cùng Creyt'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( _selectedTime == null ? 'Chưa chọn giờ nào cả, bấm nút đi bro!' : 'Giờ bạn chọn là: ${_selectedTime!.format(context)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 30), ElevatedButton.icon( onPressed: () => _selectTime(context), // Gọi hàm chọn giờ khi nhấn nút icon: const Icon(Icons.access_time), label: const Text('Chọn Giờ Ngay!', style: TextStyle(fontSize: 18)), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ], ), ), ); } } Giải thích nhanh: _selectedTime: Biến TimeOfDay? để lưu trữ giờ mà người dùng chọn. Dấu ? có nghĩa là nó có thể null (chưa chọn hoặc người dùng hủy). _selectTime(BuildContext context): Hàm async này sẽ gọi showTimePicker. initialTime: Cái này quan trọng nè. Nó là giờ mặc định khi dialog hiện ra. Nếu _selectedTime đã có giá trị thì dùng nó, không thì lấy TimeOfDay.now() (giờ hiện tại). builder: Đây là "phù thủy" giúp mấy đứa tùy chỉnh theme cho cái dialog, cho nó khớp với màu sắc của app mình. Đừng để nó lạc quẻ nha! setState: Sau khi người dùng chọn giờ và pickedTime không null, chúng ta dùng setState để cập nhật biến _selectedTime và làm mới giao diện. Mẹo của Creyt: Dùng sao cho "đỉnh của chóp"? Luôn có initialTime hợp lý: Đừng để người dùng phải cuộn mãi mới đến giờ hiện tại. Hãy set initialTime là giờ hiện tại hoặc giờ đã được chọn trước đó. Kiểm tra null cẩn thận: Kết quả từ showTimePicker có thể là null nếu người dùng nhấn nút "Cancel" hoặc click ra ngoài. Luôn kiểm tra if (pickedTime != null) trước khi xử lý. Tùy chỉnh Theme qua builder: Như trong ví dụ, dùng builder để đảm bảo TimePickerDialog có màu sắc, font chữ đồng bộ với app. Đừng để nó trông như "con ghẻ" nha! Localization auto-magic: Hàm _selectedTime!.format(context) rất hay ở chỗ nó sẽ tự động định dạng giờ theo ngôn ngữ và cài đặt của thiết bị (ví dụ: 12h AM/PM ở Mỹ, 24h ở Việt Nam). Khỏi lo vụ đa ngôn ngữ! Tối ưu UX: Đặt nút gọi TimePickerDialog ở vị trí dễ nhìn, dễ chạm. Đừng bắt người dùng phải tìm kiếm như chơi trốn tìm. Ứng dụng thực tế: "TimePickerDialog" đi đâu cũng gặp! Nhìn quanh đi, mấy đứa sẽ thấy TimePickerDialog (hoặc các phiên bản tương tự) xuất hiện khắp nơi: Google Calendar / Lịch của Apple: Khi tạo một sự kiện mới, mấy đứa chọn giờ bắt đầu/kết thúc. Ứng dụng đặt báo thức: Như cái app Đồng Hồ của điện thoại đó, chọn giờ báo thức là y chang. Các app giao đồ ăn / đặt xe: Chọn giờ giao hàng, giờ xe đến đón. Ứng dụng quản lý công việc / nhắc nhở: Set deadline, set thời gian cho một task cụ thể. Nói chung, cứ cái gì liên quan đến việc "chọn một mốc thời gian" là y như rằng có mặt anh bạn này. Khi nào nên dùng và khi nào nên "né"? Nên dùng khi: Mấy đứa cần người dùng chọn một mốc thời gian cụ thể (giờ và phút) mà không cần ngày tháng. Nó sinh ra là để làm việc này mà. Mấy đứa muốn giao diện chọn giờ chuẩn Material Design, nhất quán và đã được tối ưu về UX. Mấy đứa muốn tiết kiệm thời gian, không muốn tự code lại một cái picker phức tạp. Nên "né" khi: Cần chọn cả ngày và giờ: Lúc này, mấy đứa sẽ cần kết hợp showDatePicker với showTimePicker, hoặc dùng một thư viện bên thứ ba như flutter_datetime_picker để có một dialog chọn cả hai trong một. Cần chọn khoảng thời gian (duration): Ví dụ như "30 phút" hay "1 giờ 15 phút". TimePickerDialog chỉ chọn mốc thời gian, không phải độ dài thời gian. Yêu cầu giao diện quá "dị": Nếu app của mấy đứa có một thiết kế chọn giờ cực kỳ độc đáo, không theo chuẩn Material Design, thì có thể phải tự vẽ (custom widget) hoặc tìm thư viện khác. Nhưng anh Creyt khuyên là hạn chế, vì nó tốn công và dễ phát sinh lỗi. Kinh nghiệm của anh Creyt: Trong 90% trường hợp, TimePickerDialog của Flutter là đủ và là lựa chọn tốt nhất. Đừng cố gắng "phát minh lại bánh xe" trừ khi có lý do cực kỳ chính đáng. Nó đã được Flutter team tối ưu rất kỹ rồi, cứ thế mà dùng thô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é!

34 Đọc tiếp
Flutter: TickerProviderStateMixin - Bậc thầy điều khiển hoạt ảnh!
22/03/2026

Flutter: TickerProviderStateMixin - Bậc thầy điều khiển hoạt ảnh!

Chào các đệ tử GenZ mê code, hôm nay anh Creyt sẽ cùng các em "mổ xẻ" một "chú lính chì" thầm lặng nhưng cực kỳ quyền năng trong thế giới animation của Flutter: TickerProviderStateMixin. 1. TickerProviderStateMixin là gì mà nghe ngầu vậy anh Creyt? Đầu tiên, hãy tưởng tượng thế này: Trong một dàn nhạc giao hưởng, mỗi nhạc công (animatable widget) cần một người chỉ huy (conductor) để biết khi nào nên chơi nốt nào, nhanh chậm ra sao. Nếu không có conductor, dàn nhạc sẽ loạn xì ngầu, mỗi người một phách. Trong Flutter, các animation của chúng ta cũng vậy. Chúng cần một "nhịp đập" đều đặn, một "đồng hồ bấm giờ" để biết khi nào là lúc cập nhật trạng thái, khi nào là lúc vẽ lại UI để tạo ra chuyển động mượt mà. Cái "nhịp đập" đó, chính là Ticker. TickerProviderStateMixin chính là "người chỉ huy" đó, hay nói đúng hơn, nó là "người cung cấp" (Provider) cái "nhịp đập" (Ticker) cho các animation controller của chúng ta. Nó giúp Flutter biết được mỗi khung hình (frame) mới cần được vẽ lại, đồng bộ với tần số quét của màn hình (thường là 60fps) để tạo ra hiệu ứng mượt mà như bơ. Không có nó, animation của bạn sẽ không bao giờ chạy được, hoặc chạy như bị "đứt hơi" vậy đó! Tóm lại: Nó là một mixin mà bạn thêm vào State của StatefulWidget để cung cấp một Ticker cho AnimationController, đảm bảo animation chạy mượt mà và hiệu quả. 2. Dùng để làm gì? Bật mí sức mạnh tiềm ẩn! Thằng này sinh ra là để làm việc với AnimationController – trái tim của mọi animation tường minh (explicit animation) trong Flutter. Khi bạn khởi tạo một AnimationController, bạn sẽ thấy nó đòi hỏi một tham số vsync. Và đó chính là lúc TickerProviderStateMixin tỏa sáng! vsync (vertical synchronization) có nghĩa là đồng bộ hóa với tần số quét dọc của màn hình. Việc này cực kỳ quan trọng vì: Mượt mà: Đảm bảo animation chỉ được cập nhật khi màn hình sẵn sàng vẽ một khung hình mới, tránh hiện tượng "xé hình" (tearing) hoặc giật lag. Tiết kiệm pin: Ngăn chặn việc animation cập nhật quá nhanh hoặc quá chậm, gây lãng phí tài nguyên CPU/GPU và hao pin vô ích. TickerProviderStateMixin sẽ tự động ngừng "đập" khi widget không còn hiển thị, rất thông minh! Anh Creyt đã từng thấy nhiều bạn quên vsync hoặc truyền đại một cái gì đó vào rồi animation không chạy, hoặc chạy mà nóng máy như nung. Đó là vì các em chưa hiểu đúng vai trò của thằng TickerProviderStateMixin này đó! 3. Code Ví Dụ Minh Họa: Xem nó "nhảy múa" thế nào! Giờ thì chúng ta hãy cùng xem một ví dụ đơn giản về cách sử dụng TickerProviderStateMixin để tạo ra một hiệu ứng "mờ dần" (fade) cho một Container nhé. import 'package:flutter/material.dart'; // Bước 1: Tạo một StatefulWidget class MyAnimatedWidget extends StatefulWidget { @override _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState(); } // Bước 2: Thêm TickerProviderStateMixin vào lớp State // Đây là nơi phép màu xảy ra! class _MyAnimatedWidgetState extends State<MyAnimatedWidget> with TickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); // Bước 3: Khởi tạo AnimationController và truyền 'this' vào vsync // 'this' ở đây chính là TickerProviderStateMixin mà chúng ta vừa thêm vào! _controller = AnimationController( duration: const Duration(seconds: 2), // Animation chạy trong 2 giây vsync: this, // Đây là trái tim, là nhịp đập của animation! )..repeat(reverse: true); // Chạy lặp đi lặp lại và đảo chiều // Tạo một animation từ 0.0 đến 1.0 (mờ dần từ trong suốt đến rõ nét) _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller); } @override void dispose() { // Bước 4: Cực kỳ quan trọng! Luôn luôn giải phóng AnimationController // khi Widget không còn được sử dụng để tránh rò rỉ bộ nhớ. _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('TickerProviderStateMixin Demo')), // Sử dụng const cho hiệu suất tốt hơn body: Center( // Sử dụng FadeTransition để áp dụng hiệu ứng mờ dần child: FadeTransition( opacity: _animation, // Truyền animation vào thuộc tính opacity child: Container( width: 200, height: 200, color: Colors.blueAccent, child: const Center( child: Text( 'Anh Creyt', // Sử dụng const cho Text style: TextStyle(color: Colors.white, fontSize: 24), ), ), ), ), ), ); } } Trong ví dụ trên, _MyAnimatedWidgetState kế thừa State và sử dụng with TickerProviderStateMixin. Điều này cho phép chúng ta truyền this (chính là instance của _MyAnimatedWidgetState) vào tham số vsync của AnimationController. Kết quả là một Container màu xanh sẽ mờ dần rồi rõ nét liên tục, mượt mà như có phép thuật vậy đó! 4. Mẹo (Best Practices) từ anh Creyt: Luôn dispose() controller: Đây là lời dặn dò vàng ngọc! Nếu không, AnimationController sẽ tiếp tục chạy ngầm ngay cả khi widget đã bị loại bỏ, gây rò rỉ bộ nhớ và làm chậm ứng dụng. Hãy coi nó như việc tắt đèn khi ra khỏi phòng vậy, tiết kiệm điện và bảo vệ môi trường. SingleTickerProviderStateMixin vs TickerProviderStateMixin: SingleTickerProviderStateMixin: Dùng khi StatefulWidget của bạn chỉ cần một AnimationController. Nó nhẹ hơn và hiệu quả hơn trong trường hợp này. Hãy nghĩ nó như một nghệ sĩ solo, chỉ cần một nhạc cụ là đủ. TickerProviderStateMixin: Dùng khi StatefulWidget của bạn cần nhiều hơn một AnimationController. Ví dụ, bạn có 2-3 animation chạy độc lập trong cùng một widget. Lúc này, bạn cần cả một dàn nhạc, và TickerProviderStateMixin là người chỉ huy cho cả dàn. Mẹo ghi nhớ: Single là "một", Ticker là "nhiều". Dễ nhớ đúng không? Chỉ dùng khi cần: Đừng nhét TickerProviderStateMixin vào mọi StatefulWidget. Chỉ những widget nào có AnimationController thì mới cần đến nó thôi. Giống như không phải ai cũng cần một nhạc trưởng vậy. 5. Ví dụ thực tế các ứng dụng đã ứng dụng: Bạn có thể thấy TickerProviderStateMixin (hoặc SingleTickerProviderStateMixin) ở khắp mọi nơi trong các ứng dụng Flutter mà bạn dùng hàng ngày: Hiệu ứng chuyển cảnh (Transitions): Khi bạn mở một trang mới, các hiệu ứng trượt, mờ dần, phóng to/thu nhỏ... đều dùng đến nó. Tab Bars: Các thanh tab có hiệu ứng chuyển động mượt mà khi bạn chọn một tab khác. Loading indicators: Những vòng tròn xoay, thanh tiến trình... đều là animation. Hero animations: Hiệu ứng chuyển tiếp đẹp mắt khi một widget "bay" từ trang này sang trang khác. Cuộn danh sách (Scroll effects): Một số hiệu ứng cuộn đặc biệt cũng có thể dùng animation controller. 6. Thử nghiệm và Nên dùng cho case nào: Anh Creyt đã từng dùng TickerProviderStateMixin để tạo ra một hiệu ứng "lắc lư" nhẹ nhàng cho icon thông báo trên một ứng dụng chat, hoặc một hiệu ứng "nhấp nháy" tinh tế cho nút "Đăng ký" để thu hút sự chú ý. Nó giúp UI sống động và chuyên nghiệp hơn rất nhiều. Bạn nên dùng TickerProviderStateMixin (hoặc SingleTickerProviderStateMixin) khi: Bạn cần tạo các animation tường minh (explicit animations) với AnimationController. Bạn muốn kiểm soát chi tiết vòng đời của animation: bắt đầu, dừng, đảo chiều, lặp lại. Bạn đang xây dựng các widget phức tạp có nhiều hiệu ứng chuyển động độc lập. Bạn làm việc với các widget như FadeTransition, ScaleTransition, RotationTransition, AnimatedBuilder, hoặc CustomPainter mà muốn vẽ động. Bạn cần đồng bộ hóa animation với tần số quét của màn hình để đảm bảo hiệu suất và trải nghiệm người dùng tốt nhất. Nhớ nhé các đệ tử, TickerProviderStateMixin tuy nhỏ bé nhưng là một "tay chơi" không thể thiếu để tạo ra những animation mượt mà, sống động trong Flutter. Nắm vững nó, và các em sẽ mở khóa một level mới trong việc xây dựng UI đó! Chúc các em code vui vẻ và tạo ra những ứng dụng thật "chất"! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

169 Đọc tiếp
Flutter TickerProvider: Bật mí bí kíp làm app mượt mà như lụa
22/03/2026

Flutter TickerProvider: Bật mí bí kíp làm app mượt mà như lụa

Chào các dân chơi hệ code, lại là Creyt đây! Hôm nay, chúng ta sẽ cùng nhau 'bóc phốt' một ông trùm thầm lặng nhưng cực kỳ quan trọng trong thế giới animation của Flutter: TickerProvider. Nghe tên thì có vẻ hàn lâm, nhưng thật ra nó lại là 'cái đồng hồ' đếm nhịp cho mọi chuyển động mượt mà trên app của mấy đứa đấy. 1. TickerProvider là gì mà 'hot' thế? Thử tưởng tượng thế này nhé: Mấy đứa đang xem một buổi hòa nhạc giao hưởng hoành tráng. Mỗi nhạc công (tức là mỗi animation trong app của mấy đứa) đều cần chơi nhạc đúng nhịp, đúng phách để tạo nên một bản nhạc tuyệt vời. Nếu không có nhạc trưởng (chính là TickerProvider của chúng ta), mỗi nhạc công sẽ chơi theo ý mình, mạnh ai nấy đánh, và kết quả là... một mớ hỗn độn không ai muốn nghe. Trong Flutter, khi mấy đứa muốn tạo ra một animation có thể điều khiển được (kiểu như xoay một cái icon, di chuyển một cái widget từ A sang B, hay làm mờ dần một ảnh), mấy đứa sẽ dùng đến AnimationController. Mà cái AnimationController này, để biết khi nào thì nó cần 'nhảy một bước' (tức là cập nhật trạng thái animation), nó cần một cái 'đồng hồ' đếm nhịp. Đó chính là Ticker. TickerProvider chính là 'nhà cung cấp' những cái Ticker này. Nó đảm bảo rằng tất cả các animation trong một widget đều được đồng bộ hóa, nhận tín hiệu 'tick' đều đặn trên mỗi frame hình (thường là 60 lần/giây) để animation của mấy đứa trông mượt mà như bơ. Không có nó, animation của mấy đứa sẽ 'đứng hình' hoặc giật cục như phim 2 hình/giây vậy. Nói tóm lại, TickerProvider là một interface (giao diện) mà một State object cần implement để có thể cung cấp Ticker cho các AnimationController. Nó giúp Flutter biết khi nào cần vẽ lại UI để animation trông tự nhiên nhất. 2. Code Ví Dụ Minh Hoạ: 'Nghệ thuật' của sự mượt mà Trong Flutter, chúng ta thường dùng hai 'biến thể' của TickerProvider: SingleTickerProviderStateMixin: Dùng khi State của mấy đứa chỉ có một AnimationController. Đây là trường hợp phổ biến nhất. TickerProviderStateMixin: Dùng khi State của mấy đứa có nhiều AnimationController độc lập. Giờ mình sẽ dùng SingleTickerProviderStateMixin để làm một cái hộp xoay tròn 'mlem mlem' nhé: import 'package:flutter/material.dart'; class AnimatedRotationBox extends StatefulWidget { const AnimatedRotationBox({super.key}); @override State<AnimatedRotationBox> createState() => _AnimatedRotationBoxState(); } // Đây rồi, 'nhạc trưởng' của chúng ta: SingleTickerProviderStateMixin! class _AnimatedRotationBoxState extends State<AnimatedRotationBox> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, // 'vsync' chính là TickerProvider của chúng ta duration: const Duration(seconds: 2), // Xoay trong 2 giây )..repeat(); // Lặp lại vô tận } @override void dispose() { _controller.dispose(); // Quan trọng: Nhớ dọn dẹp 'nhạc trưởng' khi không dùng nữa! super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.rotate( angle: _controller.value * 2 * 3.14159, // Xoay 360 độ child: Container( width: 100, height: 100, color: Colors.blue, child: const Center( child: Text( 'Xoay!', style: TextStyle(color: Colors.white), ), ), ), ); }, ); } } // Để chạy thử, mấy đứa có thể dùng MaterialApp và Scaffold như này: // void main() { // runApp(const MyApp()); // } // class MyApp extends StatelessWidget { // const MyApp({super.key}); // @override // Widget build(BuildContext context) { // return MaterialApp( // home: Scaffold( // appBar: AppBar(title: const Text('TickerProvider Demo')), // body: const Center( // child: AnimatedRotationBox(), // ), // ), // ); // } // } Trong ví dụ trên: Chúng ta thêm with SingleTickerProviderStateMixin vào _AnimatedRotationBoxState. Điều này biến _AnimatedRotationBoxState thành một TickerProvider. Khi khởi tạo AnimationController, chúng ta truyền this vào tham số vsync. vsync chính là 'cái đồng hồ' mà AnimationController dùng để biết khi nào cần 'tick'. AnimatedBuilder sẽ lắng nghe sự thay đổi của _controller và rebuild widget con (ở đây là Transform.rotate) mỗi khi _controller cập nhật giá trị, tạo ra hiệu ứng xoay tròn mượt mà. 3. Mẹo Vặt (Best Practices) từ 'lão làng' Creyt Đúng người đúng việc: Luôn chọn đúng mixin. Nếu chỉ có một AnimationController, dùng SingleTickerProviderStateMixin để tiết kiệm tài nguyên. Nếu có nhiều, dùng TickerProviderStateMixin. Dọn dẹp là vàng: Luôn luôn gọi _controller.dispose() trong dispose() method của State. Nếu không, AnimationController sẽ tiếp tục chạy ngầm và giữ tham chiếu đến widget của mấy đứa ngay cả khi nó không còn hiển thị, gây ra lỗi rò rỉ bộ nhớ (memory leak) và ngốn pin điện thoại như uống nước lã. Hiểu rõ vsync: Tham số vsync trong AnimationController là nơi mấy đứa truyền vào instance của TickerProvider. Nó giống như việc mấy đứa đưa cho AnimationController cái điều khiển từ xa để nó biết khi nào cần 'kích hoạt' animation vậy. Khi nào thì dùng? Nếu mấy đứa cần điều khiển tường minh một animation (ví dụ: bắt đầu, dừng, lặp lại, đảo ngược, điều chỉnh tốc độ, kết hợp nhiều animation), thì AnimationController và TickerProvider là lựa chọn số 1. Còn mấy cái animation đơn giản kiểu AnimatedContainer hay Hero thì Flutter đã lo cho mấy đứa rồi, không cần TickerProvider trực tiếp đâu. 4. Ứng dụng thực tế: Mấy đứa thấy nó ở đâu? Thực ra, TickerProvider 'len lỏi' trong rất nhiều ứng dụng mà mấy đứa dùng hàng ngày: TikTok/Instagram Stories: Các hiệu ứng chuyển cảnh mượt mà khi vuốt giữa các story, các animation khi bấm nút like, thả tim. Ứng dụng thương mại điện tử: Hiệu ứng giỏ hàng bay lên khi thêm sản phẩm, các loading spinner khi chờ dữ liệu. Game mobile: Các animation của nhân vật, hiệu ứng skill, chuyển động của UI. Bất kỳ ứng dụng nào có UI/UX 'xịn sò': Từ navigation drawer trượt ra, các tab chuyển đổi mượt mà, đến các custom loading indicator phức tạp. Nói chung, cứ cái gì mà nó 'nhúc nhích' có chủ đích và trông 'mượt như lụa' trên app Flutter, thì khả năng cao là có 'bàn tay' của TickerProvider nhúng vào đấy. 5. Thử nghiệm và Nên dùng cho case nào? Creyt đã từng 'vật lộn' với mấy cái animation giật cục hồi mới vào nghề, cho đến khi 'ngộ' ra chân lý về TickerProvider. Mấy đứa cứ thử tưởng tượng, nếu không có nó, mỗi AnimationController sẽ tự tạo một cái Ticker riêng, và nếu có hàng chục cái animation cùng chạy, nó sẽ giống như hàng chục cái đồng hồ báo thức kêu loạn xạ trong đầu mấy đứa vậy – vừa tốn tài nguyên, vừa loạn. TickerProvider giải quyết vấn đề này bằng cách cung cấp một 'nhịp đập' chung, hiệu quả hơn rất nhiều. Nên dùng TickerProvider khi: Mấy đứa cần tạo các explicit animations (animation tường minh) với AnimationController. Mấy đứa muốn kiểm soát chặt chẽ vòng đời của animation: bắt đầu, dừng, lặp, đảo ngược, thay đổi tốc độ, lắng nghe sự kiện hoàn thành, v.v. Mấy đứa đang xây dựng các custom widget có animation phức tạp, không thể dùng các AnimatedWidget có sẵn của Flutter. Mấy đứa cần đồng bộ hóa nhiều animation khác nhau để chúng chạy mượt mà cùng nhau. Không cần dùng TickerProvider trực tiếp khi: Mấy đứa chỉ cần các implicit animations (animation ngầm định) như AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder... Flutter đã lo phần TickerProvider cho mấy đứa rồi. Mấy đứa chỉ hiển thị ảnh tĩnh, text tĩnh, không có bất kỳ chuyển động nào. Hy vọng với bài giảng 'sát sườn' này, mấy đứa đã hiểu rõ hơn về TickerProvider và không còn 'sợ hãi' khi đối mặt với animation trong Flutter nữa. Nhớ nhé, muốn app mượt mà, phải có 'nhạc trưởng' TickerProvider! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

38 Đọc tiếp
TickerMode: Bật/Tắt Animation, Flex Performance Flutter Cực Gắt!
22/03/2026

TickerMode: Bật/Tắt Animation, Flex Performance Flutter Cực Gắt!

Chào mấy đứa, hôm nay anh Creyt sẽ cùng tụi em 'vibe check' một khái niệm nghe hơi hàn lâm nhưng lại cực kỳ 'main character energy' trong Flutter: TickerMode. Nghe tên thì có vẻ phức tạp, nhưng thực ra nó như một cái công tắc đa năng, giúp tụi em 'flex' hiệu suất app một cách gọn gàng, không cần phải 'simp' theo kiểu tối ưu từng chút một đâu! 1. TickerMode là gì và để làm gì? (aka 'Cái công tắc thần thánh' đó) Trong Flutter, mọi animation đều cần một thứ gọi là Ticker. Tưởng tượng Ticker như một cái đồng hồ bấm giờ siêu tốc, mỗi khi màn hình của điện thoại refresh (khoảng 60 lần/giây), nó sẽ 'tick' một cái, báo hiệu cho animation biết là 'đã đến lúc di chuyển thêm một tí rồi đó!'. Các AnimationController mà tụi em hay dùng để tạo hiệu ứng xoay, mờ dần, hay di chuyển đều cần Ticker để hoạt động. Nhưng mà nè, có bao giờ tụi em thấy app mình tự nhiên hơi lag lag, hay pin tụt nhanh một cách 'low-key' không? Nhiều khi là do mấy cái animation vô tư chạy 'auto-pilot' ngay cả khi chẳng ai nhìn thấy nó! Ví dụ, tụi em có một PageView với 5 tab, mỗi tab có một cái animation nhỏ xinh. Khi tụi em đang ở tab 1, thì 4 cái animation ở tab 2, 3, 4, 5 kia nó vẫn cứ chạy âm thầm trong nền, đúng là 'simp' tài nguyên máy quá đi chứ! Đó chính là lúc TickerMode xuất hiện như một vị cứu tinh! TickerMode giống như một DJ chuyên nghiệp, nó có thể 'cut' nhạc (animation) ở một khu vực nhất định trong 'club' (UI subtree) nếu khu vực đó đang trống hoặc không cần thiết. Khi enabled của TickerMode được set là false, nó sẽ ra lệnh cho tất cả các Ticker trong subtree của nó 'chill out' đi, đừng có 'tick' nữa. Điều này đồng nghĩa với việc các animation trong khu vực đó sẽ tạm dừng, không tiêu tốn CPU/GPU hay pin nữa. 'No cap', nó giúp app tụi em mượt mà và tiết kiệm pin hơn hẳn! 2. Code Ví Dụ Minh Hoạ Rõ Ràng (Thực chiến luôn nha!) Để dễ hình dung, anh sẽ cho tụi em xem một ví dụ kinh điển với PageView. Tưởng tượng mỗi trang của PageView có một hình tròn đang xoay. Chúng ta chỉ muốn hình tròn ở trang hiện tại xoay thôi, còn các trang khác thì 'đứng hình' để tiết kiệm năng lượng. import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TickerMode Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const TickerModeDemo(), ); } } class TickerModeDemo extends StatefulWidget { const TickerModeDemo({super.key}); @override State<TickerModeDemo> createState() => _TickerModeDemoState(); } class _TickerModeDemoState extends State<TickerModeDemo> { final PageController _pageController = PageController(); int _currentPage = 0; @override void initState() { super.initState(); _pageController.addListener(() { if (_pageController.page != null) { setState(() { _currentPage = _pageController.page!.round(); }); } }); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TickerMode với PageView'), ), body: PageView.builder( controller: _pageController, itemCount: 3, itemBuilder: (context, index) { // Đây là điểm mấu chốt: TickerMode return TickerMode( enabled: index == _currentPage, // Chỉ enable Ticker nếu là trang hiện tại child: Center( child: AnimatedRotatingCircle( pageIndex: index, ), ), ); }, ), ); } } class AnimatedRotatingCircle extends StatefulWidget { final int pageIndex; const AnimatedRotatingCircle({required this.pageIndex, super.key}); @override State<AnimatedRotatingCircle> createState() => _AnimatedRotatingCircleState(); } // Sử dụng SingleTickerProviderStateMixin để cung cấp Ticker cho AnimationController class _AnimatedRotatingCircleState extends State<AnimatedRotatingCircle> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, // 'this' ở đây là TickerProvider duration: const Duration(seconds: 2), )..repeat(); // Lặp lại animation vô hạn } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Thêm một Text để dễ dàng thấy trang nào đang hoạt động return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ RotationTransition( turns: _controller, child: Container( width: 150, height: 150, decoration: BoxDecoration( color: Colors.primaries[widget.pageIndex % Colors.primaries.length], shape: BoxShape.circle, ), ), ), const SizedBox(height: 20), Text( 'Trang ${widget.pageIndex + 1}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), Text( // Kiểm tra xem animation có đang chạy không _controller.isAnimating ? 'Đang xoay...' : 'Đang nghỉ...', style: TextStyle( fontSize: 18, color: _controller.isAnimating ? Colors.green : Colors.red, ), ), ], ); } } Khi chạy code này, tụi em sẽ thấy chỉ hình tròn ở trang hiện tại mới xoay. Khi vuốt sang trang khác, hình tròn cũ sẽ dừng lại và hình tròn mới bắt đầu xoay. Đó là do TickerMode đã 'vibe check' và chỉ cho phép Ticker hoạt động khi index == _currentPage. 3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế ('Glow up' skill của tụi em) Ghi nhớ vai trò: Hãy nghĩ TickerMode như một 'energy saver mode' (chế độ tiết kiệm năng lượng) cho các widget animation. Nó không tự động 'kill' animation, mà nó 'pause' cái cơ chế tick của animation trong subtree của nó. Khi enabled: false, AnimationController sẽ không nhận được tín hiệu tick nữa, nên nó sẽ không cập nhật trạng thái animation. Khi nào dùng: Luôn tự hỏi: "Cái animation này có thực sự cần thiết phải chạy khi nó không hiển thị hoặc không phải là trọng tâm chú ý của người dùng không?" Nếu câu trả lời là không, hãy nghĩ đến TickerMode. Phân biệt với KeepAliveClientMixin: KeepAliveClientMixin giúp giữ trạng thái của widget (ví dụ: cuộn đến đâu, dữ liệu gì) khi nó không còn hiển thị trên màn hình nữa (như các trang của PageView khi vuốt qua). Còn TickerMode thì tập trung vào việc điều khiển animation của widget đó. Hai cái này có thể dùng chung với nhau để vừa giữ trạng thái, vừa tối ưu animation. Không lạm dụng: Đừng dùng TickerMode cho mọi thứ. Các animation quan trọng, luôn hiển thị (như loading indicator, Hero animation) thì không nên bị tắt. Hãy dùng một cách có chiến lược. 4. Ứng dụng thực tế các app/website đã dùng (Creyt's 'war stories') Trong thực tế phát triển, TickerMode được sử dụng rất nhiều trong các widget 'khủng' của Flutter: PageView: Như ví dụ ở trên, PageView tự nó đã sử dụng TickerMode để tắt animation của các trang không hiển thị, giúp trải nghiệm cuộn mượt mà hơn và tiết kiệm tài nguyên. IndexedStack: Widget này cũng thường được dùng để hiển thị một trong nhiều widget con. Các widget con không được hiển thị sẽ được 'pause' animation qua TickerMode. Offstage: Khi một widget được đặt Offstage (tức là không hiển thị nhưng vẫn tồn tại trong cây widget), nó cũng thường đi kèm với việc tắt animation của nó để tránh lãng phí. Các hệ thống tab/navigation tùy chỉnh: Nếu tụi em tự xây dựng một hệ thống tab phức tạp, việc sử dụng TickerMode để quản lý animation ở các tab không hoạt động là một 'best practice' để đảm bảo hiệu suất 'boujee' nhất. Anh Creyt từng 'ngu ngơ' để hàng tá animation chạy loạn xạ trong một app dashboard có nhiều biểu đồ động. Kết quả là app cứ giật giật, pin điện thoại nóng ran. Sau này mới 'ngộ' ra TickerMode, áp dụng vào thì app chạy mượt như lướt ván, khách hàng cứ khen lấy khen để. 'No cap', đó là một trong những bài học đắt giá về tối ưu hiệu suất! 5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào (Khi nào thì 'flex' TickerMode?) Anh đã từng thử nghiệm TickerMode trong nhiều trường hợp: Dashboard với nhiều widget động: Khi có các biểu đồ, widget cập nhật liên tục hoặc có animation riêng biệt. Chỉ cho phép animation chạy khi widget đó đang ở trên màn hình hoặc đang được người dùng tương tác. Ứng dụng có nhiều bước (multi-step forms): Chỉ animation ở bước hiện tại, các bước trước và sau thì 'standby'. Game nhỏ tích hợp trong app: Khi game đang chạy, các animation nền của app có thể được tắt để tập trung tài nguyên cho game. Khi thoát game, các animation nền lại được kích hoạt lại. Nên dùng TickerMode khi: Tụi em có một 'khu vực' UI chứa animation mà khu vực đó không phải lúc nào cũng hiển thị hoặc không phải là trọng tâm chú ý. Khi tụi em muốn cải thiện hiệu suất, giảm tải CPU/GPU và tiết kiệm pin cho thiết bị người dùng. Đặc biệt quan trọng với các ứng dụng có animation phức tạp hoặc chạy trên các thiết bị cấu hình không quá mạnh. Khi tụi em muốn có quyền kiểm soát 'granular' hơn về vòng đời animation trong các thành phần UI khác nhau. Tóm lại, TickerMode không phải là một viên đạn bạc, nhưng nó là một công cụ cực kỳ mạnh mẽ trong tay một developer 'có tầm nhìn'. Hãy dùng nó một cách khôn ngoan để 'flex' hiệu suất app của tụi em lên một tầm cao mới 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é!

43 Đọc tiếp
TickerFuture: Khi Animation trong Flutter 'đúng nhịp' GenZ!
22/03/2026

TickerFuture: Khi Animation trong Flutter 'đúng nhịp' GenZ!

Chào các 'dev-er' tương lai của hệ vũ trụ Flutter! Hôm nay, anh Creyt sẽ cùng các em khám phá một khái niệm tuy hơi 'low-level' nhưng lại là 'xương sống' của mọi animation mượt mà, đúng nhịp điệu trong app của chúng ta: TickerFuture. 1. TickerFuture là gì mà 'cool' vậy anh Creyt? Trong thế giới lập trình, đặc biệt là UI, mọi chuyển động, mọi animation đều cần một "nhịp tim" để biết khi nào cần cập nhật màn hình. Trong Flutter, "nhịp tim" đó chính là Ticker. Em cứ hình dung thế này: Khi em muốn nhảy một điệu nhảy thật "cháy", em cần một bản nhạc có nhịp điệu rõ ràng, đúng không? Ticker chính là cái "nhịp điệu" đó. Nó "tick" (đánh dấu) mỗi khi một frame mới của ứng dụng sẵn sàng được vẽ lại. Mỗi một "tick" là một cơ hội để animation của em tiến thêm một bước, tạo ra sự chuyển động mượt mà. Vậy TickerFuture là gì? Đơn giản thôi, nó là một Future – giống như một lời hứa trong tương lai vậy – mà sẽ hoàn thành ngay khi Ticker của bạn đã sẵn sàng để bắt đầu "tick". Tức là, nó đảm bảo rằng cái "nhịp điệu" đã được khởi động và sẵn sàng để "đập" những nhịp đầu tiên. Nó như việc em chờ DJ bật nhạc và xác nhận "Ok, nhạc đã lên, sẵn sàng nhảy!" vậy. Để làm gì? Nó giúp em đồng bộ hóa các animation, hoặc thực hiện một hành động nào đó chắc chắn sau khi Ticker đã được kích hoạt. Tránh tình trạng "nhạc chưa lên" mà em đã "nhảy" khiến animation bị giật lag, hoặc tệ hơn là không chạy. 2. Code Ví Dụ Minh Họa: 'Nhảy' cùng TickerFuture Để các em dễ hình dung, anh Creyt sẽ "code" một ví dụ đơn giản: một cái hộp sẽ tự động "nhảy múa" (thay đổi kích thước) nhưng chỉ khi Ticker đã "khởi động" và sẵn sàng. import 'package:flutter/material.h'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TickerFuture Demo by Creyt', theme: ThemeData( primarySwatch: Colors.deepPurple, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TickerFutureScreen(), ); } } class TickerFutureScreen extends StatefulWidget { const TickerFutureScreen({super.key}); @override State<TickerFutureScreen> createState() => _TickerFutureScreenState(); } class _TickerFutureScreenState extends State<TickerFutureScreen> with SingleTickerProviderStateMixin { // Cần mixin này để cung cấp Ticker late AnimationController _controller; late Animation<double> _animation; String _status = 'Đang chờ Ticker khởi động...'; bool _isAnimating = false; @override void initState() { super.initState(); // Khởi tạo AnimationController với vsync là 'this' (SingleTickerProviderStateMixin) _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); // Định nghĩa animation từ 50px đến 200px _animation = Tween<double>(begin: 50.0, end: 200.0).animate(_controller) ..addListener(() { // Mỗi khi giá trị animation thay đổi, vẽ lại widget setState(() {}); }) ..addStatusListener((status) { // Khi animation hoàn thành hoặc trở về trạng thái ban đầu, đảo ngược hoặc tiếp tục if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); // Đây là lúc TickerFuture "lên tiếng"! // Chúng ta chờ đợi Ticker của controller sẵn sàng (future hoàn thành) _controller.ticker.future.then((_) { // Khi Ticker đã sẵn sàng, cập nhật trạng thái UI và bắt đầu animation setState(() { _status = 'Ticker đã sẵn sàng! Bắt đầu animation "nhảy múa"...'; _isAnimating = true; }); _controller.forward(); // Bắt đầu animation }).catchError((error) { // Xử lý lỗi nếu Ticker không thể khởi động setState(() { _status = 'Lỗi Ticker: ${error.toString()}'; }); }); } @override void dispose() { // Cực kỳ quan trọng: Giải phóng AnimationController để tránh rò rỉ bộ nhớ _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TickerFuture in Action by Creyt'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _status, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 40), Container( width: _animation.value, height: _animation.value, decoration: BoxDecoration( color: _isAnimating ? Colors.deepPurpleAccent : Colors.grey[400], borderRadius: BorderRadius.circular(20), boxShadow: _isAnimating ? [ BoxShadow( color: Colors.deepPurple.withOpacity(0.4), blurRadius: 15, spreadRadius: 5, ), ] : [], ), alignment: Alignment.center, child: Text( _isAnimating ? 'Animating!' : 'Waiting...', style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), ), ), ], ), ), ); } } Trong ví dụ trên, chúng ta dùng _controller.ticker.future.then((_) { ... }); để đảm bảo rằng khối code bên trong then chỉ chạy khi Ticker đã thực sự sẵn sàng. Nếu không có dòng này, với những trường hợp phức tạp hơn, animation có thể cố gắng chạy trước khi Ticker được khởi tạo hoàn chỉnh, dẫn đến lỗi hoặc hành vi không mong muốn. 3. Mẹo (Best Practices) từ anh Creyt để 'Pro' hơn! 'Dispose' Ticker Luôn và Ngay!: Nhớ kỹ câu thần chú này: _controller.dispose() trong phương thức dispose() của State. Nếu không, Ticker sẽ tiếp tục "tick" trong nền, ngốn tài nguyên và gây rò rỉ bộ nhớ. Như việc em tắt nhạc sau khi bữa tiệc kết thúc vậy, đừng để nó chạy "chay" hoài! Chọn đúng "DJ" (TickerProvider): SingleTickerProviderStateMixin: Dùng khi widget của em chỉ có một AnimationController. Tiết kiệm tài nguyên hơn. TickerProviderStateMixin: Dùng khi widget của em cần nhiều hơn một AnimationController. Nó như một DJ có thể điều khiển nhiều bàn nhạc cùng lúc. Khi nào cần "đợi nhạc lên" (TickerFuture)?: Thường thì AnimationController sẽ tự xử lý việc khởi tạo Ticker khá tốt. Em chỉ thực sự cần đến TickerFuture khi: Em đang "debug" một vấn đề animation bị giật ở frame đầu tiên hoặc không chạy. Em cần đồng bộ nhiều animation phức tạp hoặc cần một hành động chắc chắn phải xảy ra sau khi Ticker đã sẵn sàng. Em đang xây dựng một widget animation tùy chỉnh rất "deep" và cần kiểm soát chính xác vòng đời của Ticker. Hiểu "lời hứa" (Future): Để dùng TickerFuture hiệu quả, hãy ôn lại kiến thức về Future, async/await trong Dart nhé. Nó sẽ giúp em "bắt sóng" được cách các tác vụ bất đồng bộ hoạt động. 4. Ứng dụng thực tế: "Nhịp điệu" của TickerFuture ở đâu? TickerFuture, hay Ticker nói chung, là nền tảng của rất nhiều hiệu ứng "mượt mà" em thấy hàng ngày: Game Development (Mini-games trong app): Các game nhỏ trong ứng dụng (như game "flappy bird" trong một app nào đó) cần các yếu tố di chuyển liên tục, đồng bộ. Ticker là thứ cung cấp nhịp độ để các đối tượng di chuyển "đúng phách". Complex UI Animations: Các hiệu ứng chuyển cảnh giữa các màn hình (hero animations, page transitions), các animation loading screen "xịn sò", hoặc các biểu đồ động. Khi có nhiều animation phụ thuộc vào nhau, TickerFuture có thể giúp đảm bảo chúng khởi động đúng trình tự. Video Players / Custom Media Controls: Khi em thấy thanh progress bar của video chạy mượt mà theo thời gian, hoặc các nút play/pause có hiệu ứng chuyển đổi "ngọt" thì đó chính là nhờ Ticker đang làm việc. 5. Thử nghiệm và Case nào nên dùng? Thử nghiệm: Em hãy chạy code ví dụ của anh. Sau đó, thử bỏ dòng _controller.ticker.future.then((_) { ... }); và thay bằng việc gọi _controller.forward(); trực tiếp trong initState(). Với ví dụ đơn giản này, em có thể không thấy sự khác biệt rõ rệt ngay lập tức. Nhưng hãy tưởng tượng một hệ thống animation phức tạp hơn, nơi việc khởi tạo Ticker mất nhiều thời gian hơn một chút, hoặc có nhiều Ticker cần đồng bộ. Lúc đó, việc "đợi nhạc lên" bằng TickerFuture sẽ phát huy tác dụng! Nên dùng khi nào? Khi em gặp phải các vấn đề "bug" animation như: animation không chạy ngay lập tức, bị giật ở frame đầu tiên, hoặc không đồng bộ với các yếu tố khác. Khi em đang xây dựng một thư viện animation tùy chỉnh hoặc một widget phức tạp cần kiểm soát chặt chẽ vòng đời của animation. Khi em cần đảm bảo một tác vụ nào đó (ví dụ: gửi một sự kiện phân tích, tải dữ liệu) chỉ xảy ra sau khi Ticker đã hoạt động và animation đã bắt đầu chạy. Không nên lạm dụng: Đối với hầu hết các animation đơn giản trong Flutter, AnimationController đã đủ "thông minh" để quản lý Ticker một cách tự động. Dùng TickerFuture chỉ khi em thực sự cần sự kiểm soát ở mức độ "low-level" này để giải quyết các vấn đề cụ thể, hoặc khi em muốn tạo ra những hiệu ứng "độc lạ" cần đồng bộ hóa cực kỳ chính xác. Vậy đó, 'dev-er' của anh! TickerFuture không phải là thứ bạn dùng hàng ngày, nhưng khi cần, nó sẽ là "công cụ" giúp animation của bạn "chill" và "mượt" như lướt sóng. Hãy nhớ, hiểu biết sâu về những "mảnh ghép" nhỏ như Ticker sẽ giúp bạn "hack" được những animation đỉnh cao, "tạo trend" trong giới dev Flutter đấy! Keep coding, keep rocking! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

38 Đọc tiếp
Flutter Ticker: 'Nhịp Đập' Bí Mật Của Mọi Animation Mượt Mà
22/03/2026

Flutter Ticker: 'Nhịp Đập' Bí Mật Của Mọi Animation Mượt Mà

Flutter Ticker: 'Nhịp Đập' Bí Mật Của Mọi Animation Mượt Mà Chào các chiến thần Gen Z, hôm nay anh Creyt sẽ cùng các em 'bóc tách' một khái niệm nghe hơi học thuật nhưng lại là 'linh hồn' của mọi animation mượt mà trong Flutter: Ticker. Ticker là gì mà 'hot' vậy Gen Z? Nếu coi một animation là một điệu nhảy, thì Ticker chính là ông DJ 'khét lẹt' đứng sau bàn mix, đảm bảo mỗi bước nhảy, mỗi động tác đều chuẩn nhịp, không lệch pha một mili giây nào. Hay nói cách khác, Ticker giống như một chiếc đồng hồ bấm giờ siêu chính xác, nhưng không phải để đếm giây, mà để 'báo thức' cho hệ thống biết: "Ê, đến giờ vẽ frame mới rồi đó!" mỗi khi màn hình sẵn sàng cập nhật. Trong thế giới Flutter, khi em muốn tạo ra một animation tùy chỉnh (custom animation), ví dụ như một cái nút nhấp nháy, một icon xoay tròn, hay một thanh progress bar di chuyển mượt mà, thì Ticker chính là 'bộ đếm nhịp' cung cấp các tín hiệu 'tick' đều đặn. Mỗi 'tick' này tương ứng với một frame mới được dựng hình trên màn hình của thiết bị. Và quan trọng nhất, Ticker đảm bảo các 'tick' này được đồng bộ hóa với tần số quét của màn hình (hay còn gọi là vsync - vertical synchronization), giúp animation không bị giật lag, mà mượt mà như... bơ vậy. Ticker sinh ra để làm gì? Ngày xưa, khi chưa có Ticker, mấy anh dev hay dùng Timer.periodic để tạo animation. Nghe thì có vẻ ổn, cứ mỗi X mili giây thì update UI một lần. Nhưng vấn đề là: cái Timer nó chạy theo đồng hồ hệ thống, còn màn hình của em thì lại có tần số quét riêng (60Hz, 90Hz, 120Hz...). Thế là dễ dẫn đến tình trạng 'ông nói gà, bà nói vịt', animation bị lệch pha, giật cục, không đồng bộ với màn hình, nhìn 'phèn' lắm. Ticker sinh ra để giải quyết đúng vấn đề đó. Nó không chỉ đơn thuần là một bộ đếm thời gian, mà nó 'thông minh' hơn nhiều. Ticker biết lắng nghe tín hiệu vsync từ màn hình, chỉ 'tick' khi màn hình thực sự sẵn sàng vẽ frame mới. Điều này đảm bảo: Mượt mà tối đa: Animation luôn được đồng bộ với tần số quét của màn hình. Tiết kiệm pin: Ticker còn biết 'ngủ đông' khi ứng dụng của em bị đẩy xuống nền, không cần vẽ animation nữa. Khi app được kích hoạt lại, nó mới 'tỉnh dậy' và tiếp tục công việc. Timer.periodic thì cứ chạy 'điên cuồng' dù app có ở đâu đi chăng nữa. Nói tóm lại, Ticker là nền tảng cho mọi AnimationController trong Flutter, giúp chúng ta tạo ra những chuyển động UI sống động và chuyên nghiệp. Code Ví Dụ: Ticker 'quẩy' cùng AnimationController Để sử dụng Ticker, thường thì chúng ta sẽ không trực tiếp tạo một đối tượng Ticker mà sẽ thông qua AnimationController. AnimationController cần một TickerProvider để có thể tạo ra Ticker cho riêng nó. Có hai loại TickerProvider mà các em hay dùng: SingleTickerProviderStateMixin: Dùng khi chỉ có MỘT AnimationController trong State của widget. TickerProviderStateMixin: Dùng khi có NHIỀU AnimationController trong State của widget. Đây là ví dụ kinh điển về việc làm một hình vuông xoay tròn sử dụng AnimationController và SingleTickerProviderStateMixin: import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Ticker Demo', theme: ThemeData.dark(), home: const RotationAnimationScreen(), ); } } // 1. Phải dùng StatefulWidget vì chúng ta cần quản lý trạng thái của animation class RotationAnimationScreen extends StatefulWidget { const RotationAnimationScreen({super.key}); @override State<RotationAnimationScreen> createState() => _RotationAnimationScreenState(); } // 2. Mixin SingleTickerProviderStateMixin để cung cấp Ticker cho AnimationController class _RotationAnimationScreenState extends State<RotationAnimationScreen> with SingleTickerProviderStateMixin { // 3. Khai báo AnimationController late AnimationController _controller; @override void initState() { super.initState(); // 4. Khởi tạo AnimationController // 'vsync: this' chính là nơi chúng ta cung cấp TickerProvider // duration: Thời gian hoàn thành một chu kỳ animation _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); // 5. Chạy animation lặp lại vô hạn } @override void dispose() { // 6. RẤT QUAN TRỌNG: Giải phóng AnimationController khi widget bị hủy // Nếu không, Ticker sẽ tiếp tục chạy ngầm và gây rò rỉ bộ nhớ (memory leak) _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Xoay Xoay cùng Ticker'), ), body: Center( // 7. Sử dụng AnimatedBuilder để rebuild widget khi giá trị animation thay đổi // Thay vì dùng setState trong listener của controller, // AnimatedBuilder hiệu quả hơn vì nó chỉ rebuild phần con cần thiết. child: AnimatedBuilder( animation: _controller, builder: (context, child) { // RotationTransition lấy giá trị từ animation để xoay widget con return Transform.rotate( // value của controller chạy từ 0.0 đến 1.0. // Ta nhân với 2 * pi để có một vòng xoay đầy đủ. angle: _controller.value * 2 * 3.14159, child: Container( width: 150, height: 150, color: Colors.deepPurpleAccent, child: const Center( child: Text( 'Creyt', style: TextStyle(color: Colors.white, fontSize: 24) ) ), ), ); }, ), ), ); } } Trong ví dụ trên: _RotationAnimationScreenState sử dụng SingleTickerProviderStateMixin để cung cấp vsync cho AnimationController. _controller được khởi tạo với vsync: this, tức là nó sẽ sử dụng Ticker được cung cấp bởi SingleTickerProviderStateMixin. _controller.repeat() khiến animation chạy liên tục. AnimatedBuilder lắng nghe sự thay đổi của _controller và rebuild widget con (ở đây là Transform.rotate) mỗi khi Ticker báo một 'tick' mới, tạo hiệu ứng xoay mượt mà. Và đặc biệt quan trọng: _controller.dispose() trong dispose() để dọn dẹp 'nhịp đập' khi không cần nữa. Mẹo 'xịn xò' từ anh Creyt để dùng Ticker hiệu quả Đừng bao giờ quên dispose(): Đây là lỗi 'kinh điển' nhất. Nếu em tạo AnimationController mà không dispose() nó khi widget bị hủy, Ticker bên trong sẽ vẫn tiếp tục chạy ngầm, gây rò rỉ bộ nhớ và làm chậm ứng dụng của em. Cứ như có một thằng DJ cứ chơi nhạc dù quán bar đã đóng cửa vậy. Chọn đúng TickerProvider: SingleTickerProviderStateMixin: Dùng khi chỉ có một AnimationController trong State. Đơn giản và nhẹ nhàng. TickerProviderStateMixin: Dùng khi có nhiều AnimationController trong State. Ví dụ, em muốn có nhiều animation chạy độc lập trong cùng một widget. Hiểu vsync là bạn: vsync không chỉ là một tham số, nó là nguyên tắc vàng. Nó đảm bảo animation của em đồng bộ với tần số quét của màn hình, tạo trải nghiệm mượt mà nhất cho người dùng. Sử dụng AnimatedBuilder (hoặc ListenableBuilder): Thay vì dùng addListener cho AnimationController và gọi setState(), hãy dùng AnimatedBuilder. Nó thông minh hơn, chỉ rebuild phần widget con cần thiết, tối ưu hiệu suất hơn rất nhiều. Performance là vua: Dù Ticker rất hiệu quả, đừng lạm dụng animation một cách vô tội vạ. Chỉ animate những gì cần thiết và tối ưu hóa widget con để tránh rebuild toàn bộ cây widget không cần thiết. Ticker 'góp mặt' ở đâu trong thế giới app? Ticker là 'người hùng thầm lặng' đứng sau rất nhiều hiệu ứng UI mà các em thấy hàng ngày: Loading Spinners/Progress Bars: Những vòng tròn xoay, thanh chạy đi chạy lại báo hiệu đang tải dữ liệu. Page Transitions: Hiệu ứng chuyển cảnh giữa các màn hình, ví dụ như trượt từ phải sang trái, fade in/out. Interactive UI Elements: Các nút bấm có hiệu ứng nhấn giữ, slider kéo thả mượt mà, hay các tab bar có hiệu ứng gạch chân di chuyển. Custom Animations: Bất cứ khi nào em muốn tạo một animation không có sẵn trong các widget ImplicitlyAnimatedWidget của Flutter (ví dụ: AnimatedOpacity, AnimatedContainer), Ticker sẽ là 'công cụ' đắc lực. Game nhẹ: Đối với các game đơn giản viết bằng Flutter, Ticker cũng là cơ chế để cập nhật vị trí của các đối tượng game theo từng frame. Các ứng dụng lớn như Instagram (hiệu ứng story, chuyển động khi tương tác), Spotify (thanh progress bài hát, hiệu ứng equalizer), hay Google Maps (animation khi chuyển hướng, phóng to/thu nhỏ) đều có thể sử dụng các nguyên lý tương tự Ticker để đảm bảo UI luôn phản hồi mượt mà. Kinh nghiệm 'xương máu' của anh Creyt: Khi nào nên 'triệu hồi' Ticker? Hồi xưa, anh Creyt cũng từng 'ngây thơ' dùng Timer.periodic để làm mấy cái animation đơn giản. Kết quả là nhìn nó cứ 'giật cục' sao ấy, nhiều khi còn bị lỗi UI do không đồng bộ. Đến khi 'khai sáng' được Ticker, mọi thứ như bước sang một trang mới, animation mượt mà đến bất ngờ. Vậy khi nào em nên 'triệu hồi' Ticker (qua AnimationController)? Khi cần animation phức tạp, tùy chỉnh: Nếu em muốn kiểm soát chính xác từng khía cạnh của animation (tốc độ, đường cong chuyển động, lặp lại, đảo ngược), hoặc chuỗi nhiều animation chạy nối tiếp nhau, thì AnimationController (và Ticker) là lựa chọn duy nhất. Animation không tuyến tính (non-linear): Khi em muốn hiệu ứng chuyển động tăng tốc rồi giảm tốc (ease-in-out), hoặc theo một đường cong phức tạp nào đó, AnimationController kết hợp với Curve sẽ làm được điều đó. Animation tương tác: Khi animation cần phản ứng với cử chỉ của người dùng (kéo, vuốt, chạm), ví dụ như một thanh trượt có hiệu ứng đàn hồi, hoặc một widget mở ra/đóng lại theo tốc độ kéo của ngón tay. Khi nào thì không cần dùng đến Ticker trực tiếp? Nếu animation của em đơn giản, chỉ là thay đổi một thuộc tính của widget (ví dụ: opacity, size, color, alignment) và không cần kiểm soát quá chi tiết, hãy ưu tiên dùng các ImplicitlyAnimatedWidget như AnimatedOpacity, AnimatedContainer, AnimatedAlign, Hero widget. Chúng đã tự động quản lý AnimationController và Ticker bên trong rồi, giúp code của em gọn gàng hơn nhiều. Tóm lại: Ticker là 'nhịp đập' không thể thiếu cho các animation tùy chỉnh và phức tạp trong Flutter. Nắm vững nó, em sẽ có trong tay 'quyền năng' để biến những ý tưởng UI sống động nhất thành hiện thực. Hãy thực hành code ví dụ và 'nghiền ngẫm' các mẹo của anh Creyt để trở thành một 'phù thủy animation' trong thế giới Flutter 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é!

37 Đọc tiếp
TextStyleTween: Phù Thủy Biến Hình Cho Text Của Bạn Trong Flutter!
22/03/2026

TextStyleTween: Phù Thủy Biến Hình Cho Text Của Bạn Trong Flutter!

TextStyleTween: Phù Thủy Biến Hình Cho Text Của Bạn Trong Flutter Chào các chiến thần code của gen Z! Anh Creyt đây, hôm nay chúng ta sẽ cùng "flex" với một khái niệm mà nhiều bạn hay bỏ qua, nhưng nó lại là "chìa khóa vàng" để UI của bạn trông "mượt mà" và "có hồn" hơn rất nhiều: TextStyleTween. 1. TextStyleTween là gì và để làm gì? Để anh Creyt kể bạn nghe một câu chuyện: Tưởng tượng bạn có một chiếc áo phông Gucci "real deal" hôm nay màu xanh neon cực chất, nhưng ngày mai bạn muốn nó tự động "biến hình" thành màu hồng pastel mà không cần phải thay chiếc áo khác. Và đặc biệt, quá trình biến hình đó phải mượt mà, từ từ chuyển sắc chứ không phải "phập" một cái đổi màu luôn. Trong thế giới Flutter, TextStyleTween chính là cái "phù thủy biến hình" đó, nhưng là dành cho style của chữ (TextStyle) của bạn. Nó không phải là một Widget mà bạn "thả" vào cây Widget trực tiếp, mà là một công cụ giúp bạn "nội suy" (interpolate) giữa hai TextStyle khác nhau. Nói một cách đơn giản hơn, bạn đưa cho nó một TextStyle ban đầu (gọi là begin) và một TextStyle kết thúc (gọi là end). TextStyleTween sẽ tạo ra một chuỗi các TextStyle "trung gian" giữa begin và end đó, giúp hiệu ứng chuyển đổi màu chữ, kích thước chữ, độ đậm nhạt, font chữ... trông "đỉnh của chóp" chứ không phải "giật cục như phim 24 hình" ngày xưa. Để làm gì ư? Để tạo ra các hiệu ứng chuyển động "có gu" cho chữ, làm cho UI của bạn trở nên sinh động, phản hồi tốt hơn với người dùng và tạo ra trải nghiệm "wow" đó mà các app "xịn xò" hay có. 2. Code Ví Dụ Minh Họa Rõ Ràng Để sử dụng TextStyleTween, chúng ta thường kết hợp nó với AnimationController và AnimatedBuilder (hoặc TweenAnimationBuilder nếu bạn muốn đơn giản hóa). Dưới đây là một ví dụ kinh điển: Chúng ta sẽ tạo một Widget đơn giản, khi bạn nhấn nút, kích thước và màu sắc của chữ sẽ thay đổi mượt mà. import 'package:flutter/material.dart'; class TextStyleTweenExample extends StatefulWidget { const TextStyleTweenExample({super.key}); @override State<TextStyleTweenExample> createState() => _TextStyleTweenExampleState(); } class _TextStyleTweenExampleState extends State<TextStyleTweenExample> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<TextStyle> _textStyleAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), // Thời gian chuyển đổi ); // Định nghĩa TextStyle bắt đầu và kết thúc final TextStyle beginStyle = TextStyle( fontSize: 20.0, color: Colors.blueAccent, fontWeight: FontWeight.normal, fontFamily: 'Roboto', ); final TextStyle endStyle = TextStyle( fontSize: 36.0, color: Colors.deepOrange, fontWeight: FontWeight.bold, fontFamily: 'Montserrat', ); // Khởi tạo TextStyleTween _textStyleAnimation = TextStyleTween( begin: beginStyle, end: endStyle, ).animate(_controller); // Lắng nghe trạng thái của animation để đảo ngược hoặc lặp lại _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggleAnimation() { if (_controller.isAnimating) { _controller.stop(); } else if (_controller.status == AnimationStatus.dismissed) { _controller.forward(); } else if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); // Hoặc reverse tùy trạng thái hiện tại } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextStyleTween Demo'), backgroundColor: Colors.teal, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Sử dụng AnimatedBuilder để rebuild Widget khi animation thay đổi AnimatedBuilder( animation: _textStyleAnimation, builder: (context, child) { return Text( 'Creyt Code', // Text muốn áp dụng style style: _textStyleAnimation.value, // Lấy giá trị TextStyle hiện tại từ animation ); }, ), const SizedBox(height: 30), ElevatedButton( onPressed: _toggleAnimation, style: ElevatedButton.styleFrom( backgroundColor: Colors.teal, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 12), textStyle: const TextStyle(fontSize: 18), ), child: const Text('Thay đổi Style'), ), ], ), ), ); } } void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TextStyleTween Demo App', theme: ThemeData(primarySwatch: Colors.blue), home: const TextStyleTweenExample(), ); } } Trong ví dụ trên: Chúng ta định nghĩa beginStyle và endStyle với các thuộc tính khác nhau (kích thước, màu sắc, độ đậm, font). TextStyleTween sẽ lo việc nội suy giữa các thuộc tính này. _controller điều khiển tiến trình của animation. _textStyleAnimation là kết quả của TextStyleTween được .animate() bởi _controller. AnimatedBuilder là "người thợ" có nhiệm vụ rebuild Text Widget mỗi khi _textStyleAnimation.value thay đổi, tạo ra hiệu ứng mượt mà. 3. Mẹo (Best Practices) để ghi nhớ và dùng thực tế Hiểu rõ "đường đi nước bước": TextStyleTween là một phần của hệ thống animation lớn hơn của Flutter. Đừng cố gắng "ép" nó làm mọi thứ. Nó chỉ lo phần nội suy TextStyle thôi. Các "anh em" như AnimationController, AnimatedBuilder (hoặc TweenAnimationBuilder) mới là những người điều khiển tổng thể. "Tối ưu hóa vẻ đẹp": Thời gian animation là yếu tố then chốt. Tránh làm hiệu ứng quá nhanh (dưới 200ms) sẽ khiến nó khó nhận ra hoặc giật cục, và cũng đừng quá chậm (trên 1 giây) sẽ gây cảm giác chờ đợi. Khoảng 300-700ms thường là "điểm vàng" cho hầu hết các hiệu ứng chuyển đổi style. "Đừng quên người anh em": TextStyleTween luôn cần một Animation<double> (thường là từ AnimationController) để biết nó đang ở điểm nào trong quá trình chuyển đổi. Và nó luôn cần một AnimatedBuilder hoặc TweenAnimationBuilder để thực sự "vẽ" lại Widget với TextStyle mới. "Style consistency": Khi định nghĩa begin và end TextStyle, hãy đảm bảo các thuộc tính bạn không muốn thay đổi là giống nhau ở cả hai style. Ví dụ, nếu chỉ muốn đổi màu, hãy giữ nguyên fontSize, fontWeight, fontFamily... để tránh những hiệu ứng "lạ" không mong muốn. 4. Ví dụ thực tế các ứng dụng/website đã ứng dụng App đọc báo/đọc sách (ví dụ: Kindle, Google Sách): Khi người dùng thay đổi kích thước font chữ, màu nền, app thường không chỉ "phập" một cái là đổi, mà có hiệu ứng chuyển đổi mượt mà để mắt người dùng không bị "sốc" và dễ dàng theo dõi. TextStyleTween có thể được dùng để làm mượt mà sự thay đổi kích thước và màu sắc của văn bản. Giao diện game (Game UI): Khi có thông báo "Level Up!", "Game Over!", hoặc hiển thị điểm số, chữ thường "bùng nổ" với hiệu ứng màu sắc, kích thước thay đổi linh hoạt để tạo sự kịch tính và hấp dẫn. TextStyleTween giúp các hiệu ứng này trông chuyên nghiệp hơn. Landing Pages hoặc Marketing Apps: Các tiêu đề, nút bấm có hiệu ứng hover đổi màu, đổi kích thước chữ nhẹ nhàng khi người dùng di chuột qua hoặc nhấn vào, tạo cảm giác tương tác "xịn xò" và thu hút. App học ngoại ngữ: Khi bạn chọn một từ vựng, nó có thể "phóng to" hoặc đổi màu để nhấn mạnh, thu hút sự chú ý của người học. 5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào Anh Creyt đã từng "đau đầu" với việc làm cho các hiệu ứng chữ trông tự nhiên và không bị "cứng nhắc". Trước khi có các Tween chuyên biệt, việc tự "chế" hiệu ứng từng thuộc tính một (màu, kích thước, độ đậm...) rất tốn công và dễ lỗi. TextStyleTween ra đời như một "pha cứu thua" ngoạn mục, gói gọn tất cả vào một công cụ duy nhất. Nên dùng TextStyleTween cho các trường hợp sau: Tạo hiệu ứng tương tác: Khi người dùng chạm vào một đoạn văn bản, hoặc di chuột qua một nút, bạn muốn chữ có hiệu ứng "phóng to" hoặc "đổi màu" nhẹ nhàng để phản hồi hành động đó. Hiệu ứng Loading/Trạng thái: Trong quá trình tải dữ liệu, bạn có thể cho một đoạn text "Loading..." nhấp nháy màu hoặc thay đổi kích thước nhẹ nhàng để báo hiệu cho người dùng rằng app vẫn đang hoạt động. Highlight nội dung: Khi bạn muốn làm nổi bật một phần văn bản quan trọng trong một khoảng thời gian nhất định, sau đó trở lại trạng thái bình thường. Chuyển đổi theme: Khi người dùng chuyển đổi giữa theme sáng và tối, màu sắc chữ có thể chuyển đổi mượt mà thay vì "nhảy" đột ngột. Không nên dùng khi: Bạn chỉ cần thay đổi TextStyle một cách đột ngột mà không cần bất kỳ hiệu ứng chuyển tiếp nào (ví dụ: đổi theme tối/sáng mà không cần hiệu ứng chuyển màu cho chữ). Khi hiệu suất là cực kỳ cực kỳ quan trọng và hiệu ứng chuyển đổi không mang lại giá trị trải nghiệm đáng kể (mặc dù TextStyleTween khá nhẹ, nhưng mọi animation đều có chi phí). Khi bạn cần các hiệu ứng phức tạp hơn liên quan đến layout hoặc biến đổi hình học của chữ (xoay, kéo giãn 3D), lúc đó bạn sẽ cần các Widget animation mạnh hơn như Hero, AnimatedContainer kết hợp với Transform hoặc các custom painter. Nhớ nhé các bạn, TextStyleTween không chỉ làm đẹp cho app của bạn mà còn nâng tầm trải nghiệm người dùng lên một đẳng cấp mới. Đừng ngại thử nghiệm và "phù phép" cho những dòng chữ của mình nhé! Anh Creyt tin các bạn sẽ làm được! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

40 Đọc tiếp
TextSpan Flutter: Phù Thủy Biến Hóa Text Cho Gen Z!
22/03/2026

TextSpan Flutter: Phù Thủy Biến Hóa Text Cho Gen Z!

Chào các đồng chí Gen Z, hôm nay chúng ta sẽ cùng Creyt khám phá một "phép thuật" nhỏ nhưng có võ trong Flutter, đó là TextSpan. Nghe cái tên có vẻ hơi "học thuật" nhưng tin thầy đi, nó dễ như ăn kẹo mà lại biến UI của bạn thành "level max" ngay lập tức. 1. TextSpan Là Gì Mà Lại "Hot" Thế? Để dễ hình dung, các bạn cứ tưởng tượng thế này: Bạn có một bức tường trống và muốn trang trí nó. Nếu dùng Text widget thông thường, thì giống như bạn chỉ có một cuộn giấy dán tường to đùng, dán hết cả bức tường một kiểu duy nhất. Chán òm! Nhưng với TextSpan, bạn như có trong tay một bộ sưu tập sticker đủ loại, đủ màu sắc, đủ hình dáng, thậm chí có cả sticker phát sáng hay sticker có thể chạm vào để mở nhạc. Bạn có thể dán mỗi miếng sticker vào một vị trí, tạo nên một tác phẩm nghệ thuật đa dạng, sống động ngay trên bức tường chữ của mình. Nói một cách "coder" hơn, TextSpan không phải là một widget độc lập mà là một thành phần cấu tạo bên trong RichText widget. Nó cho phép bạn định nghĩa các đoạn văn bản (hay "span" - đoạn nhỏ) với các thuộc tính styling (font size, color, weight, v.v.) và hành vi (như onTap - khi chạm vào) khác nhau, tất cả trong cùng một khối văn bản duy nhất. Mục đích là để "mix & match" nhiều style và tương tác trên cùng một dòng chữ. 2. Code Ví Dụ Minh Họa: "Thấy Tận Mắt, Rờ Tận Tay" Giờ thì không nói nhiều nữa, chúng ta cùng xem TextSpan nó "ảo diệu" thế nào qua ví dụ code sau: import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; // Quan trọng để dùng TapGestureRecognizer void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TextSpan Demo của thầy Creyt', theme: ThemeData( primarySwatch: Colors.blue, ), home: const TextSpanScreen(), ); } } class TextSpanScreen extends StatelessWidget { const TextSpanScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSpan của thầy Creyt'), ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ RichText( text: TextSpan( text: 'Chào bạn, đây là một ', style: const TextStyle( fontSize: 18, color: Colors.black87, fontFamily: 'Roboto', ), children: <TextSpan>[ TextSpan( text: 'ví dụ siêu cool ', style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.deepPurple, ), ), TextSpan( text: 'về ', style: const TextStyle( fontStyle: FontStyle.italic, color: Colors.grey, ), ), TextSpan( text: 'TextSpan ', style: const TextStyle( color: Colors.red, fontSize: 20, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() ..onTap = () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Bạn vừa chạm vào TextSpan đó nha!')), ); }, ), TextSpan( text: 'trong Flutter. ', ), TextSpan( text: 'Click vào đây ', style: const TextStyle( color: Colors.blue, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() ..onTap = () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Mở link gì đó đi bạn!')), ); // Trong ứng dụng thực tế, bạn có thể dùng url_launcher để mở URL: // launchUrl(Uri.parse('https://creyt.dev')); }, ), TextSpan( text: 'để xem điều bất ngờ!', ), ], ), ), const SizedBox(height: 30), // Một ví dụ khác với hashtag và @mention RichText( text: TextSpan( text: 'Đừng quên theo dõi ', style: const TextStyle( fontSize: 16, color: Colors.black54, ), children: <TextSpan>[ TextSpan( text: '#CreytDev ', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.teal, ), recognizer: TapGestureRecognizer() ..onTap = () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tìm Creyt trên mạng xã hội!')), ); }, ), TextSpan( text: 'và ', ), TextSpan( text: '@FlutterGenz ', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.orangeAccent, ), recognizer: TapGestureRecognizer() ..onTap = () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Khám phá thêm về Flutter cho Gen Z!')), ); }, ), TextSpan( text: 'nhé!', ), ], ), ), ], ), ), ), ); } } Giải thích code: Chúng ta dùng RichText widget làm "container" chính. Nó nhận một TextSpan làm thuộc tính text. TextSpan gốc (root TextSpan) sẽ định nghĩa text và style mặc định cho toàn bộ khối văn bản. Bên trong children của TextSpan gốc, chúng ta có thể thêm nhiều TextSpan con khác. Mỗi TextSpan con này có thể có text và style riêng, ghi đè lên style của cha nếu được định nghĩa. Điểm đặc biệt là thuộc tính recognizer. Ở đây, chúng ta dùng TapGestureRecognizer để bắt sự kiện chạm (tap) vào một phần văn bản cụ thể. Khi chạm, nó sẽ gọi hàm onTap và bạn có thể thực hiện bất kỳ hành động nào, như hiển thị SnackBar hay mở một URL. 3. Mẹo Hay & Best Practices Từ Creyt Đừng lạm dụng: Nếu bạn chỉ cần một đoạn văn bản với một kiểu chữ duy nhất, hãy dùng Text('Hello', style: TextStyle(...)) cho gọn gàng và hiệu quả hơn. RichText với TextSpan có một chút "overhead" (chi phí xử lý) nhỏ hơn Text đơn giản. Quản lý recognizer cẩn thận: Khi dùng TapGestureRecognizer (hoặc các GestureRecognizer khác), nếu widget của bạn là StatefulWidget, bạn nên khởi tạo recognizer trong initState và nhớ dispose nó trong dispose để tránh memory leak. Trong StatelessWidget như ví dụ trên, Flutter sẽ tự quản lý khá tốt cho các trường hợp đơn giản, nhưng với các logic phức tạp hơn, cân nhắc StatefulWidget và dispose thủ công. Kế thừa Style (Inherited styles): TextSpan có thuộc tính style. Nếu bạn không định nghĩa style cho một TextSpan con, nó sẽ tự động kế thừa style từ TextSpan cha gần nhất. Tận dụng điều này để giảm thiểu việc lặp lại code style. Accessibility (Khả năng tiếp cận): Đảm bảo các phần text có thể tương tác (như link, button text) có đủ độ tương phản màu sắc và được các công cụ hỗ trợ đọc màn hình (screen reader) nhận diện đúng. Điều này giúp ứng dụng của bạn thân thiện hơn với mọi người dùng. 4. Ứng Dụng Thực Tế: "TextSpan Đã Có Mặt Ở Đâu?" Bạn có thể bất ngờ khi biết TextSpan (hoặc các kỹ thuật tương tự ở các nền tảng khác) xuất hiện ở khắp mọi nơi: Mạng xã hội (Facebook, Twitter, Instagram): Khi bạn thấy một bài đăng có @mention người khác, #hashtag, hoặc link web được highlight và có thể click được, đó chính là một ứng dụng kinh điển của việc định dạng văn bản giàu có. Ứng dụng Chat (Zalo, Messenger): Tin nhắn có link, số điện thoại, email được tự động nhận diện và biến thành clickable text. Điều khoản sử dụng/Chính sách bảo mật: Các văn bản dài thường có những đoạn từ khóa quan trọng hoặc link đến các chính sách con được định dạng khác biệt và có thể click. Ứng dụng đọc tin tức/blog: Tiêu đề, trích dẫn, hoặc các đoạn text đặc biệt trong bài viết được định dạng riêng để thu hút sự chú ý. 5. Thử Nghiệm & Nên Dùng Cho Case Nào? Nên dùng TextSpan khi: Bạn cần một đoạn văn bản duy nhất nhưng lại muốn mỗi phần của nó có một style riêng biệt (màu sắc, kích thước, font, in đậm, gạch chân, v.v.). Đây là "sân nhà" của nó. Bạn muốn một phần của văn bản có thể tương tác được (click để mở link, hiển thị tooltip, v.v.) mà không cần phải tách thành các widget riêng biệt. Bạn đang xây dựng các tính năng như tự động highlight từ khóa tìm kiếm, hiển thị các tag, hoặc tạo các rich text editor cơ bản. Không nên dùng TextSpan khi: Bạn chỉ cần một đoạn văn bản với một style duy nhất. Dùng Text('Hello', style: TextStyle(...)) là đủ và hiệu quả hơn rất nhiều. Khi bạn cần các khối văn bản độc lập hoàn toàn và muốn kiểm soát layout của chúng bằng các widget như Row, Column. TextSpan chỉ làm việc bên trong một khối văn bản duy nhất, không phải để sắp xếp các khối text riêng lẻ. 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
TextSelectionThemeData: Phù phép màu sắc cho vùng chọn văn bản Flutter
22/03/2026

TextSelectionThemeData: Phù phép màu sắc cho vùng chọn văn bản Flutter

Chào các "coder hệ Z"! Anh Creyt lại lên sóng đây. Hôm nay, chúng ta sẽ "soi" một thằng bé mà nhiều khi mấy đứa bỏ qua, nhưng nó lại cực kỳ quan trọng để app của mình không chỉ "chạy" mà còn phải "chất", phải "đỉnh của chóp" – đó chính là TextSelectionThemeData trong Flutter. 1. TextSelectionThemeData là "thằng nào" và nó "làm gì"? Nghe cái tên TextSelectionThemeData chắc mấy đứa thấy hơi "academic" đúng không? Đừng lo, anh Creyt sẽ "dịch" cho dễ hiểu. Tưởng tượng thế này: khi mấy đứa dùng điện thoại hoặc máy tính, mỗi khi chọn một đoạn văn bản nào đó (ví dụ, để copy cái caption thả thính), nó sẽ hiện lên một cái "vùng sáng" (highlight) và hai cái "chấm tròn" (selection handles) để mấy đứa kéo chọn, cùng với cái "vạch nhấp nháy" (cursor) khi gõ chữ. TextSelectionThemeData chính là "nghệ nhân" đứng sau để "trang điểm" cho mấy cái thành phần đó. Nó cho phép mấy đứa tùy chỉnh màu sắc của: selectionColor: Cái vùng highlight màu mè khi mấy đứa kéo chọn chữ. cursorColor: Cái vạch nhấp nháy "lấp la lấp lánh" khi con trỏ đang chờ nhập liệu. selectionHandleColor: Hai cái "tay cầm" bé xinh hình tròn hay hình giọt nước mà mấy đứa dùng để mở rộng hoặc thu hẹp vùng chọn. Nói cách khác, nó là "bộ kit trang điểm" cho trải nghiệm tương tác với văn bản. Thay vì để Flutter dùng màu mặc định "nhạt nhẽo" như "trà sữa không đường", mấy đứa có thể biến nó thành "ly trà sữa full topping" đúng chuẩn branding của app mình. Mục tiêu là gì? Để app của mấy đứa không chỉ "ngon" về chức năng mà còn "đã mắt" về thị giác, tạo ra một trải nghiệm "smooth như kem" cho người dùng. 2. Code Ví Dụ Minh Họa: "Thực hành ngay và luôn!" Giờ thì "xắn tay áo" vào code thôi! Anh sẽ hướng dẫn mấy đứa cách áp dụng TextSelectionThemeData một cách toàn cục (cho cả app) và cục bộ (cho một widget cụ thể). Cách 1: Áp dụng toàn cục với ThemeData (phương pháp "một phát ăn ngay") Đây là cách "nhanh gọn lẹ" nhất để đồng bộ màu sắc cho toàn bộ app của mấy đứa. Mấy đứa sẽ "nhét" nó vào trong ThemeData của MaterialApp. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Creyt\'s Text Selection Demo', theme: ThemeData( // Đây rồi, "ngôi sao" của chúng ta! textSelectionTheme: TextSelectionThemeData( selectionColor: Colors.purple.withOpacity(0.3), // Màu highlight "tím mộng mơ" cursorColor: Colors.deepPurpleAccent, // Con trỏ "tím biếc" selectionHandleColor: Colors.deepPurple, // Tay kéo "tím lịm tìm sim" ), 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('Text Selection Theme Demo'), ), body: Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Thử chọn đoạn văn bản này xem! Màu sắc đã khác rồi đó nha.', style: TextStyle(fontSize: 20), textAlign: TextAlign.center, ), const SizedBox(height: 30), TextField( decoration: const InputDecoration( labelText: 'Nhập gì đó vào đây nè', border: OutlineInputBorder(), ), // TextField sẽ tự động kế thừa theme từ MaterialApp ), const SizedBox(height: 30), TextFormField( maxLines: 3, decoration: const InputDecoration( labelText: 'Vùng nhập liệu nhiều dòng', border: OutlineInputBorder(), ), ), ], ), ), ), ); } } Cách 2: Áp dụng cục bộ với TextSelectionTheme (phương pháp "đặc trị") Đôi khi, mấy đứa chỉ muốn một vùng chọn văn bản cụ thể có màu sắc "độc lạ" mà không ảnh hưởng đến phần còn lại của app. Lúc này, TextSelectionTheme widget sẽ là "cứu tinh". import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Creyt\'s Local Text Selection Demo', theme: ThemeData( // Theme mặc định cho cả app (có thể khác) primarySwatch: Colors.green, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local Text Selection Theme Demo'), ), body: Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Đoạn văn bản này sẽ dùng theme mặc định của app.', style: TextStyle(fontSize: 18), textAlign: TextAlign.center, ), const SizedBox(height: 30), // Áp dụng theme riêng cho TextField này TextSelectionTheme( data: TextSelectionThemeData( selectionColor: Colors.orange.withOpacity(0.4), // Màu cam "nổi bần bật" cursorColor: Colors.red, // Con trỏ "đỏ rực" selectionHandleColor: Colors.deepOrange, // Tay kéo "cam cháy" ), child: TextField( decoration: const InputDecoration( labelText: 'Nhập gì đó vào đây (theme cam)', border: OutlineInputBorder(), ), ), ), const SizedBox(height: 30), TextField( decoration: const InputDecoration( labelText: 'TextField này vẫn dùng theme mặc định.', border: OutlineInputBorder(), ), ), ], ), ), ), ); } } 3. Mẹo Vặt (Best Practices) từ "lão làng" Creyt Đồng bộ là "chân ái": Luôn cố gắng giữ màu sắc vùng chọn khớp với bảng màu (color scheme) tổng thể của app. Đừng để nó "lạc quẻ" như "nốt trầm giữa bản nhạc rap". Dùng Theme.of(context).colorScheme.primary hoặc secondary để đảm bảo tính nhất quán. Độ tương phản (Contrast): Mấy đứa chọn màu highlight phải đảm bảo chữ vẫn đọc được rõ ràng. Màu highlight quá sáng trên nền chữ sáng, hoặc quá tối trên nền chữ tối là "điểm trừ cực mạnh" đó nha. Hãy nghĩ về khả năng tiếp cận (accessibility) cho người dùng có thị lực kém. Trải nghiệm người dùng (UX): Màu sắc vùng chọn nên "mềm mại", không gây chói mắt. Mục đích là để dẫn dắt sự chú ý, không phải để "hù dọa" người dùng. Dark Mode/Light Mode: Đừng quên tùy chỉnh TextSelectionThemeData cho cả hai chế độ sáng/tối. Một màu đẹp ở light mode có thể "thảm họa" ở dark mode và ngược lại. 4. Ứng dụng thực tế: Ai đã dùng? Dùng như thế nào? Thực ra, mấy đứa đang dùng nó mỗi ngày mà không biết đó thôi! Hầu hết các ứng dụng "xịn sò" đều tùy chỉnh cái này để tạo nên "chất riêng": Các ứng dụng nhắn tin (WhatsApp, Telegram): Mỗi ứng dụng có một tông màu chủ đạo riêng. Khi mấy đứa chọn tin nhắn hay nhập liệu, vùng highlight và con trỏ sẽ theo màu brand của họ, tạo cảm giác "thuộc về" ứng dụng đó. Ứng dụng ghi chú (Notion, Evernote): Họ thường cho phép người dùng tùy chỉnh theme, và tất nhiên, màu sắc vùng chọn cũng thay đổi theo để đồng bộ với theme đó. Các trình duyệt web (Chrome, Safari): Mấy đứa để ý xem, khi chọn văn bản trên một website, màu highlight có thể thay đổi tùy thuộc vào CSS của trang web đó. Trong Flutter, TextSelectionThemeData làm công việc tương tự. Ứng dụng đọc sách (Kindle, Google Books): Khi mấy đứa highlight để đánh dấu một đoạn sách, màu highlight đó cũng được tùy chỉnh để dễ đọc và phù hợp với giao diện đọc sách. 5. Thử nghiệm và Nên dùng cho Case nào? Nên dùng khi: Xây dựng Brand Identity: Đây là cách đơn giản nhưng hiệu quả để "khắc dấu" thương hiệu của mấy đứa vào từng ngóc ngách của app. Cải thiện UX/UI: Một app có giao diện đẹp, đồng bộ sẽ tạo ấn tượng tốt hơn rất nhiều, giúp người dùng cảm thấy "dễ chịu" khi tương tác. Hỗ trợ Dark Mode/Light Mode: Khi app của mấy đứa có nhiều theme, TextSelectionThemeData là công cụ không thể thiếu để đảm bảo mọi thứ đều "nuột nà" ở mọi chế độ. Tạo điểm nhấn đặc biệt: Trong một số trường hợp, mấy đứa muốn một TextField nào đó có màu sắc khác biệt để thu hút sự chú ý (ví dụ: trường nhập mã khuyến mãi). Không nên dùng khi: Lạm dụng màu sắc: Đừng biến app của mình thành "cầu vồng 7 sắc" mỗi khi chọn văn bản. Sự đơn giản và tinh tế luôn là chìa khóa. Phá vỡ tính nhất quán: Trừ khi có lý do rất rõ ràng, đừng đặt những màu sắc "chỏi" nhau hoặc khác biệt hoàn toàn với theme chung của app. Nó sẽ làm người dùng cảm thấy "khó hiểu" và "rối mắt". Nhớ nhé, TextSelectionThemeData không chỉ là một "công cụ" mà còn là một "ngôn ngữ" để app của mấy đứa "nói" lên cá tính. Đừng bỏ qua nó, hãy "chơi đùa" với nó để tạo ra những trải nghiệm "đỉnh cao" cho người dùng! Hẹn gặp lại trong bài học tiếp theo của anh Creyt! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

45 Đọc tiếp
TextSelectionTheme: 'Độ' app Flutter chất lừ từ chi tiết nhỏ nhất!
22/03/2026

TextSelectionTheme: 'Độ' app Flutter chất lừ từ chi tiết nhỏ nhất!

Các em cứ hình dung thế này, khi mình lướt TikTok, đọc tin nhắn hay soạn email, đôi lúc mình muốn bôi đen một đoạn chữ để copy, cắt, hay đơn giản là để "đánh dấu" đoạn đó quan trọng. Cái thao tác bôi đen ấy, chính là "text selection" đó. Và cái màu sắc của đoạn chữ được bôi đen, cái "cờ lê" nhỏ nhỏ để kéo giãn vùng chọn, đó chính là "giao diện" của text selection. 1. TextSelectionTheme là gì? Để làm gì? (Theo phong cách GenZ và Creyt) TextSelectionTheme trong Flutter, nói một cách dễ hiểu, nó giống như "bộ trang phục" cho cái thao tác bôi đen chữ của em vậy. Mặc định, Flutter nó có một bộ đồ "công sở" màu xanh lam hoặc màu xám xám trông cũng được, nhưng đôi khi nó không hợp với "gu" thời trang tổng thể của app mình. Ví dụ, app em tông màu tím mộng mơ, mà bôi đen ra màu xanh chuối thì "out trình" quá đúng không? TextSelectionTheme sinh ra là để em có thể "tút tát" lại cái màu sắc của vùng chọn văn bản, màu của các "tay cầm" (selection handles) và con trỏ (cursor) sao cho nó "tone sur tone" với cái theme app của em. Nó giúp app của em trông chuyên nghiệp hơn, "đã mắt" hơn và "ăn nhập" hơn trong mọi ngóc ngách. Nó giống như việc em đi mua đồ vậy. Thay vì mặc định hãng nào cũng chỉ có một màu áo sơ mi trắng, thì nay em có thể chọn màu hồng pastel, xanh mint, hay đen huyền bí tùy thích. TextSelectionTheme chính là cái "bảng màu" để em "design" lại trải nghiệm bôi đen của người dùng, biến một chi tiết nhỏ nhặt thành một điểm nhấn "có gu". 2. Code Ví Dụ Minh Hoạ Rõ Ràng, Chuẩn Kiến Thức Nói suông thì ai cũng nói được, giờ anh em mình cùng "xắn tay áo" vào code một phát cho "nó má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: 'TextSelectionTheme Demo', theme: ThemeData( primarySwatch: Colors.deepPurple, // Example primary color // Global text selection theme for the entire app textSelectionTheme: const TextSelectionThemeData( selectionColor: Colors.deepPurpleAccent, // Màu vùng chọn cursorColor: Colors.purple, // Màu con trỏ selectionHandleColor: Colors.purpleAccent, // Màu tay cầm kéo ), ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelectionTheme by Creyt'), backgroundColor: Theme.of(context).primaryColor, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ const Text( 'Thử bôi đen đoạn văn bản này xem có gì "khác bọt" không nhé, các đệ tử!', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), TextField( controller: _controller, decoration: const InputDecoration( labelText: 'Nhập gì đó vào đây để thử bôi đen...', border: OutlineInputBorder(), ), maxLines: 5, ), const SizedBox(height: 20), // You can also override the theme locally if needed TextSelectionTheme( data: const TextSelectionThemeData( selectionColor: Colors.redAccent, // Override locally cursorColor: Colors.red, selectionHandleColor: Colors.orange, ), child: const SelectableText( 'Đây là một đoạn văn bản có thể chọn được. Hãy thử bôi đen đoạn này để thấy sự khác biệt của TextSelectionTheme cục bộ nhé!', style: TextStyle(fontSize: 16), ), ), ], ), ), ); } } 3. Giải thích Code "Tận Răng": Trong ví dụ trên, anh em mình có thể thấy rõ ràng cách TextSelectionTheme hoạt động. TextSelectionThemeData: "Bảng điều khiển" màu sắc Đây chính là "bảng điều khiển" để em tùy chỉnh các màu sắc. Nó có các thuộc tính chính: selectionColor: Màu của cái vùng chữ được bôi đen. Giống như màu của bút highlight của em vậy. cursorColor: Màu của con trỏ nhấp nháy khi em gõ chữ. Cái "chấm nhỏ" báo hiệu em đang gõ ở đâu đó. selectionHandleColor: Màu của hai cái "tay cầm" nhỏ xíu ở hai đầu vùng chọn, dùng để kéo giãn vùng chọn. Hai cái "cờ lê" thần thánh để em "căn chỉnh" vùng bôi đen đó. Đặt TextSelectionTheme ở đâu? Cách "chuẩn bài" nhất là đặt nó trong ThemeData của MaterialApp (hoặc CupertinoApp). Khi đó, toàn bộ các TextField, TextFormField, SelectableText trong app của em sẽ "thừa hưởng" cái theme này. Đây là cách để "đồng bộ" trải nghiệm người dùng toàn cục. Tuy nhiên, nếu em muốn "phá cách" một chút, chỉ một vài chỗ đặc biệt có màu bôi đen khác biệt, em có thể dùng widget TextSelectionTheme để bọc riêng từng widget cụ thể. Như trong ví dụ, anh đã bọc SelectableText với một TextSelectionTheme khác để các em thấy rõ sự "linh hoạt" của nó. 4. Mẹo (Best Practices) Để Ghi Nhớ Hoặc Dùng Thực Tế: "Tone sur tone" là chân ái: Luôn cố gắng chọn màu selectionColor, cursorColor, selectionHandleColor sao cho nó hợp với primaryColor hoặc accentColor của app em. Đừng để nó "lạc quẻ" như mặc áo vest với quần đùi đi họp nhé. Sự nhất quán tạo nên vẻ đẹp chuyên nghiệp. Đừng quá "lòe loẹt": Màu sắc nổi bật là tốt, nhưng đừng quá chói mắt hay quá tương phản khiến người dùng khó chịu. Mục đích là để dễ nhìn, chứ không phải để "dọa" người dùng. Kiểm tra trên các nền tảng (Android/iOS): Đôi khi, các màu này có thể hiển thị hơi khác một chút giữa Android và iOS. Hãy test kỹ để đảm bảo trải nghiệm đồng nhất hoặc có điều chỉnh phù hợp. Sử dụng Theme.of(context): Khi cần truy cập các màu đã định nghĩa trong theme, hãy dùng Theme.of(context).textSelectionTheme.selectionColor để đảm bảo em luôn lấy được màu sắc chính xác theo theme hiện tại, kể cả khi theme đó được override cục bộ. 5. Ví Dụ Thực Tế Các Ứng Dụng/Website Đã Ứng Dụng (hoặc tương tự): Các em có thấy khi dùng ứng dụng Zalo, Messenger, hay thậm chí là Google Docs trên điện thoại không? Khi em bôi đen một đoạn chat hay một đoạn văn bản, cái màu highlight nó sẽ không phải là màu xanh mặc định của hệ điều hành đâu. Nó thường sẽ là màu xanh lá của Zalo, màu xanh dương của Messenger, hay màu xanh của Google Drive/Docs. Đó chính là cách họ "cá nhân hóa" trải nghiệm người dùng, làm cho app của họ có "bản sắc" riêng, kể cả từ những chi tiết nhỏ nhất như Text Selection. Các trình duyệt web hiện đại cũng cho phép các nhà phát triển CSS tùy chỉnh ::selection để thay đổi màu bôi đen trên website. Về bản chất, nó cũng là một cách để đồng bộ UI/UX đấy. 6. Thử Nghiệm Đã Từng Và Hướng Dẫn Nên Dùng Cho Case Nào: Creyt đã từng "vật lộn" thế nào: Ngày xưa, khi Flutter mới ra, anh Creyt cũng từng đau đầu với mấy cái màu mặc định này lắm. App làm tông đen-vàng mà bôi đen ra cái màu xanh lè của Android, nhìn nó "phèn" hết sức. Sau này mới biết đến TextSelectionTheme, đúng là "chân ái" cứu rỗi cuộc đời dev của anh em mình. Nên dùng cho case nào? App có branding riêng: Đây là lúc em cần TextSelectionTheme nhất. Khi app em có một bộ nhận diện thương hiệu rõ ràng (màu sắc, font chữ), thì mọi chi tiết nhỏ nhất cũng nên "thở" ra cái branding đó. App có Dark Mode/Light Mode: Cực kỳ quan trọng! Màu bôi đen mặc định có thể ổn với Light Mode, nhưng khi chuyển sang Dark Mode, nó có thể trở nên khó nhìn hoặc chói mắt. Em cần tùy chỉnh TextSelectionTheme riêng cho từng chế độ để đảm bảo trải nghiệm tốt nhất. Cải thiện khả năng đọc: Trong một số trường hợp, màu bôi đen mặc định có thể không đủ tương phản với màu nền của text. Việc tùy chỉnh giúp tăng cường khả năng đọc, đặc biệt với người dùng có thị lực kém. Tạo điểm nhấn đặc biệt: Dù ít gặp hơn, nhưng đôi khi em muốn một vùng văn bản nào đó có màu bôi đen đặc biệt để thu hút sự chú ý. Lúc này, dùng TextSelectionTheme cục bộ là một ý hay. Đấy, các em thấy không? Một chi tiết nhỏ như TextSelectionTheme thôi, nhưng nếu biết cách dùng, nó có thể nâng tầm app của em lên một đẳng cấp khác, từ một app "tầm thường" thành một sản phẩm "có gu", "có tâm". Hãy nhớ, sự tinh tế nằm ở những chi tiết nhỏ nhất! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

42 Đọc tiếp
Điểm Neo Chữ: Giải Mã TextSelectionPoint trong Flutter
22/03/2026

Điểm Neo Chữ: Giải Mã TextSelectionPoint trong Flutter

Chào các "dev-er" tương lai, hôm nay anh Creyt sẽ "bung lụa" một khái niệm nghe thì "hack não" nhưng thực ra lại "easy game" nếu các em chịu khó nghe anh "chém gió" tí. Từ khóa hôm nay là TextSelectionPoint – cái thứ mà nghe thôi đã thấy "căng đét" rồi đúng không? Đừng lo, anh sẽ biến nó thành câu chuyện "quẹt" chữ mà ai cũng hiểu. 1. TextSelectionPoint là gì? (Kiểu Gen Z) Nói một cách "ngôn tình" Gen Z, TextSelectionPoint chính là "cái điểm neo" của mỗi cú "quẹt" chọn chữ của các em trên màn hình. Tưởng tượng các em đang dùng ngón tay "vuốt" để chọn một đoạn text. Cái điểm mà ngón tay các em bắt đầu và kết thúc chính là hai TextSelectionPoint đó. Nhưng mà, nó không chỉ đơn thuần là một vị trí (index) đâu nha. Nó còn có một "cái thần thái" riêng, gọi là TextAffinity. Cứ như bạn đứng ở vạch đích, nhưng có thể là "mép trong" hay "mép ngoài" vạch vậy. Cụ thể: index: Đây là vị trí của ký tự trong chuỗi văn bản. Kiểu như bạn đang đứng ở ký tự thứ 5, thứ 10 gì đó. affinity: Cái này mới "ảo diệu" nè. Nó cho biết "điểm neo" của bạn đang "hút" về phía nào của ký tự đó. Có hai loại: TextAffinity.upstream: Nghĩa là điểm đó đang "ngả" về phía trước ký tự (bên trái nếu là văn bản LTR). Cứ như nó đang "nương tựa" vào ký tự đằng trước vậy. TextAffinity.downstream: Nghĩa là điểm đó đang "ngả" về phía sau ký tự (bên phải nếu là văn bản LTR). Nó đang "ôm ấp" ký tự hiện tại. Hiểu đơn giản, TextSelectionPoint là một cặp bài trùng (index, affinity) giúp Flutter biết chính xác "cái ranh giới" của vùng chọn chữ của bạn là ở đâu, không lệch đi đâu một ly. 2. Để làm gì? (Why it matters) "Anh Creyt ơi, em dùng Text với TextField mặc định vẫn chọn chữ ầm ầm mà có thấy dùng cái này đâu?" – Đúng rồi, vì Flutter nó "tự động hóa" cho mình rồi. Nhưng nếu các em muốn "flex" trình độ, muốn "custom" một cái widget hiển thị văn bản riêng, hoặc muốn tạo ra những hiệu ứng chọn chữ "độc lạ Bình Dương" thì TextSelectionPoint chính là "chìa khóa vàng" đó. Nó giúp các em: Kiểm soát chính xác vùng chọn: Không chỉ biết bắt đầu ở ký tự nào, mà còn biết "mép nào" của ký tự đó. Tạo widget text "đỉnh của chóp": Khi xây dựng các trình soạn thảo code, trình chỉnh sửa rich text, hay bất kỳ widget nào cần tương tác sâu với text selection, các em sẽ cần đến nó để vẽ vùng chọn, xử lý copy/paste, v.v. Xử lý các trường hợp "khó nhằn": Ví dụ, khi chọn chữ ở cuối dòng, đầu dòng, hoặc khi có các ký tự đặc biệt, affinity sẽ giúp phân biệt rõ ràng. 3. Code Ví Dụ Minh Họa Rõ Ràng Để các em dễ hình dung, anh Creyt sẽ làm một ví dụ đơn giản. Chúng ta sẽ tạo một đoạn văn bản và giả lập một vùng chọn, sau đó "soi" xem các TextSelectionPoint của vùng chọn đó trông như thế nào nhé. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TextSelectionPoint Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const TextSelectionPointScreen(), ); } } class TextSelectionPointScreen extends StatefulWidget { const TextSelectionPointScreen({super.key}); @override State<TextSelectionPointScreen> createState() => _TextSelectionPointScreenState(); } class _TextSelectionPointScreenState extends State<TextSelectionPointScreen> { final String _text = "Hôm nay anh Creyt dạy TextSelectionPoint rất dễ hiểu."; TextSelection? _currentSelection; String _selectionInfo = "Chưa có vùng chọn nào."; @override void initState() { super.initState(); // Giả lập một vùng chọn ban đầu _currentSelection = const TextSelection( baseOffset: 12, // Bắt đầu từ chữ 'Creyt' extentOffset: 25, // Kết thúc ở chữ 'TextSelectionPoint' affinity: TextAffinity.downstream, isDirectional: true, ); _updateSelectionInfo(); } void _updateSelectionInfo() { if (_currentSelection == null) { setState(() { _selectionInfo = "Chưa có vùng chọn nào."; }); return; } // Lấy TextSelectionPoint từ TextSelection final TextSelectionPoint basePoint = _currentSelection!.base; final TextSelectionPoint extentPoint = _currentSelection!.extent; // In thông tin chi tiết của từng điểm neo setState(() { _selectionInfo = ''' Vùng chọn hiện tại: "${_currentSelection!.textInside(_text)}" Điểm BẮT ĐẦU (Base Point): - Index: ${basePoint.index} (Ký tự: '${_text[basePoint.index]}') - Affinity: ${basePoint.affinity} Điểm KẾT THÚC (Extent Point): - Index: ${extentPoint.index} (Ký tự: '${_text[extentPoint.index - 1]}') - Affinity: ${extentPoint.affinity} Giải thích: Base Point index ${basePoint.index} là vị trí của chữ '${_text[basePoint.index]}'. Extent Point index ${extentPoint.index} là vị trí SAU chữ '${_text[extentPoint.index - 1]}' (vì extentOffset thường trỏ đến vị trí *sau* ký tự cuối cùng được chọn). '''; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelectionPoint trong Flutter'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Đoạn văn bản gốc:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), const SizedBox(height: 8), SelectableText( _text, style: const TextStyle(fontSize: 16), // Khi người dùng tự chọn, chúng ta cập nhật _currentSelection onSelectionChanged: (selection, cause) { setState(() { _currentSelection = selection; _updateSelectionInfo(); }); }, ), const Divider(height: 32), const Text( 'Thông tin chi tiết về TextSelectionPoint:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), color: Colors.grey[200], width: double.infinity, child: Text( _selectionInfo, style: const TextStyle(fontFamily: 'monospace', fontSize: 14), ), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() { _currentSelection = const TextSelection( baseOffset: 0, extentOffset: 5, // Chọn chữ "Hôm n" affinity: TextAffinity.downstream, isDirectional: true, ); _updateSelectionInfo(); }); }, child: const Text('Chọn "Hôm n"'), ), ElevatedButton( onPressed: () { setState(() { _currentSelection = const TextSelection( baseOffset: 30, extentOffset: 34, // Chọn chữ "rất" affinity: TextAffinity.upstream, isDirectional: true, ); _updateSelectionInfo(); }); }, child: const Text('Chọn "rất" (affinity upstream)'), ), ], ), ), ); } } Trong ví dụ này, anh Creyt dùng SelectableText để các em có thể tự "quẹt" chọn và thấy thông tin thay đổi. Ngoài ra, anh còn giả lập các vùng chọn cố định để các em thấy rõ cách basePoint và extentPoint (là các TextSelectionPoint) được trích xuất và hiển thị thông tin index và affinity của chúng. Lưu ý: baseOffset và extentOffset trong TextSelection cũng chính là index của base và extent TextSelectionPoint đó. base là điểm bắt đầu vùng chọn, extent là điểm kết thúc. Khi baseOffset < extentOffset thì base là điểm đầu tiên theo thứ tự văn bản, extent là điểm cuối cùng. Ngược lại, nếu bạn chọn từ phải sang trái, extent có thể có index nhỏ hơn base. 4. Mẹo Nhỏ từ Creyt (Best Practices) "Thần thái" của affinity là quan trọng: Đừng bao giờ quên TextAffinity. Nó giúp phân biệt các trường hợp "râu ria" như chọn giữa hai ký tự, hoặc chọn ở vị trí xuống dòng. Cứ nghĩ nó là "nam châm" hút điểm neo về phía nào của ký tự đó. Đừng tự "phát minh lại bánh xe": Nếu Text và TextField của Flutter đã đáp ứng đủ nhu cầu, cứ dùng chúng. Chỉ khi nào các em cần "custom" sâu, "chơi trội" thì mới cần đụng đến TextSelectionPoint. Test "tới bến" các trường hợp biên: Văn bản rỗng, văn bản chỉ có một ký tự, văn bản nhiều dòng, văn bản có ký tự đặc biệt (emoji, ký tự unicode phức tạp). Đây là những "chiêu" giúp các em "lên trình" debug và hiểu sâu hơn. Debug bằng cách print: Khi "bí", cứ print index và affinity của các TextSelectionPoint ra console. Nó sẽ "mách nước" cho các em rất nhiều đó. 5. Ứng Dụng Thực Tế (App Examples) Cứ tưởng tượng bất kỳ ứng dụng nào mà các em thấy người ta "quẹt quẹt" chọn chữ "xịn sò" thì chắc chắn có bóng dáng của TextSelectionPoint (hoặc các khái niệm tương tự trong các framework khác) ở đó: Trình soạn thảo code (VS Code, Android Studio, Xcode): Các em có thể chọn từng dòng, từng khối code, highlight syntax. Để làm được điều đó, họ phải biết chính xác từng điểm bắt đầu và kết thúc của vùng chọn. Ứng dụng ghi chú, soạn thảo văn bản (Notion, Google Docs, Medium): Các app này cho phép chọn text, bôi đậm, in nghiêng, highlight màu mè. Tất cả đều dựa trên việc xác định chính xác vùng text được chọn. Các app đọc sách, báo: Chức năng highlight một đoạn văn để ghi chú, chia sẻ cũng là một ví dụ điển hình. Ứng dụng dịch thuật: Khi bạn chọn một đoạn text để dịch, ứng dụng cần biết chính xác đoạn nào cần dịch. 6. Thử Nghiệm & Nên Dùng Khi Nào Khi nào nên "triển" TextSelectionPoint? Xây dựng widget text "cây nhà lá vườn": Nếu bạn muốn tự tay tạo một widget hiển thị văn bản từ đầu (thường là dùng CustomPainter hoặc các widget cấp thấp hơn) và cần hỗ trợ chọn chữ. Tạo hiệu ứng chọn chữ "độc lạ": Ví dụ, bạn muốn khi người dùng chọn một từ, nó tự động chọn cả câu chứa từ đó, hoặc muốn có hiệu ứng animation khi chọn. Tạo trình soạn thảo "rich text" phức tạp: Cần kiểm soát từng milimet của vùng chọn để áp dụng định dạng, chèn đối tượng, v.v. Xử lý văn bản đa hướng (Bi-directional text): Trong các ngôn ngữ như Ả Rập, Hebrew, hướng chữ có thể thay đổi. TextAffinity trở nên cực kỳ quan trọng để xác định đúng ranh giới vùng chọn. Thử nghiệm "sương sương" tại nhà: Anh Creyt thách các em thử tạo một TextSelection với cùng một index nhưng thay đổi affinity (ví dụ: baseOffset: 5, affinity: TextAffinity.upstream và baseOffset: 5, affinity: TextAffinity.downstream) và quan sát xem vùng chọn của nó có "xê dịch" tí nào không, đặc biệt là ở các vị trí có ký tự xuống dòng hoặc khoảng trắng. Các em sẽ thấy sự khác biệt "nhẹ nhàng" nhưng lại rất quan trọng đó. Tóm lại, TextSelectionPoint là một "mảnh ghép" quan trọng trong việc làm chủ text selection trong Flutter, đặc biệt khi các em muốn "nâng tầm" ứng dụng của mình lên một đẳng cấp khác. Cứ "nghịch" nhiều vào, rồi các em sẽ thấy nó "dễ như ăn kẹo" thôi! Chúc các em code vui vẻ! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

36 Đọc tiếp
TextSelectionOverlay: Nâng cấp trải nghiệm chọn văn bản trong Flutter
22/03/2026

TextSelectionOverlay: Nâng cấp trải nghiệm chọn văn bản trong Flutter

Này mấy đứa, hôm nay mình cùng nhau “mổ xẻ” một cái thứ mà nhìn thì nhỏ xíu, nhưng lại có võ cực kỳ trong việc “nâng tầm” trải nghiệm người dùng trong app Flutter của mình. Đó chính là TextSelectionOverlay. 1. TextSelectionOverlay là gì mà nghe “ngầu” vậy anh Creyt? Thực ra, TextSelectionOverlay nó giống như cái “bộ đồ nghề” mà các em thấy mỗi khi mình nhấn giữ vào một đoạn văn bản trên điện thoại để chọn chữ, copy, paste, hay cắt ấy. Nhớ không? Cái mà nó hiện ra hai cái “tay cầm” (handle) để mình kéo qua kéo lại, rồi cái thanh menu nhỏ nhỏ phía trên (toolbar) có chữ Copy, Paste, Cut ấy. Đấy, nguyên cái “combo” đó, chính là TextSelectionOverlay. Nói một cách Genz hơn thì nó là cái “vibe” khi user tương tác với text. Thay vì chỉ là mấy cái màu mè, nút bấm mặc định nhàm chán, mình có thể “tút tát” nó lại cho thật “gu” của app mình, hoặc thậm chí là thêm mấy tính năng “độc lạ” mà app khác không có. Giống như các em đi quán cafe, cùng là cà phê thôi, nhưng quán nào có decor đẹp, menu sáng tạo, có “chất” riêng thì mình thích hơn đúng không? App mình cũng vậy, cái TextSelectionOverlay chính là một phần của cái “chất” đó! Để làm gì ư? Đơn giản là để app của mình trông chuyên nghiệp hơn, “ăn nhập” hơn với branding tổng thể, hoặc cung cấp các chức năng đặc biệt ngay tại chỗ mà người dùng cần, tăng tính tiện lợi và “đã” mắt hơn khi sử dụng. 2. Code Ví Dụ Minh Hoạ: “Tút tát” TextSelectionOverlay Ví dụ 1: Thay đổi màu sắc cơ bản với TextSelectionTheme (Dễ mà hiệu quả) Đây là cách “nhẹ đô” nhất để thay đổi màu của các thành phần trong TextSelectionOverlay như handle, cursor, và màu nền khi chọn chữ. Mình sẽ dùng TextSelectionThemeData trong ThemeData hoặc TextSelectionTheme widget. 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: 'Text Selection Demo', theme: ThemeData( // Đây là cách mình 'tút tát' màu cho TextSelectionOverlay textSelectionTheme: const TextSelectionThemeData( cursorColor: Colors.deepPurple, // Màu con trỏ nhấp nháy selectionColor: Colors.deepPurpleAccent.withOpacity(0.3), // Màu nền khi chọn chữ selectionHandleColor: Colors.deepPurple, // Màu của 'tay cầm' (handle) ), // Thêm màu nền cho Scaffold để dễ nhìn hơn scaffoldBackgroundColor: Colors.grey[100], colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelectionOverlay Customization'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), body: const Center( child: Padding( padding: EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Chào mừng đến với lớp học của anh Creyt!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), SizedBox(height: 20), SelectableText( 'Đây là một đoạn văn bản mà các bạn có thể nhấn giữ để chọn. Thử xem các màu sắc của handle và vùng chọn đã thay đổi chưa nhé! Thấy xịn hơn chưa?', style: TextStyle(fontSize: 18), textAlign: TextAlign.justify, ), SizedBox(height: 40), TextField( decoration: InputDecoration( labelText: 'Thử gõ và chọn ở đây nữa nè', border: OutlineInputBorder(), ), ), ], ), ), ), ); } } Giải thích: Đơn giản là mình bọc toàn bộ app trong MaterialApp và dùng ThemeData để định nghĩa textSelectionTheme. Các thuộc tính như cursorColor, selectionColor, selectionHandleColor sẽ giúp mình đổi màu cho con trỏ, vùng chọn và các handle. Nó sẽ ảnh hưởng đến tất cả các SelectableText, TextField, TextFormField trong app. Ví dụ 2: Tùy biến sâu hơn với TextSelectionControls (Dân chơi hệ pro) Khi các em muốn thay đổi hình dáng của handle, hoặc thêm/bớt các nút trong thanh toolbar (menu copy/paste), thì lúc này TextSelectionControls là “vũ khí” tối thượng. Mình sẽ tạo một class kế thừa từ TextSelectionControls và override các phương thức cần thiết. import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Custom Text Selection Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), useMaterial3: true, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Custom TextSelectionOverlay'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), body: Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Chào mừng đến với lớp học của anh Creyt!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 20), // Dùng SelectionArea để áp dụng custom controls SelectionArea( selectionControls: MyCustomTextSelectionControls(context), child: const SelectableText( 'Đây là một đoạn văn bản mà các bạn có thể nhấn giữ để chọn. Hãy để ý xem handle đã đổi màu thành Teal và thanh menu có thêm nút "Search Google" chưa nhé!', style: TextStyle(fontSize: 18), textAlign: TextAlign.justify, ), ), const SizedBox(height: 40), TextField( decoration: const InputDecoration( labelText: 'Thử gõ và chọn ở đây nữa nè', border: OutlineInputBorder(), ), // TextField cũng có thể dùng chung controls nếu không được override // Hoặc bạn có thể bọc nó trong SelectionArea riêng với controls khác selectionControls: MyCustomTextSelectionControls(context), ), ], ), ), ), ); } } // Đây là class 'dân chơi hệ pro' của mình class MyCustomTextSelectionControls extends MaterialTextSelectionControls { MyCustomTextSelectionControls(this.context); final BuildContext context; /// Override màu của handle để nó 'ăn nhập' với màu app @override Color getHandleColor(TextSelectionThemeData data) { return Theme.of(context).colorScheme.tertiary; // Màu teal từ ThemeData } /// Override cái 'bộ đồ nghề' (toolbar) khi chọn chữ @override Widget buildToolbar( BuildContext context, Rect globalEditableRegion, double textLineHeight, Offset selectionMidpoint, List<TextSelectionPoint> endpoints, TextSelectionDelegate delegate, ValueListenable<bool> hideToolbar, ) { final List<Widget> customButtons = [ // Nút 'Copy' mặc định TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.get </* 'copy' */ TextSelectionToolbarTextButton.get , // copy onPressed: () => delegate.copySelection(SelectionChangedCause.toolbar), child: const Text('Copy'), ), // Nút 'Paste' mặc định TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.get , // paste onPressed: () => delegate.pasteText(SelectionChangedCause.toolbar), child: const Text('Paste'), ), // Thêm nút 'Search Google' của riêng mình TextSelectionToolbarTextButton( padding: TextSelectionToolbarTextButton.get , // custom onPressed: () { // Implement logic to search Google with selected text final String selectedText = delegate.textEditingValue.text.substring( delegate.textEditingValue.selection.start, delegate.textEditingValue.selection.end, ); print('Searching Google for: $selectedText'); // Mấy đứa có thể mở link web ở đây nè // launchUrl(Uri.parse('https://www.google.com/search?q=$selectedText')); delegate.hideToolbar(); // Ẩn toolbar sau khi bấm }, child: const Text('Search Google'), ), ]; return TextSelectionToolbar( anchor: globalEditableRegion.center + Offset(0, -textLineHeight / 2), children: customButtons, ); } /// Override hình dáng handle @override Widget buildHandle( BuildContext context, TextSelectionHandleType type, double textLineHeight) { // Đổi màu handle thành màu accent của app, và làm nó nhỏ hơn chút final Color handleColor = getHandleColor(Theme.of(context).textSelectionTheme); return SizedBox( width: 20.0, height: 20.0, child: CustomPaint( painter: _TextSelectionHandlePainter(handleColor), ), ); } } // CustomPainter để vẽ cái handle hình tròn nhỏ xinh class _TextSelectionHandlePainter extends CustomPainter { _TextSelectionHandlePainter(this.color); final Color color; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint()..color = color; canvas.drawCircle(Offset(size.width / 2, size.height / 2), size.width / 2, paint); } @override bool shouldRepaint(_TextSelectionHandlePainter oldPainter) { return color != oldPainter.color; } } Giải thích: Mình tạo MyCustomTextSelectionControls kế thừa từ MaterialTextSelectionControls để tận dụng các logic mặc định mà Flutter cung cấp. getHandleColor: Dùng để định nghĩa màu cho handle. Anh Creyt đã đổi nó thành màu tertiary của ColorScheme để nó “ăn rơ” hơn với theme. buildToolbar: Đây là nơi mình “chế biến” cái thanh menu. Mình có thể thêm/bớt các TextSelectionToolbarTextButton hoặc bất kỳ widget nào khác vào danh sách children của TextSelectionToolbar. Ở đây anh Creyt đã thêm nút Search Google. buildHandle: Cái này cho phép mình vẽ lại hoàn toàn hình dáng của handle. Anh Creyt đã vẽ một cái hình tròn nhỏ xinh bằng CustomPaint và _TextSelectionHandlePainter. Cuối cùng, mình dùng SelectionArea widget và truyền MyCustomTextSelectionControls vào thuộc tính selectionControls để áp dụng các tùy chỉnh này cho các SelectableText bên trong nó. Đối với TextField, mình có thể truyền trực tiếp vào selectionControls của TextField. 3. Mẹo Vặt Của Creyt (Best Practices) Để Ghi Nhớ Và Dùng Thực Tế Đừng “làm quá”: Tùy biến là tốt, nhưng đừng làm nó quá khác lạ đến mức người dùng không nhận ra đây là chức năng chọn văn bản nữa. Giữ cho nó trực quan và dễ hiểu. Đồng bộ UI/UX: Màu sắc, hình dáng của handle và toolbar nên “ăn nhập” với tổng thể thiết kế của app. Đừng để nó lạc quẻ như “áo gấm đi đêm” nhé. Đảm bảo Accessibility: Nhớ rằng không phải ai cũng có ngón tay thon thả hay thị lực tốt. Handle nên đủ lớn để dễ chạm, màu sắc phải có độ tương phản tốt để dễ nhìn. Flutter đã làm rất tốt điều này với các widget mặc định, khi tùy biến mình cần cân nhắc. Thử nghiệm đa nền tảng: Android và iOS có những “gu” thiết kế riêng. TextSelectionOverlay có thể trông hơi khác nhau. Hãy test trên cả hai để đảm bảo trải nghiệm mượt mà nhất. Chỉ tùy biến khi cần: Nếu app của em chỉ cần chức năng copy/paste cơ bản, thì cứ để mặc định cho Flutter lo. Đừng “cố đấm ăn xôi” tùy biến chỉ vì muốn “khác người” mà không có mục đích rõ ràng. 4. Ứng Dụng Thực Tế: Ai Đã Dùng Rồi? Các em để ý các app sau, họ tận dụng TextSelectionOverlay rất xịn sò: Notion / Medium: Khi các em chọn một đoạn văn bản trong các ứng dụng ghi chú hoặc đọc bài viết này, thanh toolbar không chỉ có Copy mà còn có các tùy chọn Bold, Italic, Highlight, Comment, hoặc thậm chí là Turn into block. Đó chính là tùy biến TextSelectionControls đó! Google Docs / Microsoft Word: Đây là “ông tổ” của việc tùy biến thanh chọn văn bản. Các em có thể thấy vô vàn tùy chọn định dạng, comment, dịch thuật khi chọn một đoạn text. Ứng dụng đọc sách (Kindle, Google Books): Khi chọn một từ hoặc đoạn văn, các app này thường hiện ra các tùy chọn như Highlight, Note, Search Dictionary, Translate, hay Share Quote. Cực kỳ tiện lợi cho “mọt sách” đúng không? 5. Thử Nghiệm Và Nên Dùng Cho Case Nào? Vậy khi nào thì mình nên “động tay” vào TextSelectionOverlay? App có Branding mạnh: Nếu app của em có một bộ màu sắc, font chữ riêng biệt, việc tùy biến handle và toolbar theo branding sẽ giúp app trông “chuyên nghiệp” và “có gu” hơn rất nhiều. Cần thêm chức năng độc đáo: Đây là lúc buildToolbar phát huy tác dụng. Ví dụ, trong một app từ điển, khi chọn một từ, em có thể thêm nút Tra từ, Thêm vào danh sách học. Hoặc trong một app mạng xã hội, có thể có nút Share Quote để chia sẻ nhanh đoạn văn bản đó lên story. Cải thiện trải nghiệm người dùng (UX) và Accessibility: Nếu handle mặc định quá nhỏ hoặc khó nhìn trên một số thiết bị, mình có thể thay đổi kích thước, màu sắc để nó dễ tương tác hơn. Hoặc nếu app của em hướng đến người dùng có nhu cầu đặc biệt, việc tùy biến này là cực kỳ quan trọng. Nhớ nhé, TextSelectionOverlay không chỉ là một chi tiết nhỏ, nó là một phần quan trọng tạo nên sự tinh tế và khác biệt cho app của các em. Hãy tận dụng nó để “phù phép” cho app của mình trở nên “xịn sò” hơn trong mắt người dùng! Anh Creyt tin mấy đứa làm được! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

41 Đọc tiếp