Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
ShaderMaskLayer: Phù phép UI Flutter với Hiệu Ứng Mặt Nạ Đỉnh Cao
21/03/2026

ShaderMaskLayer: Phù phép UI Flutter với Hiệu Ứng Mặt Nạ Đỉnh Cao

Hôm nay, anh Creyt sẽ 'bóc tách' cho tụi em một cái 'magic trick' cực đỉnh trong Flutter, giúp UI của tụi em từ 'bình thường' hóa 'phi thường' chỉ trong một nốt nhạc: đó chính là ShaderMask (và đằng sau nó là ShaderMaskLayer). Tưởng tượng thế này: em có một bức tranh (child widget), và em muốn 'che' một phần của nó đi, hoặc tô màu cho nó theo một kiểu 'gradient' siêu ngầu, hoặc thậm chí là dùng một bức ảnh khác làm 'khuôn' để cắt cái bức tranh gốc. ShaderMask chính là cái 'khuôn thần kỳ' đó! Nó không chỉ đơn thuần là cắt hình vuông, hình tròn đâu nha. Cái 'khuôn' này có thể là một dải màu chuyển sắc (gradient), một tấm ảnh mờ ảo, hay thậm chí là một hiệu ứng 'glitch' do em tự code ra. Về cơ bản, nó dùng một Shader (bộ tô màu) để làm mặt nạ. Chỗ nào cái Shader này 'tô' màu rõ, thì cái widget con của em sẽ hiện ra. Chỗ nào nó 'tô' trong suốt, thì widget con sẽ biến mất. Đơn giản là vậy! Và ShaderMaskLayer? À, đó là 'công nhân' cần mẫn phía sau hậu trường, là cái 'bàn vẽ' mà Flutter dùng để thực hiện tất cả các phép màu về mặt nạ này. Tụi em dùng ShaderMask trên bề mặt, còn ShaderMaskLayer là cái 'công cụ' mà Flutter gọi ra để vẽ vời, xử lý pixel các kiểu con đà điểu. Code Ví Dụ: Chữ Gradient Siêu Ngầu Nói nhiều lý thuyết khô khan quá đúng không? Thôi, mình 'nhảy' thẳng vào code để thấy nó 'cool' cỡ nào nè. Ví dụ kinh điển nhất, và cũng là cái tụi em hay thấy trên mấy cái app 'xịn xò' là: Chữ chuyển màu gradient. 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 ShaderMask Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const GradientTextScreen(), ); } } class GradientTextScreen extends StatelessWidget { const GradientTextScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ShaderMask: Chữ Gradient Siêu Ngầu'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Đây là màn trình diễn của ShaderMask ShaderMask( // Cái này là 'bộ lọc màu' hay 'khuôn' của mình nè shaderCallback: (bounds) { return const LinearGradient( colors: [Colors.purple, Colors.pink, Colors.red], // Dải màu chuyển sắc begin: Alignment.topLeft, // Bắt đầu từ góc trên bên trái end: Alignment.bottomRight, // Kết thúc ở góc dưới bên phải ).createShader(bounds); // Tạo shader từ dải màu đó }, blendMode: BlendMode.srcIn, // Cách mà shader hòa trộn với widget con // Đây là 'bức tranh' mà mình muốn áp dụng mặt nạ child: const Text( 'Creyt\'s Code Vibes', style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, // Màu ở đây không quan trọng lắm vì sẽ bị ShaderMask thay thế color: Colors.white, // Mặc định là trắng, nhưng shader sẽ override ), ), ), const SizedBox(height: 30), ShaderMask( shaderCallback: (bounds) { return const RadialGradient( colors: [Colors.yellow, Colors.orange, Colors.red], center: Alignment.center, radius: 0.8, ).createShader(bounds); }, blendMode: BlendMode.srcIn, child: const Icon( Icons.star, size: 100, color: Colors.white, // Cũng sẽ bị override ), ), const SizedBox(height: 30), // Thử với một Image làm mask (hoặc áp dụng mask lên Image) ShaderMask( shaderCallback: (bounds) { // Tưởng tượng bạn có một hình ảnh đen trắng, // phần màu trắng sẽ cho phép child hiện ra, // phần màu đen sẽ che đi. Ở đây mình dùng gradient giả lập. return const LinearGradient( colors: [Colors.transparent, Colors.black, Colors.transparent], stops: [0.0, 0.5, 1.0], begin: Alignment.topCenter, end: Alignment.bottomCenter, ).createShader(bounds); }, blendMode: BlendMode.dstIn, // DstIn: hiển thị nơi cả mask và child đều có pixel child: Image.network( 'https://picsum.photos/200', // Một hình ảnh bất kỳ width: 200, height: 200, fit: BoxFit.cover, ), ), ], ), ), ); } } Trong ví dụ này, anh dùng LinearGradient để tạo ra một dải màu chuyển sắc từ tím, hồng đến đỏ. Cái dải màu này chính là Shader của chúng ta, và nó được dùng làm 'mặt nạ' cho widget Text con. Kết quả là, chữ 'Creyt's Code Vibes' sẽ được tô màu gradient siêu ngầu! Mẹo Vặt Từ Lão Làng Creyt (Best Practices) Mấy đứa nghe kỹ đây, đây là mấy cái 'mẹo vặt' từ lão làng Creyt mà tụi em nên 'bỏ túi' để dùng ShaderMask cho 'chuẩn bài' nè: Hiểu BlendMode: Cái thuộc tính blendMode trong ShaderMask quan trọng lắm nha. Nó quyết định cách Shader (mask) và child (nội dung) hòa trộn với nhau. BlendMode.srcIn: Thường dùng nhất. Nó sẽ chỉ hiển thị phần child nằm trong vùng 'có màu' của shader. Như ví dụ chữ gradient ấy. BlendMode.dstIn: Hiển thị phần child nơi cả shader và child đều có pixel. Thường dùng khi shader là một hình ảnh 'texture' để tạo hiệu ứng 'cắt gọt' cho child. Cứ thử nghiệm mấy cái blendMode khác nhau để xem hiệu ứng nào 'hợp gu' nhất. Performance (Hiệu suất): ShaderMask khá 'ngốn' tài nguyên, đặc biệt nếu Shader của em phức tạp (ví dụ, dùng ImageShader với ảnh lớn, hoặc custom shader phức tạp). Nên dùng có chọn lọc, đừng lạm dụng quá mức nếu không cần thiết. Kết hợp với các Widget khác: ShaderMask thường đi kèm với các widget khác như ClipRRect để tạo ra những hiệu ứng mặt nạ trên các hình dạng đặc biệt, hoặc AnimatedBuilder để tạo hiệu ứng động cho Shader. Thử nghiệm với các loại Gradient: Đừng chỉ dừng lại ở LinearGradient. Hãy thử RadialGradient (chuyển màu từ tâm ra) hay SweepGradient (chuyển màu xoay tròn) để tạo ra các hiệu ứng độc đáo hơn. Custom Shader (Level Up): Nếu muốn 'đỉnh của chóp', em có thể tự viết CustomShader bằng ngôn ngữ GLSL rồi nhúng vào Flutter. Cái này thì hơi 'khoai' một chút nhưng kết quả thì 'ảo diệu' khỏi bàn! (Cái này thì để dành cho buổi học khác nha, hôm nay mình 'nhẹ nhàng' thôi). Ứng Dụng Thực Tế: Ai Đã Dùng? Tụi em có biết mấy cái app 'hot hit' mà tụi em dùng hàng ngày đã ứng dụng cái 'chiêu' này như thế nào không? Spotify: Thường xuyên sử dụng gradient cho các tiêu đề bài hát, tên nghệ sĩ, hoặc các nút bấm để tạo cảm giác hiện đại, 'chill' và thu hút thị giác. ShaderMask là một trong những công cụ để họ làm điều đó. Instagram/TikTok: Mặc dù không phải ShaderMask trực tiếp, nhưng concept 'filter' ảnh/video mà tụi em dùng hàng ngày chính là ứng dụng của Shader (bộ tô màu). Tưởng tượng ShaderMask là một 'filter' cho các widget UI của em. Các ứng dụng ngân hàng/tài chính: Đôi khi họ dùng gradient để làm nổi bật số dư, các chỉ số quan trọng, tạo cảm giác 'sang chảnh' và đáng tin cậy. Game UI: Các thanh máu, thanh mana trong game thường có hiệu ứng gradient hoặc texture fill. Khi thanh máu giảm, phần gradient cũng có thể thay đổi để tạo hiệu ứng thị giác mạnh mẽ hơn. ShaderMask có thể giúp tạo ra những hiệu ứng này một cách linh hoạt. Khi Nào Nên Dùng và Tránh Dùng? Anh Creyt đã 'chinh chiến' với ShaderMask này không ít lần rồi, và đây là vài lời khuyên 'xương máu' từ kinh nghiệm thực tế: Nên dùng khi nào? Tạo điểm nhấn thương hiệu (Branding): Khi muốn logo, tiêu đề, hoặc các yếu tố quan trọng của app có một dải màu gradient đặc trưng, 'không đụng hàng'. Hiệu ứng thị giác 'sang chảnh': Các button, card, hoặc text cần một vẻ ngoài cao cấp, hiện đại, thu hút ánh nhìn. UI động (Dynamic UI): Khi em muốn hiệu ứng chuyển màu thay đổi theo trạng thái (ví dụ, thanh tiến trình, thanh máu thay đổi màu khi gần hết). Masking ảnh/widget với hình dạng phức tạp: Mặc dù ClipRRect hay ClipPath cũng làm được, nhưng ShaderMask cho phép em dùng một ImageShader để tạo mặt nạ dựa trên độ trong suốt của một bức ảnh khác, mở ra nhiều khả năng sáng tạo hơn. Khi nào thì 'tạm dừng' và suy nghĩ lại? Khi chỉ cần một màu solid: Đừng 'lấy dao mổ trâu giết gà' khi chỉ cần tô một màu đơn giản. Dùng TextStyle(color: ...) hoặc Container(color: ...) là đủ. Hiệu suất là ưu tiên hàng đầu: Nếu em đang làm một app mà mỗi mili giây đều quý giá, và em muốn áp dụng ShaderMask cho rất nhiều element cùng lúc, hãy cẩn thận. Test kỹ hiệu suất trên các thiết bị yếu hơn trước khi 'nhảy' vào. Khi chỉ cần bo góc đơn giản: ClipRRect sẽ là lựa chọn tốt hơn nhiều so với ShaderMask nếu mục đích chỉ là bo tròn các góc của một widget. Tóm lại, ShaderMask là một công cụ cực kỳ mạnh mẽ trong 'kho vũ khí' của một 'dev' Flutter để tạo ra những UI 'đỉnh cao' và có tính thẩm mỹ. Hãy 'nghịch' nó thật nhiều, 'vọc' nó thật kỹ, và em sẽ thấy UI của mình 'lên một tầm cao mớ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é!

40 Đọc tiếp
SelectionRegistrar: Vị Quản Gia Thầm Lặng Của Vùng Chọn Flutter
21/03/2026

SelectionRegistrar: Vị Quản Gia Thầm Lặng Của Vùng Chọn Flutter

Chào các em, lại là anh Creyt đây! Hôm nay, chúng ta sẽ "mổ xẻ" một anh chàng thầm lặng nhưng cực kỳ quan trọng trong thế giới Flutter: SelectionRegistrar. Nghe cái tên thì có vẻ hơi "học thuật" và "khô khan" đúng không? Nhưng đừng lo, anh sẽ biến nó thành câu chuyện cổ tích hiện đại, nơi các em là những phù thủy code tài ba. 1. SelectionRegistrar Là Gì? Để Làm Gì? (Theo Hướng Gen Z) Để dễ hình dung, các em hãy tưởng tượng thế này: Các em đang ở một bữa tiệc sinh nhật hoành tráng, và trên bàn có rất nhiều mẩu giấy note nhỏ xinh, mỗi mẩu ghi một câu nói hay ho (SelectableText widgets). Mỗi mẩu giấy này đều có thể được "chọn" để đọc kỹ hơn, hoặc "copy" lại để gửi cho crush. SelectionRegistrar chính là anh chàng quản lý tiệc kiêm "thủ thư" của đống giấy note này. Anh ta không trực tiếp đọc hay sao chép nội dung, nhưng anh ta có một cuốn sổ thần kỳ ghi lại tất tần tật thông tin về vị trí, trạng thái của tất cả các mẩu giấy note có thể "chọn" trong khu vực tiệc đó. Khi các em, người chủ tiệc (SelectionArea), muốn "chọn" một hay nhiều mẩu giấy, anh quản lý sẽ dựa vào cuốn sổ của mình để giúp các em thao tác mượt mà, từ việc hiển thị tay cầm kéo chọn (selection handles) cho đến bật menu "Copy" thần thánh. Nói một cách "code-friendly" hơn, SelectionRegistrar trong Flutter là một widget nội bộ (thường được cung cấp bởi SelectionArea) đóng vai trò là điểm đăng ký tập trung cho tất cả các widget con có khả năng chọn văn bản (như SelectableText, TextField, CupertinoTextField) trong một nhánh cây widget nhất định. Nó giúp điều phối và quản lý toàn bộ quá trình chọn văn bản, đảm bảo các vùng chọn không bị chồng chéo, các menu ngữ cảnh xuất hiện đúng chỗ, và trải nghiệm người dùng được liền mạch. 2. Code Ví Dụ Minh Họa Rõ Ràng Trong Flutter, các em thường sẽ không trực tiếp tương tác với SelectionRegistrar. Thay vào đó, các em sẽ sử dụng SelectionArea – một widget tiện lợi đã "đóng gói" sẵn SelectionRegistrar và mọi logic cần thiết để quản lý vùng chọn. Khi các em bọc các widget có thể chọn (như SelectableText) bên trong SelectionArea, SelectionRegistrar sẽ tự động hoạt động "ẩn mình" phía sau. 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: 'SelectionRegistrar Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SelectionRegistrar Demo'), ), body: Center( // Bọc toàn bộ khu vực muốn có khả năng chọn văn bản bằng SelectionArea child: SelectionArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Chào mừng các em đến với thế giới Flutter!', style: TextStyle(fontSize: 20), ), const SizedBox(height: 20), // SelectableText 1 const SelectableText( 'Đây là đoạn văn bản đầu tiên mà các em có thể chọn.', textAlign: TextAlign.center, style: TextStyle(fontSize: 16, color: Colors.deepPurple), ), const SizedBox(height: 10), // SelectableText 2 const SelectableText( 'Hãy thử long-press và kéo để chọn nhiều đoạn nhé!', textAlign: TextAlign.center, style: TextStyle(fontSize: 16, color: Colors.teal), ), const SizedBox(height: 30), // TextField cũng tự động tương thích với SelectionArea Container( padding: const EdgeInsets.symmetric(horizontal: 20), child: const TextField( decoration: InputDecoration( labelText: 'Nhập gì đó vào đây để chọn thử!', border: OutlineInputBorder(), ), maxLines: 2, ), ), const SizedBox(height: 20), // Một đoạn Text thường không chọn được const Text( 'Đoạn này thì không chọn được đâu nha.', style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), ), ], ), ), ), ); } } Trong ví dụ trên, khi các em chạy ứng dụng và giữ (long-press) vào một trong các đoạn SelectableText hoặc TextField, các em sẽ thấy: Các tay cầm chọn văn bản xuất hiện. Các em có thể kéo chọn qua nhiều đoạn SelectableText khác nhau trong cùng SelectionArea. Menu ngữ cảnh (Copy, Cut, Paste) xuất hiện đúng lúc. Tất cả những điều "vi diệu" này đều nhờ anh chàng SelectionRegistrar đang làm việc cật lực phía sau hậu trường, nhận đăng ký từ các SelectableText và TextField và báo cho SelectionArea biết "chúng nó" đang ở đâu, trạng thái thế nào để quản lý. 3. Mẹo (Best Practices) Từ Anh Creyt "Đừng Tự Làm Anh Hùng": Trừ khi các em đang xây dựng một widget chọn văn bản siêu cấp phức tạp của riêng mình, đừng cố gắng tự tạo hoặc tương tác trực tiếp với SelectionRegistrar. Hãy để SelectionArea lo phần đó. Nó giống như việc các em có siêu năng lực nhưng không cần tự tay xây nhà, mà dùng dịch vụ xây dựng chuyên nghiệp vậy. Hiểu Vai Trò "Thủ Thư": Luôn ghi nhớ SelectionRegistrar là người quản lý, tập hợp thông tin về các vùng chọn. Hiểu được vai trò này sẽ giúp các em debug dễ hơn nếu có vấn đề về chọn văn bản. Phạm Vi Quan Trọng: SelectionArea sẽ định nghĩa "phạm vi" hoạt động của SelectionRegistrar. Chỉ những widget con nằm trong cây con của SelectionArea mới được đăng ký và quản lý bởi SelectionRegistrar của nó. Giống như anh quản lý tiệc chỉ lo cho khu vực tiệc của mình thôi, không sang tiệc nhà hàng xóm đâu. Kiểm Tra BuildContext: Nếu có lúc cần truy cập SelectionRegistrar (ví dụ, để lấy thông tin về vùng chọn hiện tại), các em có thể dùng SelectionRegistrar.of(context). Nhưng nhớ là phải đảm bảo context đó nằm trong một SelectionArea hợp lệ nhé, nếu không sẽ "toang" đấy. 4. Ứng Dụng Thực Tế Đã Dùng Hầu như bất kỳ ứng dụng nào cho phép người dùng chọn và tương tác với văn bản đều đang sử dụng hoặc có cơ chế tương tự SelectionRegistrar: Ứng dụng Chat (WhatsApp, Telegram): Khi các em nhấn giữ một tin nhắn để copy, forward. Trình duyệt web trong app (WebView): Chọn văn bản trên trang web hiển thị trong app của các em. Ứng dụng ghi chú (Google Keep, Notion): Chọn, copy, di chuyển các đoạn văn bản trong ghi chú. Ứng dụng đọc sách điện tử: Chọn một đoạn văn để tra từ điển, highlight hoặc chia sẻ. Nói chung, cứ nơi nào có văn bản và người dùng muốn "túm" lấy nó để làm gì đó, thì 99% là có một "anh quản lý" như SelectionRegistrar đang làm việc. 5. Thử Nghiệm Đã Từng và Hướng Dẫn Nên Dùng Cho Case Nào Thử nghiệm: Anh Creyt đã từng thử nghiệm việc không bọc các SelectableText trong SelectionArea. Kết quả là gì? Mỗi SelectableText sẽ hoạt động như một "hòn đảo cô đơn", các em chỉ có thể chọn văn bản trong một SelectableText đó thôi. Không thể kéo chọn liền mạch từ SelectableText này sang SelectableText khác. Menu ngữ cảnh cũng có thể hoạt động không đồng bộ hoặc tệ hơn là không xuất hiện. Hướng dẫn nên dùng cho case nào: Các em nên sử dụng SelectionArea (tức là gián tiếp dùng SelectionRegistrar) bất cứ khi nào: Cần khả năng chọn văn bản linh hoạt: Khi các em muốn người dùng có thể chọn và sao chép văn bản từ các widget Text thông thường, hoặc các widget hiển thị văn bản khác. Nhiều vùng chọn trong cùng một khu vực: Đặc biệt hữu ích khi các em có nhiều SelectableText hoặc TextField và muốn người dùng có thể kéo chọn liền mạch qua chúng. Trải nghiệm người dùng nhất quán: SelectionArea đảm bảo hành vi chọn văn bản của ứng dụng các em nhất quán với các tiêu chuẩn của nền tảng (Android/iOS), bao gồm cả việc hiển thị các tay cầm và menu ngữ cảnh. Tóm lại: SelectionRegistrar là một phần quan trọng của hệ thống chọn văn bản trong Flutter, thường được ẩn sau SelectionArea. Hiểu về nó giúp các em nắm vững cách Flutter xử lý tương tác người dùng và tạo ra những ứng dụng mượt mà, chuyên nghiệp hơn. Hãy cứ để SelectionArea làm nhiệm vụ "quản lý" cho các em, và tập trung vào việc tạo ra những nội dung thật "chất" để người dùng tha hồ mà "chọn" nhé! Hẹn gặp lại trong bài học tiếp theo! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

42 Đọc tiếp
SelectionContainer: Bậc thầy 'chọn' chữ trong Flutter của Gen Z
21/03/2026

SelectionContainer: Bậc thầy 'chọn' chữ trong Flutter của Gen Z

Chào các dân chơi hệ code, anh Creyt lại lên sóng đây! Hôm nay, chúng ta sẽ cùng nhau 'mổ xẻ' một thằng cu tưởng chừng nhỏ bé nhưng lại có võ công thâm hậu trong Flutter: SelectionContainer. Nghe tên thì 'học thuật' vậy thôi, chứ nó chính là 'vệ sĩ' bảo kê cho mấy cái chữ của bạn được quyền 'đi du lịch' (copy-paste) từ app này sang app khác đấy! 1. SelectionContainer là gì mà 'uy tín' vậy? Thôi bỏ qua mấy cái định nghĩa khô khan trên docs đi. Anh em Gen Z hiểu nôm na thế này: Tưởng tượng app của bạn là một khu vườn thượng uyển đẹp mê hồn, đầy rẫy những bông hoa (là các đoạn text, thông tin). Mặc định, bạn chỉ có thể ngắm hoa thôi, chứ không được phép 'hái' (chọn và copy) đâu nhé. Khó chịu không? SelectionContainer chính là cái biển báo 'Tự do hái hoa' mà bạn cắm vào những khu vực cụ thể trong vườn. Nó không phải là bông hoa, cũng không phải là cái kéo để hái, mà nó là người cấp phép, định danh khu vực nào được quyền thao tác chọn văn bản. Nói cách khác, trong Flutter, khi bạn muốn người dùng có thể chọn và sao chép một đoạn văn bản hay một nhóm văn bản mà theo mặc định nó không cho phép (hoặc bạn muốn kiểm soát chặt chẽ hơn), thì SelectionContainer chính là 'chân ái'. Nó là một widget cấp thấp, giúp bạn đánh dấu một khu vực cụ thể trong cây widget của mình là 'có thể lựa chọn' (selectable region). 2. Dùng để làm gì? 'Quyền năng' copy-paste trong tầm tay! Tại sao lại phải dùng nó khi Text widget trong MaterialApp thường đã cho phép chọn rồi? À, đây mới là cái hay này: Kiểm soát vùng chọn: Đôi khi bạn có một Column chứa nhiều Text widget, và bạn muốn người dùng có thể chọn tất cả chúng như một khối duy nhất, không phải từng cái một. SelectionContainer giúp bạn làm điều đó. Cho các widget không phải Text: Bạn tạo một widget tùy chỉnh hiển thị văn bản, nhưng nó không phải là Text widget truyền thống. Mặc định nó sẽ không cho chọn. SelectionContainer sẽ 'phù phép' cho nó. Tắt/bật linh hoạt: Muốn một đoạn văn bản lúc thì cho chọn, lúc thì không? SelectionContainer là công cụ của bạn. Hỗ trợ RichText và các layout phức tạp: Khi bạn dùng RichText để tạo ra các đoạn văn bản với nhiều style khác nhau, SelectionContainer sẽ đảm bảo trải nghiệm chọn mượt mà. Nói chung, nó là công cụ để bạn 'thẩm quyền hóa' việc copy-paste trong app của mình, biến những nội dung 'bất khả xâm phạm' thành 'có thể trích xuất' một cách dễ dàng. 3. Code Ví Dụ Minh Họa: 'Chọn' ngay và luôn! Để anh em thấy rõ 'sức mạnh' của nó, chúng ta cùng xem vài ví dụ 'thực chiến' nhé. Nhớ là SelectionContainer thường được dùng kết hợp với SelectionArea (là một widget 'cao cấp' hơn, tiện lợi hơn, bọc SelectionContainer bên trong) để quản lý vùng chọn. Ví dụ 1: Làm cho một đoạn văn bản đơn giản có thể chọn (dù Text thường đã chọn được) import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'SelectionContainer Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('SelectionContainer Basic')), body: Center( child: SelectionContainer.disabled( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Đoạn này không chọn được đâu nha!', style: TextStyle(fontSize: 20, color: Colors.red), ), const SizedBox(height: 20), // Dùng SelectionContainer để bọc một vùng có thể chọn SelectionContainer.selectable( child: const Text( 'Anh Creyt chào Gen Z! Đoạn này thì chọn thoải mái nhé.', style: TextStyle(fontSize: 20, color: Colors.green), ), ), const SizedBox(height: 20), const Text( 'Còn đoạn dưới đây lại vô hiệu hóa chọn.', style: TextStyle(fontSize: 18), ), ], ), ), ), ); } } Trong ví dụ trên, anh dùng SelectionContainer.disabled ở ngoài cùng để vô hiệu hóa toàn bộ khả năng chọn văn bản cho Column. Sau đó, anh dùng SelectionContainer.selectable để ghi đè lại và chỉ cho phép chọn đoạn Text cụ thể bên trong nó. Thấy 'quyền năng' chưa? Ví dụ 2: Kết hợp với SelectionArea để quản lý vùng chọn lớn hơn SelectionArea là một wrapper tiện lợi hơn, nó tự động quản lý SelectionContainer cho bạn. Thường thì bạn sẽ bọc toàn bộ Scaffold body hoặc thậm chí MaterialApp bằng SelectionArea. 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: 'SelectionArea Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('SelectionArea & Container')), body: SelectionArea( // Mọi thứ trong SelectionArea này đều có thể chọn được mặc định child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const Text( 'Đây là một đoạn văn bản dài mà bạn có thể chọn và sao chép thoải mái. Nó nằm trong SelectionArea.', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), // Dù nằm trong SelectionArea, nhưng SelectionContainer.disabled // sẽ ghi đè và vô hiệu hóa chọn cho đoạn này. SelectionContainer.disabled( child: const Text( 'Đoạn văn bản này lại bị anh Creyt 'khóa' không cho chọn, dù nó nằm trong vùng SelectionArea lớn.', style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic, color: Colors.grey), ), ), const SizedBox(height: 20), const Text( 'Còn đây là một đoạn khác, vẫn trong SelectionArea, nên vẫn chọn được như thường.', style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), // Ví dụ về một widget tùy chỉnh không phải Text, // nhưng muốn nó có thể chọn được nội dung bên trong. SelectionContainer.selectable( child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.lightBlue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blueAccent), ), child: const Text( 'Đây là nội dung từ một custom widget mà bạn vẫn có thể chọn. Tuyệt vời không?', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ), ], ), ), ), ); } } Qua ví dụ này, anh em thấy rõ cách SelectionContainer có thể 'ghi đè' lên SelectionArea ở cấp cao hơn để kiểm soát từng vùng nhỏ một. Nó giống như bạn có một chính sách chung cho cả nước (SelectionArea), nhưng lại có những quy định đặc biệt cho từng tỉnh (SelectionContainer) vậy. 4. Mẹo (Best Practices) từ 'lão làng' Creyt Ưu tiên SelectionArea trước: Đối với phần lớn các trường hợp, bạn chỉ cần bọc toàn bộ Scaffold body hoặc MaterialApp bằng SelectionArea. Nó sẽ tự động làm cho tất cả Text widget bên trong có thể chọn được, cực kỳ tiện lợi. SelectionContainer cho trường hợp 'đặc biệt': Chỉ dùng SelectionContainer khi bạn cần kiểm soát cực kỳ chi tiết: vô hiệu hóa chọn ở một vùng cụ thể trong SelectionArea lớn, hoặc bật chọn cho một widget custom không phải Text. Đừng lạm dụng: Không cần thiết phải bọc từng Text widget nhỏ bằng SelectionContainer nếu chúng đã nằm trong một SelectionArea lớn hơn. Việc này có thể gây dư thừa và đôi khi ảnh hưởng nhẹ đến hiệu năng (dù thường không đáng kể). Hiểu cách hoạt động của SelectionManager: Mặc định, MaterialApp và CupertinoApp đã có một DefaultSelectionManager lo vụ chọn văn bản rồi. SelectionArea và SelectionContainer hoạt động trên nền tảng đó để cung cấp sự linh hoạt hơn. Accessibility (Khả năng tiếp cận): Việc cho phép chọn và sao chép văn bản là một điểm cộng lớn cho khả năng tiếp cận. Người dùng có thể dễ dàng lấy thông tin để dùng cho các mục đích khác (ví dụ: tra cứu, chia sẻ, lưu trữ). 5. Ứng dụng/Website đã 'thẩm thấu' SelectionContainer Thực ra, SelectionContainer là một widget nội bộ của Flutter để cho phép chức năng chọn văn bản, chứ không phải là một thành phần UI hiển thị rõ ràng. Tuy nhiên, bất kỳ ứng dụng Flutter nào mà bạn có thể chọn và sao chép văn bản từ đó đều đang gián tiếp sử dụng hoặc dựa vào cơ chế tương tự SelectionContainer để hoạt động. Ví dụ: Các ứng dụng đọc tin tức/blog (Medium, VnExpress, Báo Mới): Bạn đọc một bài báo, thấy đoạn nào hay thì bôi đen, copy để chia sẻ. Đó chính là SelectionContainer đang làm nhiệm vụ. Ứng dụng nhắn tin (Zalo, Messenger, WhatsApp): Bạn copy một câu nói 'bá đạo' của bạn bè để gửi cho đứa khác. SelectionContainer 'góp công' vào đó. Ứng dụng ghi chú (Google Keep, Notion): Chắc chắn phải có chức năng chọn/copy rồi, nếu không thì ghi chú làm gì? Các trang thương mại điện tử (Shopee, Lazada): Bạn muốn copy tên sản phẩm, mô tả để tìm kiếm thêm thông tin. SelectionContainer là 'người hùng thầm lặng'. Nói chung, hễ chỗ nào bạn thao tác 'nhấn giữ' (long press) và kéo để bôi đen chữ được, thì y như rằng có 'bóng dáng' của một SelectionContainer nào đó đang làm nhiệm vụ của nó! 6. Thử nghiệm và Nên dùng cho case nào? Nên dùng khi: Xây dựng các widget hiển thị văn bản tùy chỉnh: Nếu bạn không dùng Text widget mà tự vẽ chữ, hoặc dùng các thư viện render text đặc biệt, bạn sẽ cần SelectionContainer để kích hoạt tính năng chọn. Quản lý vùng chọn phức tạp: Khi bạn muốn một Column chứa nhiều Text widget được chọn như một khối duy nhất, hoặc bạn có các vùng văn bản đan xen giữa có thể chọn và không thể chọn. Tắt chọn cho một số vùng nhất định: Bạn có một SelectionArea bao quát cả app, nhưng muốn một số đoạn văn bản không được phép chọn (ví dụ: số điện thoại nội bộ, mã bí mật...). Dùng SelectionContainer.disabled. Tăng cường khả năng tiếp cận: Đảm bảo người dùng có thể dễ dàng trích xuất thông tin từ ứng dụng của bạn. Không nên dùng (hoặc nên dùng SelectionArea thay thế) khi: Tất cả Text widget trong MaterialApp: Mặc định chúng đã có thể chọn được rồi, trừ khi bạn muốn vô hiệu hóa chúng. Bạn chỉ cần bật chọn cho toàn bộ màn hình: Dùng SelectionArea bọc Scaffold body là đủ, không cần SelectionContainer cho từng item nhỏ. Nội dung hoàn toàn không liên quan đến văn bản: Ví dụ: một hình ảnh, một nút bấm (button), một icon. Dù có bọc SelectionContainer cũng chẳng có gì để chọn đâu nhé! Nhớ nhé anh em, SelectionContainer không phải là 'siêu nhân' mà là 'người điều phối' giúp cho việc chọn văn bản trong app của bạn trở nên linh hoạt và mạnh mẽ hơn. Hiểu rõ nó, bạn sẽ có thêm một 'vũ khí' lợi hại để làm app Flutter 'xịn xò' hơn rất nhiều! Chúc anh em code vui vẻ, gặp lại trong bài giảng 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é!

50 Đọc tiếp
SelectionArea Flutter: Biến App Bạn Thành Máy Photocopy Content Xịn Sò
21/03/2026

SelectionArea Flutter: Biến App Bạn Thành Máy Photocopy Content Xịn Sò

Chào các dân chơi hệ Flutter! Hôm nay, anh Creyt sẽ "unbox" một widget nghe có vẻ đơn giản nhưng lại là "key" để app của mấy đứa "flex" độ xịn sò về UX. Đó chính là SelectionArea – Nghe tên đã thấy vibe "chọn lựa" rồi đúng không? Tưởng tượng thế này: App của mấy đứa như một cuốn sách hay ho, nhưng trước giờ, người dùng chỉ có thể đọc thôi. Muốn trích dẫn một câu, một đoạn thơ tâm đắc để share lên story, hay đơn giản là copy cái tên sản phẩm siêu dài để tìm kiếm, thì chịu chết. Giống như mấy đứa xem phim qua màn hình, muốn "chộp" lấy nhân vật ra ngoài để hun một cái cũng không được vậy! SelectionArea chính là "cây đũa thần" biến cuốn sách tĩnh đó thành một bản PDF mà mấy đứa có thể bôi đen, copy, paste thoải mái. Nó biến nội dung trên app của mấy đứa từ "chỉ để ngắm" thành "có thể chạm và tương tác". Nói ngắn gọn, nó là cái "remote control" cho phép người dùng "photocopy" nội dung từ app của mấy đứa ra thế giới bên ngoài một cách "chill" nhất. SelectionArea Là Gì Và Để Làm Gì? Về cơ bản, SelectionArea trong Flutter là một widget được thiết kế để cho phép người dùng chọn (select) văn bản hoặc các widget con khác nằm trong phạm vi của nó. Khi được bọc trong SelectionArea, bất kỳ Text widget nào (hoặc widget hiển thị văn bản) bên trong sẽ tự động có khả năng được chọn bằng cách nhấn giữ (long press) hoặc kéo chuột (trên desktop/web). Tại sao nó lại quan trọng? Đơn giản thôi: Trải nghiệm người dùng (UX). Trong thế giới số hóa hiện nay, việc copy-paste thông tin là một hành động cơ bản và thiết yếu. Nếu app của mấy đứa mà người dùng không copy được text, họ sẽ cảm thấy khó chịu, giống như đang gõ phím mà bàn phím bị kẹt vậy. Nó làm giảm đi sự tiện lợi, làm mất đi tính "thân thiện" của app. SelectionArea giải quyết triệt để vấn đề này, biến app của mấy đứa từ một "bức tường" thành một "cánh cửa" mở ra sự tương tác. Code Ví Dụ Minh Hoạ Rõ Ràng Nói lý thuyết nhiều quá, giờ anh em mình "flex code" cho nó nóng hổi. Để thấy sức mạnh của SelectionArea, mấy đứa chỉ cần bọc những widget mà mấy đứa muốn cho phép chọn text vào trong nó là xong. Đơn giản như ăn kẹo! import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'SelectionArea Demo by Creyt', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SelectionArea - Copy-Paste Thần Thánh'), ), body: SelectionArea( // Bọc toàn bộ phần thân để mọi text đều có thể chọn child: SingleChildScrollView( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Chào các bạn Gen Z!', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), const Text( 'Đây là một đoạn văn bản mà trước đây bạn không thể chọn và sao chép. Nhưng giờ đây, nhờ có SelectionArea, bạn có thể dễ dàng bôi đen và copy nó. Hãy thử nhấn giữ (long press) hoặc kéo chuột để chọn nhé!', style: TextStyle(fontSize: 16), ), const SizedBox(height: 20), const Text( 'Một đoạn văn khác với thông tin quan trọng:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), const Text( 'Flutter là một framework UI mã nguồn mở do Google tạo ra. Nó được sử dụng để phát triển các ứng dụng đa nền tảng cho Android, iOS, Linux, macOS, Windows, Google Fuchsia và web từ một codebase duy nhất.', style: TextStyle(fontSize: 16), ), const SizedBox(height: 20), const Text( 'Địa chỉ liên hệ: 123 Đường Lập Trình, Quận Code, Thành phố Flutterville.', style: TextStyle(fontSize: 16, fontStyle: FontStyle.italic), ), const SizedBox(height: 50), // Ví dụ về một phần không muốn cho phép chọn Container( color: Colors.grey[200], padding: const EdgeInsets.all(15), child: const Text( 'Phần này được bọc trong một widget khác, nhưng vì nó nằm trong SelectionArea chung, nó vẫn có thể chọn được. Để KHÔNG cho phép chọn, bạn cần bọc nó trong SelectionContainer.disabled.', style: TextStyle(fontSize: 14, color: Colors.red), ), ), const SizedBox(height: 20), // Ví dụ về cách vô hiệu hóa Selection cho một phần cụ thể SelectionContainer.disabled( child: Container( color: Colors.yellow[100], padding: const EdgeInsets.all(15), child: const Text( 'Bạn sẽ không thể chọn đoạn văn bản này vì nó được bọc trong SelectionContainer.disabled. Đây là cách để bảo vệ thông tin mà bạn không muốn người dùng copy.', style: TextStyle(fontSize: 14, color: Colors.blueGrey), ), ), ), ], ), ), ), ); } } Mẹo Vặt (Best Practices) Từ Anh Creyt Để dùng SelectionArea một cách "pro" và hiệu quả, mấy đứa cần nhớ mấy tips sau đây, đây là "kinh nghiệm xương máu" của anh Creyt đó: Bọc càng cao càng tốt: Thường thì, mấy đứa nên bọc SelectionArea ở một cấp độ cao trong widget tree của mình, ví dụ như bọc toàn bộ Scaffold's body hoặc thậm chí là MaterialApp nếu muốn toàn bộ app đều có thể chọn. Điều này đảm bảo mọi văn bản trong app đều có khả năng tương tác. Kiểm soát vùng chọn với SelectionContainer.disabled: Đôi khi, có những đoạn văn bản mấy đứa không muốn cho người dùng copy (ví dụ: mã bảo mật, thông tin nhạy cảm, hoặc đơn giản là một phần UI không phải là nội dung). Lúc này, hãy dùng SelectionContainer.disabled để bọc riêng phần đó lại. Nó giống như đặt một tấm kính cường lực lên một phần của cuốn sách vậy, vẫn nằm trong SelectionArea lớn nhưng không cho phép chọn. Tùy chỉnh giao diện chọn (selectionControls): Mặc định, SelectionArea sẽ dùng giao diện chọn mặc định của nền tảng (Material hoặc Cupertino). Nhưng mấy đứa hoàn toàn có thể tùy chỉnh các "tay cầm" (handles) để chọn, các menu popup (copy, paste, share) bằng cách cung cấp một SelectionControls custom cho SelectionArea. Cái này thì hơi "advanced" một chút, nhưng khi cần "flex" UI độc đáo thì nó là "vũ khí bí mật" đó. Hiệu suất: Đừng lo lắng quá về hiệu suất khi dùng SelectionArea với văn bản thông thường. Nó được tối ưu khá tốt. Tuy nhiên, nếu mấy đứa có hàng ngàn Text widget bên trong một SelectionArea khổng lồ và phức tạp, thì cũng nên cân nhắc một chút. Nhưng với các ứng dụng thông thường, cứ "quất" đi! Nested SelectionAreas: Mấy đứa có thể có nhiều SelectionArea lồng nhau. Khi đó, vùng chọn sẽ được xử lý bởi SelectionArea gần nhất với widget được chọn. Nhưng tốt nhất là nên có một SelectionArea lớn duy nhất và dùng SelectionContainer.disabled để vô hiệu hóa các vùng nhỏ. Ứng Dụng Thực Tế Mấy đứa nghĩ xem, những app nào mà mấy đứa hay copy-paste nhất? App đọc báo, đọc truyện, đọc sách (e.g., Kindle, Medium, VNExpress): Chắc chắn rồi! Người dùng cần trích dẫn, lưu lại những câu văn hay, những tin tức quan trọng. App ghi chú (e.g., Notion, Google Keep): Mấy đứa ghi chú thì đương nhiên phải copy nội dung từ chỗ này sang chỗ khác để sắp xếp, chỉnh sửa chứ. App tài liệu, hướng dẫn (e.g., Google Docs, Wikipedia): Nơi mà thông tin là vàng, việc copy-paste để nghiên cứu, học tập là cực kỳ cần thiết. Các trang web thương mại điện tử (e.g., Shopee, Lazada): Mấy đứa muốn copy tên sản phẩm, mã SKU, hoặc mô tả sản phẩm để tìm kiếm hoặc chia sẻ. SelectionArea chính là cái "linh hồn" thầm lặng giúp những app này trở nên "friendly" hơn rất nhiều. Thử Nghiệm Của Anh Creyt Và Khi Nào Nên Dùng Anh Creyt đã từng "vật lộn" với việc này hồi xưa, khi Flutter chưa có SelectionArea "chuẩn chỉ". Hồi đó, muốn copy text là phải tự "hack" bằng cách dùng GestureDetector rồi Clipboard.setData, mà nó "cùi bắp" lắm, không có cái thanh kéo chọn hay menu popup "xịn sò" như bây giờ đâu. Mãi đến khi SelectionArea ra đời, anh em lập trình viên mới được "giải thoát". Nên dùng cho case nào? Content-heavy apps: Bất kỳ app nào mà nội dung text là trung tâm (news, blogs, docs, e-books). Forms with pre-filled information: Khi mấy đứa hiển thị thông tin mà người dùng có thể muốn copy để dùng ở nơi khác (ví dụ: mã đơn hàng, địa chỉ giao hàng). Debugging/Logging displays: Trong các công cụ debug nội bộ, việc cho phép chọn và copy log rất hữu ích. Không nên dùng cho case nào (hoặc cần cân nhắc): Các phần UI mà việc chọn text không có ý nghĩa: Ví dụ: các nút bấm, icon, hình ảnh (trừ khi mấy đứa muốn copy alt text của hình ảnh, nhưng đó là câu chuyện khác). Thông tin nhạy cảm/bảo mật: Như đã nói, dùng SelectionContainer.disabled để vô hiệu hóa chọn cho những phần này. Đừng để người dùng vô tình copy mật khẩu hay mã OTP trên màn hình của họ nhé! Tóm lại, SelectionArea là một "người bạn đồng hành" không thể thiếu để nâng tầm trải nghiệm người dùng trong app Flutter của mấy đứa. Hãy dùng nó một cách thông minh và linh hoạt để app của mấy đứa không chỉ đẹp mà còn tiện lợi nữa! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

52 Đọc tiếp
Copy-Paste Có Gu: SelectableText.rich – Bật Mode Chép Văn Bản Đa Sắc Màu trong Flutter!
21/03/2026

Copy-Paste Có Gu: SelectableText.rich – Bật Mode Chép Văn Bản Đa Sắc Màu trong Flutter!

Chào các "coder nhí" tương lai của thế giới số! Anh Creyt đây, hôm nay chúng ta sẽ cùng "mổ xẻ" một "viên ngọc" bé nhỏ nhưng cực kỳ quyền năng trong Flutter, đó là SelectableText.rich. Nghe tên thôi đã thấy "sang chảnh" rồi đúng không? Đừng lo, anh sẽ "giải mã" nó dễ hiểu hơn cả việc chơi game mobile vậy. SelectableText.rich: Khi Văn Bản Của Bạn Muốn "Làm Dáng" Và Vẫn Muốn Được "Chép Nguyên"! Các em hình dung thế này: Bình thường, khi các em copy một đoạn văn bản từ đâu đó, nhất là mấy bài viết có màu mè, in đậm, in nghiêng lung tung, thì khi paste ra, ôi thôi! Nó "trở về tuổi thơ" với cái font mặc định, đen thui, xấu hoắc. Giống như các em đi dự tiệc với bộ đồ lộng lẫy, nhưng lúc về nhà lại bị bắt thay đồ ngủ vậy. SelectableText thường của Flutter cũng thế. Nó cho phép người dùng chọn và sao chép văn bản, nhưng chỉ với một kiểu định dạng duy nhất. Nó giống như một chiếc hộp chỉ đựng được một loại bánh thôi vậy. Nhưng SelectableText.rich thì khác bọt hoàn toàn! Nó chính là "chiếc hộp bento cao cấp" cho văn bản của các em. Mỗi "ngăn" trong hộp bento đó (mà trong Flutter gọi là TextSpan) có thể đựng một "món ăn" (một đoạn văn bản) với "hương vị" (định dạng: màu sắc, font chữ, in đậm, in nghiêng) riêng biệt. Và tuyệt vời hơn nữa, người dùng có thể "chọn món" (chọn đoạn văn bản) nào họ thích, và khi "mang về" (copy), món ăn đó vẫn giữ nguyên "hương vị" ban đầu! Không còn cảnh mất định dạng nữa! Tóm lại: SelectableText.rich là một widget cho phép hiển thị một đoạn văn bản phong phú (có nhiều kiểu định dạng khác nhau) và vẫn giữ nguyên khả năng cho phép người dùng chọn (highlight) và sao chép (copy) các phần văn bản đó, bao gồm cả định dạng của chúng. Code Ví Dụ: "Bóc Tem" Ngay "Hộp Bento Văn Bản"! Để các em dễ hình dung, anh Creyt sẽ "phù phép" một đoạn code nhỏ để thấy được sự "thần kỳ" của SelectableText.rich: 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: 'SelectableText.rich Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SelectableText.rich của Creyt'), ), body: Center( child: Padding( padding: const EdgeInsets.all(20.0), child: SelectableText.rich( TextSpan( text: 'Chào các bạn! Đây là ', style: const TextStyle( fontSize: 18, color: Colors.black87, ), children: <TextSpan>[ TextSpan( text: 'một đoạn văn bản ', style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.blue, fontSize: 20, ), ), TextSpan( text: 'siêu cấp ', style: TextStyle( fontStyle: FontStyle.italic, color: Colors.purple.shade700, fontSize: 18, ), ), TextSpan( text: 'có thể chọn ', style: const TextStyle( decoration: TextDecoration.underline, color: Colors.green, fontSize: 18, ), ), TextSpan( text: 'và sao chép ', style: const TextStyle( backgroundColor: Colors.yellowAccent, color: Colors.redAccent, fontSize: 18, ), ), TextSpan( text: 'với nhiều định dạng khác nhau! ', style: const TextStyle( fontFamily: 'RobotoMono', color: Colors.deepOrange, fontSize: 16, ), ), // Ví dụ thêm một TextSpan có thể tương tác (tùy chọn) TextSpan( text: 'Click vào đây!', style: const TextStyle( color: Colors.indigo, decoration: TextDecoration.underline, fontWeight: FontWeight.bold, ), recognizer: TapGestureRecognizer() ..onTap = () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Bạn đã click vào TextSpan này!')), ); }, ), ], ), ), ), ), ); } } Trong ví dụ trên, các em thấy đó, chúng ta dùng TextSpan lồng nhau để tạo ra các phần văn bản với TextStyle riêng biệt. Từ in đậm, in nghiêng, gạch chân, đổi màu, đến cả đổi font chữ hay có nền màu. Và điều "vi diệu" là tất cả những định dạng đó đều được giữ nguyên khi người dùng chọn và copy! Mẹo & Best Practices: "Bí Kíp Võ Lâm" Của Giảng Viên Creyt Khi nào thì "bung lụa" SelectableText.rich? Khi cần văn bản "đa sắc màu": Nếu UI của các em có những đoạn văn bản cần nhiều kiểu định dạng (in đậm, nghiêng, màu mè) và người dùng cần khả năng copy nguyên xi cái "gu" đó. Ví dụ: một bài blog, một đoạn trích dẫn quan trọng, hay một phần hướng dẫn sử dụng. Phân biệt với "anh em": Text: Chỉ để hiển thị, không chọn được. Dùng khi chỉ cần show thông tin tĩnh. Text.rich: Hiển thị văn bản đa dạng định dạng (dùng TextSpan), nhưng cũng không chọn được. Dùng khi cần hiển thị đẹp mà không cần tương tác copy. SelectableText: Cho phép chọn và copy, nhưng chỉ với một kiểu định dạng duy nhất cho toàn bộ văn bản. Dùng khi cần copy văn bản đơn giản. SelectableText.rich: Chính là "người hùng" của chúng ta, kết hợp ưu điểm của Text.rich (hiển thị đa định dạng) và SelectableText (có thể chọn và copy). Dùng khi cần cả hai! Đừng "lạm dụng" quá mức: Mặc dù SelectableText.rich rất mạnh mẽ, nhưng đừng dùng nó cho mọi thứ. Nếu chỉ là một đoạn văn bản đơn giản, không cần chọn hay chỉ có một style, hãy dùng Text hoặc SelectableText thường thôi. "Đao to búa lớn" quá đôi khi lại "phản tác dụng" hoặc làm code phức tạp không cần thiết. Tối ưu hóa "sức khỏe": Với TextSpan lồng nhau, đôi khi việc render có thể tốn tài nguyên hơn một chút so với Text đơn giản. Tuy nhiên, trong hầu hết các trường hợp sử dụng thông thường, các em sẽ không cảm thấy sự khác biệt đáng kể. Chỉ cần lưu ý nếu các em đang hiển thị hàng ngàn TextSpan cùng lúc (mà điều này hiếm khi xảy ra). "Nhân văn" với Accessibility: Luôn đảm bảo TextStyle của các em có độ tương phản màu tốt, font size đủ lớn để mọi người, kể cả những người có thị lực kém, cũng có thể đọc và tương tác dễ dàng. Đây là "điểm cộng" rất lớn cho app của các em đó! Tùy chỉnh hành vi chọn: Các em có thể "đào sâu" hơn bằng cách dùng các thuộc tính như onSelectionChanged để biết khi nào người dùng thay đổi vùng chọn, hay selectionControls để tùy chỉnh giao diện của các "tay cầm" chọn văn bản (ví dụ: đổi màu, đổi hình dạng). Ứng Dụng Thực Tế: "Show Hàng" Các App Đã Dùng! Các em có thể thấy SelectableText.rich hoặc các cơ chế tương tự được áp dụng ở đâu? Ứng dụng đọc tin tức/blog (Medium, VnExpress, Kipalog...): Khi các em đọc một bài viết có nhiều đoạn in đậm, trích dẫn, và muốn copy một phần nào đó để chia sẻ, nó thường giữ nguyên định dạng. SelectableText.rich là một ứng cử viên sáng giá cho việc này. Ứng dụng ghi chú/tài liệu (Google Keep, Notion, Evernote): Các em ghi chú với highlight, in đậm, in nghiêng. Khi cần copy một đoạn ghi chú để dán vào email hay một app khác, việc giữ nguyên định dạng là cực kỳ quan trọng. Ứng dụng hiển thị tài liệu hướng dẫn/FAQ: Những trang này thường có các phần câu hỏi/trả lời với các từ khóa được in đậm, và người dùng có thể muốn copy câu trả lời nguyên văn. Thử Nghiệm & Nên Dùng Cho Case Nào? Thử nghiệm: Anh khuyến khích các em hãy "vọc vạch" ngay với ví dụ code trên. Thay đổi các TextStyle, thêm bớt TextSpan, thử các thuộc tính như textAlign, textDirection. Hãy thử copy đoạn văn bản từ app của mình và paste vào một trình soạn thảo văn bản bất kỳ để xem nó có giữ nguyên định dạng không nhé! Nên dùng cho case nào? Hiển thị nội dung "rich text" mà người dùng cần tương tác sao chép: Đây là trường hợp "chuẩn bài" nhất. Các trang "About Us", "Terms & Conditions", "Privacy Policy": Những trang này thường có nhiều định dạng và người dùng có thể muốn copy các điều khoản cụ thể. Thông báo, hướng dẫn chi tiết: Khi các em cần hiển thị thông báo quan trọng với các phần được nhấn mạnh và người dùng có thể muốn lưu lại. Nhớ nhé các "dev tương lai", SelectableText.rich không chỉ giúp app của các em đẹp hơn, mà còn "nâng tầm" trải nghiệm người dùng lên một level mới. Hãy "thuần hóa" nó và biến nó thành "vũ khí" lợi hại trong kho tàng Flutter của mình! Hẹn gặp lại trong những bài học "chất như nước cất" lần sau! 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é!

47 Đọc tiếp
SelectableText: Khi chữ không còn 'bất động' trong app Flutter!
21/03/2026

SelectableText: Khi chữ không còn 'bất động' trong app Flutter!

Chào các "dev-er" Gen Z, Bạn đã bao giờ tức điên khi lướt app, thấy cái đoạn text rõ ràng mồn một trên màn hình mà không tài nào copy được chưa? Kiểu như, "Ê, thông tin ngay trước mắt mà sao tui không 'tóm' được vậy?" Cảm giác như bị trêu ngươi phải không? Đó chính là lúc SelectableText của Flutter bước ra sân khấu như một vị cứu tinh! Giảng viên Creyt cam đoan, sau bài này, bạn sẽ làm chủ siêu năng lực biến chữ trên app thành "chữ sống", sẵn sàng để người dùng "tóm" lấy và mang đi bất cứ đâu. 1. SelectableText là gì và để làm gì? Hiểu đơn giản, SelectableText là một widget sinh ra để giải quyết nỗi đau muôn thuở: cho phép người dùng chọn (select) và sao chép (copy) nội dung văn bản ngay trong ứng dụng của bạn. Nó giống như bạn có một cuốn sách giấy bình thường (widget Text mặc định) thì chỉ có thể đọc thôi. Nhưng khi bạn "biến hình" nó thành SelectableText, cuốn sách đó bỗng có thêm tính năng highlight và copy y hệt như bạn đang đọc một tài liệu PDF vậy. Người dùng có thể chạm giữ, kéo để chọn đoạn văn bản họ muốn, và một menu nhỏ sẽ hiện ra cho phép họ sao chép. Tại sao lại cần nó? Vì đôi khi, người dùng không chỉ muốn đọc. Họ muốn lưu lại một câu nói hay, một đoạn code, một địa chỉ email, hay đơn giản là một dòng OTP mà bạn hiển thị. SelectableText chính là cây cầu nối giữa thông tin bạn cung cấp và nhu cầu tương tác của người dùng. 2. Code Ví Dụ Minh Hoạ Rõ Ràng Để dễ hình dung, chúng ta hãy đặt cạnh nhau một widget Text thông thường và một SelectableText để thấy sự khác biệt 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: 'SelectableText Demo của Creyt', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('SelectableText: Sức mạnh của sự tương tác'), ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Đây là một đoạn text KHÔNG THỂ chọn và copy. Hãy thử chạm giữ xem!', textAlign: TextAlign.center, style: TextStyle(fontSize: 18, color: Colors.redAccent), ), const SizedBox(height: 40), const SelectableText( 'Đây là một đoạn text CÓ THỂ chọn và copy. Chạm giữ và trải nghiệm sự khác biệt!', textAlign: TextAlign.center, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.green, ), // Một số thuộc tính tùy chỉnh khác của SelectableText cursorColor: Colors.blue, showCursor: true, cursorWidth: 2.0, // Khi có sự thay đổi trong vùng chọn onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { print('Vùng chọn đã thay đổi: ${selection.textInside(this.toString())}'); }, ), const SizedBox(height: 40), const SelectableText.rich( TextSpan( text: 'Bạn cũng có thể dùng ', style: TextStyle(fontSize: 16, color: Colors.black87), children: <TextSpan>[ TextSpan( text: 'SelectableText.rich ', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.purple), ), TextSpan( text: 'để tạo văn bản đa phong cách và vẫn copy được!', ), ], ), textAlign: TextAlign.center, ), ], ), ), ), ); } } Trong ví dụ trên, bạn sẽ thấy rõ: đoạn văn bản màu đỏ là Text thông thường, bạn không thể làm gì với nó ngoài việc đọc. Còn đoạn màu xanh lá cây và tím là SelectableText, bạn có thể chạm giữ, kéo để chọn và sau đó sao chép (copy) hoặc cắt (cut) tuỳ theo menu ngữ cảnh của hệ điều hành. 3. Mẹo Hay (Best Practices) từ Giảng viên Creyt Dùng đúng chỗ, đúng lúc: Không phải text nào cũng cần SelectableText. Các tiêu đề lớn, label của button, hoặc những đoạn text mang tính trang trí thì không nên dùng. Hãy tưởng tượng bạn cố chọn tiêu đề của một cuốn sách – hơi vô nghĩa đúng không? Chỉ dùng khi người dùng thực sự có nhu cầu tương tác (chọn, copy) với nội dung đó. Tùy chỉnh cursorColor, showCursor, cursorWidth: Để trải nghiệm chọn text mượt mà và đẹp mắt, bạn có thể tùy chỉnh màu sắc và độ dày của con trỏ. Điều này giúp app của bạn trông "pro" hơn nhiều. onSelectionChanged: Đây là một callback cực kỳ hữu ích! Nó cho phép bạn biết khi nào người dùng bắt đầu chọn, thay đổi vùng chọn, hoặc kết thúc việc chọn. Bạn có thể dùng nó để ghi log, hoặc thậm chí là kích hoạt một hành động khác dựa trên văn bản được chọn. SelectableText.rich cho văn bản phức tạp: Nếu bạn cần hiển thị văn bản với nhiều phong cách (in đậm, nghiêng, màu sắc khác nhau) nhưng vẫn muốn nó có thể chọn được, SelectableText.rich với TextSpan là "chân ái" đấy. Nó giống như bạn có một bức tranh ghép từ nhiều mảnh nhỏ, nhưng vẫn có thể "chộp" lấy cả bức tranh một cách dễ dàng. Tránh lạm dụng trong ListView/GridView lớn: Nếu bạn có một danh sách cực dài các SelectableText trong ListView hoặc GridView, đôi khi có thể ảnh hưởng nhẹ đến hiệu suất. Hãy cân nhắc nếu thực sự cần thiết cho mọi item. 4. Ứng dụng Thực Tế: Ai đã dùng SelectableText? Bạn có thể thấy ý tưởng của SelectableText (hoặc các cơ chế tương tự) ở khắp mọi nơi trong các ứng dụng hàng ngày: Ứng dụng ghi chú (Evernote, Google Keep): Bạn muốn copy một đoạn ghi chú quan trọng. Ứng dụng đọc sách/tin tức (Kindle, Google News): Highlight một câu nói hay, copy một đoạn văn để chia sẻ. Ứng dụng nhắn tin/mạng xã hội (Zalo, Facebook Messenger): Chạm giữ tin nhắn để sao chép nội dung. Các trang web, blog, tài liệu: Bất cứ nơi nào có nội dung chữ viết mà bạn muốn người dùng có thể dễ dàng lấy thông tin. 5. Thử nghiệm và Nên dùng cho Case nào? Thử nghiệm đã từng: Giảng viên Creyt đã từng "đau đầu" khi phát triển một ứng dụng tài liệu nội bộ. Ban đầu dùng Text và nhận vô số feedback kiểu "sao không copy được anh ơi?". Sau khi chuyển sang SelectableText, mọi người "mừng như bắt được vàng". Bài học rút ra là: đừng đánh giá thấp nhu cầu cơ bản của người dùng! Nên dùng cho các case: Hiển thị nội dung dài, chi tiết: Các bài viết, mô tả sản phẩm, điều khoản dịch vụ, FAQ. Thông tin cần sao chép nhanh: Mã OTP, mã giảm giá, địa chỉ email, số điện thoại, mật khẩu tạm thời. Ứng dụng giáo dục hoặc tài liệu: Cho phép sinh viên/người đọc dễ dàng trích dẫn, sao chép thông tin để học tập hoặc nghiên cứu. Nơi người dùng có thể muốn chia sẻ nội dung: Cho phép họ copy một phần nội dung để dán vào ứng dụng khác hoặc chia sẻ qua mạng xã hội. Không nên dùng cho các case: Text trên các nút bấm (Buttons): Người dùng muốn bấm, không muốn chọn. Text trang trí hoặc không có ý nghĩa khi sao chép: Ví dụ: "Chào mừng bạn đến với ứng dụng!" - ít ai muốn copy câu này. Text là một phần của hình ảnh hoặc biểu tượng: SelectableText chỉ làm việc với văn bản. Vậy là xong! SelectableText tuy nhỏ mà có võ, phải không các "dev-er"? Hãy dùng nó một cách thông minh để nâng tầm trải nghiệm người dùng trong app Flutter của bạn nhé. Hẹn gặp lại trong những bài học "chất như nước cất" lần sau! 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
ScrollableState: 'Hack' Cuộn Trang Flutter Cực Chất Với Anh Creyt!
21/03/2026

ScrollableState: 'Hack' Cuộn Trang Flutter Cực Chất Với Anh Creyt!

Ê mấy đứa, hôm nay anh Creyt lại mang đến một món đồ chơi "hack não" nhưng mà cực kỳ "chill" trong thế giới Flutter đây: ScrollableState! ScrollableState là gì mà "ghê gớm" vậy anh Creyt? Tưởng tượng nha, cái màn hình điện thoại của tụi bây bé tí, mà nội dung thì dài dằng dặc như cuốn tiểu thuyết ngôn tình 1000 chương vậy. Để xem hết, tụi bây phải "cuộn", đúng không? Cái thằng ScrollableState này chính là 'linh hồn', là 'bộ não' đằng sau tất cả những thứ có thể cuộn trong app Flutter của tụi bây. Từ cái ListView lướt feed Facebook, GridView xem ảnh Instagram, hay thậm chí là SingleChildScrollView để đọc một bài blog dài ngoằng – tất cả đều có một ScrollableState ngầm điều hành. Nó là cái 'trạng thái nội bộ' (internal state) mà Flutter dùng để quản lý mọi thứ liên quan đến việc cuộn. Nó giống như cái camera trong game nhập vai của tụi bây vậy, luôn biết nhân vật đang ở đâu, nhìn về hướng nào để hiển thị cảnh quan cho đúng. Nó làm được những gì? ScrollableState không chỉ đơn thuần là biết 'mày đang ở đâu' trên cái trang cuộn đó đâu. Không, nó 'pro' hơn nhiều! Nó nắm giữ toàn bộ thông tin về: Vị trí hiện tại của cuộn (scroll offset): Đang ở pixel thứ mấy từ đầu trang. Tốc độ cuộn: Nhanh hay chậm. Hướng cuộn: Đang cuộn lên hay xuống. Giới hạn cuộn (scroll extent): Tổng chiều dài có thể cuộn được. Vật lý cuộn (scroll physics): Các hiệu ứng khi cuộn chạm biên (như kéo giãn, nảy lên). Nói chung là, mọi thứ liên quan đến việc 'di chuyển' nội dung trên màn hình, ScrollableState đều biết tuốt. Làm sao để "nói chuyện" với ScrollableState? Nhưng mà, cái ScrollableState này nó hơi "khép kín", nó là "internal state" của hệ thống, mình không thể trực tiếp "nói chuyện" với nó được. Vậy làm sao để mình "ra lệnh" cho nó, hay "hỏi" nó xem đang cuộn đến đâu? Đó là lúc "người đại diện" của nó xuất hiện: ScrollController! Coi nó như cái "remote control" vạn năng của tụi bây vậy. Cứ gắn ScrollController vào bất kỳ widget nào có thể cuộn được (như ListView, GridView, SingleChildScrollView), là tụi bây có thể bắt đầu "flex" với nó rồi. Với ScrollController, tụi bây có thể: Kiểm tra vị trí cuộn hiện tại: Biết người dùng đang ở đâu. Cuộn đến một vị trí cụ thể: Dùng animateTo (có hiệu ứng) hoặc jumpTo (tức thì). Lắng nghe sự kiện cuộn: Biết khi nào người dùng bắt đầu cuộn, dừng cuộn, hay cuộn đến cuối trang. Code Ví Dụ Minh Hoạ "Sương Sương" Để dễ hình dung, anh Creyt sẽ làm một ví dụ đơn giản: một danh sách dài và một nút "Lên đầu trang" (Back to Top) chỉ hiện ra khi tụi bây cuộn xuống một đoạn nhất đị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: 'ScrollableState Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const ScrollableStateScreen(), ); } } class ScrollableStateScreen extends StatefulWidget { const ScrollableStateScreen({super.key}); @override State<ScrollableStateScreen> createState() => _ScrollableStateScreenState(); } class _ScrollableStateScreenState extends State<ScrollableStateScreen> { // 1. Khởi tạo ScrollController final ScrollController _scrollController = ScrollController(); bool _showBackToTopButton = false; // Biến để ẩn/hiện nút "Lên đầu trang" @override void initState() { super.initState(); // 2. Lắng nghe sự kiện cuộn _scrollController.addListener(() { // Khi người dùng cuộn xuống quá 200 pixel, hiện nút if (_scrollController.position.pixels >= 200 && !_showBackToTopButton) { setState(() { _showBackToTopButton = true; }); } // Khi người dùng cuộn lên trên 200 pixel, ẩn nút else if (_scrollController.position.pixels < 200 && _showBackToTopButton) { setState(() { _showBackToTopButton = false; }); } // debugPrint('Vị trí cuộn: ${_scrollController.position.pixels}'); }); } @override void dispose() { // 3. Luôn luôn dispose ScrollController khi không dùng nữa _scrollController.dispose(); super.dispose(); } // Hàm cuộn lên đầu trang void _scrollToTop() { _scrollController.animateTo( 0, // Cuộn về vị trí 0 (đầu trang) duration: const Duration(milliseconds: 500), // Trong 0.5 giây curve: Curves.easeInOut, // Với hiệu ứng mượt mà ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ScrollableState Demo'), ), body: ListView.builder( // 4. Gắn ScrollController vào ListView controller: _scrollController, itemCount: 100, // Danh sách có 100 mục itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.all(8.0), child: Card( elevation: 4, child: ListTile( leading: CircleAvatar(child: Text('${index + 1}')), title: Text('Mục số ${index + 1}'), subtitle: const Text('Đây là nội dung của một mục trong danh sách dài.'), ), ), ); }, ), // Nút "Lên đầu trang" chỉ hiện khi _showBackToTopButton là true floatingActionButton: _showBackToTopButton ? FloatingActionButton( onPressed: _scrollToTop, child: const Icon(Icons.arrow_upward), ) : null, // Nếu không thì ẩn đi ); } } Mẹo Hay và Best Practices từ Anh Creyt: "Dọn dẹp" sau khi chơi: Luôn nhớ gọi _scrollController.dispose() trong hàm dispose() của StatefulWidget để tránh rò rỉ bộ nhớ. Coi như chơi xong thì cất đồ chơi vào hộp vậy, gọn gàng, sạch sẽ. Đừng "overkill": Nếu mục đích của tụi bây chỉ là biết người dùng cuộn đến đâu để ẩn/hiện một cái AppBar hay BottomNavigationBar mà không cần điều khiển cuộn, thì đôi khi NotificationListener lại là lựa chọn "chill" hơn, đỡ phải tạo ScrollController lằng nhằng. Nó giống như nghe "ngóng" tiếng động xung quanh hơn là trực tiếp điều khiển vậy. "Flex" với ScrollPhysics: Muốn cuộn mượt mà như iOS hay "nảy" như Android? ScrollController cho phép tụi bây tùy chỉnh ScrollPhysics để tạo ra trải nghiệm cuộn độc đáo, đúng "vibe" app của mình. Ví dụ, BouncingScrollPhysics() cho iOS-like bounce, ClampingScrollPhysics() cho Android-like clamp. Cẩn thận với jumpTo và animateTo: jumpTo thì tức thì, phù hợp cho việc nhảy đến một vị trí ngay lập tức (ví dụ: chuyển tab). animateTo thì mượt mà hơn, có hiệu ứng chuyển động, nhưng tốn thời gian. Chọn cái nào tùy vào tình huống nhé, đừng để người dùng "giật mình" với jumpTo khi không cần thiết. Ứng Dụng Thực Tế "Hơi Bị Xịn" của ScrollableState: Nút "Lên đầu trang" (Back to Top): Như ví dụ trên, đây là ứng dụng phổ biến nhất. Các app như Instagram, Facebook, Shopee đều có nút này khi cuộn xuống quá sâu. Hiệu ứng Parallax: Khi ảnh nền di chuyển chậm hơn nội dung chính, tạo cảm giác chiều sâu. Các trang web "chất chơi" hay app giới thiệu sản phẩm thường dùng cái này. ScrollController sẽ cung cấp offset để tính toán vị trí của các layer khác nhau. Infinite Scroll (Cuộn vô tận): Tự động tải thêm nội dung khi người dùng cuộn gần đến cuối danh sách (ví dụ: feed của TikTok, Facebook, Twitter). ScrollController giúp tụi bây biết được khi nào cần "kêu gọi" API để load thêm data. Auto-play video khi cuộn đến: Các app video ngắn như TikTok hay YouTube Shorts sẽ tự động phát video khi nó xuất hiện trên màn hình, và tạm dừng khi cuộn đi. ScrollController ở đây đóng vai trò là "sensor" nhận biết vị trí và ra lệnh cho trình phát media. Kinh Nghiệm "Xương Máu" của Anh Creyt và Nên Dùng Khi Nào? "Anh Creyt từng 'hack' cái vụ cuộn này để tạo một cái danh sách sản phẩm vô tận (infinite scroll), cứ cuộn gần đến cuối là nó tự động load thêm sản phẩm mới. Hồi đó dùng ScrollController để lắng nghe _scrollController.position.pixels == _scrollController.position.maxScrollExtent đó. Đỉnh của chóp luôn!" "Hoặc có lần, anh cần một cái AppBar ẩn đi khi cuộn xuống và hiện ra khi cuộn lên. Thay vì dùng SliverAppBar (cái này dễ rồi), anh 'chơi lớn' dùng ScrollController để tự tay setState cho opacity của AppBar dựa vào hướng cuộn. Hơi 'hack não' tí nhưng mà hiểu sâu hơn về cơ chế cuộn và cho phép tùy biến không giới hạn." Khi nào nên dùng ScrollController? Khi tụi bây cần điều khiển trực tiếp việc cuộn (cuộn đến vị trí X, cuộn lên đầu). Khi tụi bây muốn lắng nghe chính xác vị trí cuộn, hướng cuộn, hoặc trạng thái cuộn để thực hiện các hành động phức tạp (như infinite scroll, parallax, hiệu ứng tùy chỉnh). Khi tụi bây muốn thay đổi vật lý cuộn của một widget cụ thể. Nói chung, khi nào tụi bây muốn app của mình "thông minh" hơn, "phản ứng" với thao tác cuộn của người dùng, hoặc muốn "điều khiển" việc cuộn một cách chủ động, thì cứ nhớ đến thằng ScrollController và cái ScrollableState đằng sau nó nhé. Đừng ngại thử nghiệm, cứ "code" đi rồi "fix" sau! Chúc tụi bây "flex" với Flutter vui vẻ! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

44 Đọc tiếp
Flutter Navigation: 'RestorableRoutePushReplacement' - Sếp Creyt giải mã!
21/03/2026

Flutter Navigation: 'RestorableRoutePushReplacement' - Sếp Creyt giải mã!

Chào các 'dev-er' GenZ năng động! Anh là Creyt đây. Hôm nay, chúng ta sẽ cùng 'mổ xẻ' một khái niệm nghe có vẻ hơi 'hack não' nhưng lại cực kỳ quyền năng trong Flutter: RestorableRoutePushReplacement. 1. RestorableRoutePushReplacement: Phép thuật 'Time-traveling Navigation' là gì? Đầu tiên, hãy tưởng tượng ứng dụng của các em là một chuyến phiêu lưu, và mỗi màn hình là một điểm dừng chân. Khi các em dùng Navigator.pushReplacement, nó giống như việc các em đến một điểm dừng mới, và 'xóa sạch' dấu vết của điểm dừng trước đó khỏi bản đồ hành trình của mình. Tức là, các em không thể 'quay lại' điểm cũ bằng nút back nữa. Thẳng tiến về phía trước! Nhưng cuộc đời đâu phải lúc nào cũng suôn sẻ, đúng không? Đôi khi, ứng dụng của chúng ta bị 'crash' hoặc bị hệ điều hành 'giết chết' để giải phóng bộ nhớ (nhất là trên mobile). Khi ứng dụng khởi động lại, các em muốn nó 'nhớ' được mình đang ở đâu và trạng thái của màn hình đó như thế nào. Đây chính là lúc RestorableRoutePushReplacement tỏa sáng! Nó không chỉ là một cú pushReplacement thông thường. Nó là một cú pushReplacement có 'trí nhớ siêu phàm'. Để làm gì? Nói một cách đơn giản, khi các em dùng Navigator.restorablePushReplacement, các em đang nói với Flutter rằng: "Này, tôi vừa thay thế màn hình hiện tại bằng một màn hình mới. Nếu ứng dụng của tôi mà 'chết lâm sàng' rồi tỉnh lại, hãy đảm bảo rằng bạn biết tôi đã ở màn hình này (màn hình mới) và bạn có thể khôi phục lại trạng thái của nó một cách chính xác!". Nó đặc biệt hữu ích khi các em muốn đảm bảo rằng trạng thái của ứng dụng (ví dụ: một giá trị trong form, vị trí cuộn, hoặc một lựa chọn nào đó) vẫn được giữ nguyên sau khi ứng dụng bị tắt và khởi động lại, ngay cả khi các em đã thay đổi luồng điều hướng bằng pushReplacement. 2. Code Ví Dụ Minh Họa: 'Hack' Trí Nhớ Ứng Dụng Để minh họa, chúng ta sẽ tạo một ứng dụng đơn giản với hai màn hình: HomeScreen và DetailScreen. HomeScreen sẽ có một bộ đếm RestorableInt. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( restorationScopeId: 'app', title: 'Restorable Route Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), routes: { '/home': (context) => const HomeScreen(), '/detail': (context) => const DetailScreen(), }, ); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> with RestorationMixin { // Khai báo một RestorableProperty để giữ trạng thái của bộ đếm final RestorableInt _counter = RestorableInt(0); @override String? get restorationId => 'homeScreen'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_counter, 'counter'); } @override void dispose() { _counter.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Home Screen')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Counter: ${_counter.value}', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() { _counter.value++; }); }, child: const Text('Increment Counter'), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Dùng restorablePushReplacementNamed để thay thế màn hình // và đảm bảo trạng thái navigation được khôi phục Navigator.restorablePushReplacementNamed( context, '/detail', ); }, child: const Text('Go to Detail (with Restorable Push Replacement)'), ), ], ), ), ); } } class DetailScreen extends StatefulWidget { const DetailScreen({super.key}); @override State<DetailScreen> createState() => _DetailScreenState(); } class _DetailScreenState extends State<DetailScreen> with RestorationMixin { final RestorableString _message = RestorableString('Hello from Detail!'); @override String? get restorationId => 'detailScreen'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_message, 'message'); } @override void dispose() { _message.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Detail Screen')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _message.value, style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { setState(() { _message.value = 'Message updated!'; }); }, child: const Text('Update Message'), ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Quay về Home. Vì Home đã bị 'replace', nút back sẽ không hoạt động // Chúng ta phải pushNamed lại Home nếu muốn quay về. // Nhưng cái hay là, nếu app bị kill và restore, nó sẽ nhớ bạn đã ở Detail. Navigator.restorablePushNamed(context, '/home'); }, child: const Text('Go back to Home'), ), ], ), ), ); } } Cách kiểm tra: Chạy ứng dụng. Trên HomeScreen, tăng bộ đếm lên vài lần (ví dụ: 3). Nhấn nút "Go to Detail (with Restorable Push Replacement)". Các em sẽ thấy DetailScreen. Quan trọng: Buộc ứng dụng dừng lại (ví dụ: dùng "Don't keep activities" trên Android Developer Options, hoặc kill app từ Task Manager trên iOS/Android). Mở lại ứng dụng. Các em sẽ thấy ứng dụng khởi động lại và trực tiếp hiển thị DetailScreen, không phải HomeScreen ban đầu. Điều này chứng tỏ RestorationManager đã nhớ rằng DetailScreen là màn hình cuối cùng sau khi HomeScreen bị thay thế. Nếu các em quay lại HomeScreen từ DetailScreen (bằng nút 'Go back to Home'), các em sẽ thấy bộ đếm trên HomeScreen đã được khôi phục về giá trị 3 (hoặc giá trị cuối cùng trước khi các em rời đi), chứng tỏ RestorableInt đã hoạt động đúng. 3. Mẹo (Best Practices) để Ghi nhớ & Dùng Thực tế Khi nào dùng? Khi các em có một luồng điều hướng mà việc thay thế màn hình là một phần quan trọng của logic ứng dụng (ví dụ: sau khi đăng nhập thành công, thay thế màn hình đăng nhập bằng màn hình chính), VÀ các em muốn đảm bảo rằng trạng thái của màn hình mới (hoặc các màn hình có thể truy cập được từ đó) được khôi phục chính xác nếu ứng dụng bị 'chết' và khởi động lại. Nó giúp duy trì context điều hướng một cách bền vững. Đừng lạm dụng! Không phải mọi pushReplacement đều cần restorable. Chỉ dùng khi các em thực sự cần khả năng khôi phục trạng thái ứng dụng qua các lần tắt/mở lại, đặc biệt là khi các em có các RestorableProperty trên các màn hình liên quan. RestorationId là chìa khóa: Nhớ đặt restorationId duy nhất cho MaterialApp, RestorationScope và từng StatefulWidget có dùng RestorationMixin để Flutter biết phải khôi phục cái gì ở đâu. Kiểm tra kỹ lưỡng: Cách tốt nhất để kiểm tra là mô phỏng việc ứng dụng bị hệ điều hành 'giết' (như đã hướng dẫn ở trên) thay vì chỉ tắt và mở lại thông thường. 4. Ứng dụng/Website đã ứng dụng Thực tế, RestorableRoutePushReplacement không phải là một tính năng 'nhìn thấy' được trực tiếp trên giao diện người dùng như một nút bấm hay hiệu ứng. Nó là một phần của kiến trúc nền tảng, giúp ứng dụng trở nên mạnh mẽ và đáng tin cậy hơn. Bất kỳ ứng dụng di động nào có: Flow đăng nhập/đăng ký phức tạp: Sau khi đăng nhập, màn hình login bị thay thế bởi màn hình Home. Nếu ứng dụng bị kill, người dùng không muốn thấy màn hình login nữa mà muốn được đưa thẳng về Home với trạng thái cuối cùng. Deep linking (liên kết sâu): Khi người dùng nhấp vào một liên kết sâu đưa họ vào một màn hình cụ thể trong ứng dụng, có thể luồng điều hướng sẽ pushReplacement một phần stack. RestorableRoutePushReplacement sẽ đảm bảo rằng trạng thái sau khi deep link được khôi phục nếu ứng dụng bị ngắt. Các ứng dụng có nhiều bước nhập liệu hoặc cấu hình: Ví dụ, các ứng dụng ngân hàng, ứng dụng chỉnh sửa ảnh với nhiều bước, nơi việc mất dữ liệu giữa chừng là không thể chấp nhận được. Các ứng dụng lớn như Google Maps, Facebook, Instagram (phiên bản mobile) đều sử dụng các cơ chế khôi phục trạng thái tương tự để đảm bảo trải nghiệm người dùng liền mạch, ngay cả khi ứng dụng bị tắt bất ngờ. 5. Thử nghiệm và Nên dùng cho Case nào Thử nghiệm đã từng: Anh đã từng gặp các dự án Flutter mà khách hàng than phiền rằng "Sao ứng dụng của tôi cứ bị reset về màn hình chính sau khi tôi chuyển sang ứng dụng khác một lúc?". Hóa ra là do họ không tận dụng RestorationManager và các phương thức restorable* của Navigator. Khi ứng dụng bị kill, toàn bộ trạng thái bị mất. Nên dùng cho Case nào: Luồng xác thực (Authentication Flow): Sau khi người dùng đăng nhập thành công, các em thường Navigator.pushReplacement màn hình đăng nhập bằng màn hình chính. Nếu ứng dụng bị kill, các em muốn người dùng quay lại màn hình chính, chứ không phải màn hình đăng nhập trống rỗng. Thanh toán nhiều bước (Multi-step Checkout): Giả sử người dùng đang ở bước cuối cùng của thanh toán, ứng dụng bị kill. Khi mở lại, các em muốn họ quay lại chính bước đó, không phải bắt đầu lại từ đầu. Cấu hình ứng dụng ban đầu (Onboarding/Setup): Sau khi hoàn thành quá trình onboarding, các em thay thế nó bằng màn hình chính. Đảm bảo trạng thái này được lưu để người dùng không phải onboarding lại. Nhớ nhé, các dev-er! RestorableRoutePushReplacement không chỉ là một dòng code, nó là một lời hứa về sự bền bỉ và trải nghiệm người dùng không gián đoạn cho ứng dụng của các em. Hãy dùng nó một cách thông minh và có chiến lược để 'hack' trí nhớ của ứng dụng một cách hiệu quả 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é!

56 Đọc tiếp
RestorableRouteFuture: Hồi Sinh Chuyến Đi App Của Gen Z!
21/03/2026

RestorableRouteFuture: Hồi Sinh Chuyến Đi App Của Gen Z!

Chào các đệ tử mê code, anh Creyt đây! Hôm nay chúng ta sẽ cùng "đào" một khái niệm nghe hơi "nghiêm trọng" nhưng lại cực kỳ "chill phết" khi hiểu rõ: RestorableRouteFuture trong Flutter. Nghe cái tên đã thấy có vibe "cứu vớt" rồi đúng không? RestorableRouteFuture là gì và để làm gì? Tưởng tượng thế này, các bạn đang "lướt phím" trên một ứng dụng Flutter nào đó, ví dụ như đang điền một cái form đăng ký dài "thượt" hoặc đang trong quá trình thanh toán online phức tạp. Bỗng dưng, điện thoại của bạn báo pin yếu, hoặc bạn mở quá nhiều app, và "BÙM!", hệ điều hành quyết định "kill" ứng dụng của bạn để giải phóng bộ nhớ. Khi bạn mở lại app, "cái nư" của bạn là muốn nó quay lại đúng cái màn hình bạn đang dang dở, đúng không? Chứ đâu muốn nó về lại trang chủ, rồi lại phải điền lại từ đầu, "ô dề" cực! Đó chính là lúc RestorableRouteFuture xuất hiện như một "siêu anh hùng" của sự kiên nhẫn người dùng. Nó không chỉ là một cái "bookmark" đơn thuần. Nó giống như một cỗ máy thời gian, ghi nhớ không chỉ "bạn đang ở đâu" mà còn "bạn đang chờ đợi điều gì từ chuyến đi đó" (tức là cái Future mà route đó trả về). Khi app bị "hồi sinh", nó sẽ biết cách đưa bạn trở lại đúng cái "ngã ba đường" đó, và thậm chí còn tiếp tục chờ đợi kết quả như thể chưa hề có cuộc chia ly! Nói một cách "học thuật" hơn, RestorableRouteFuture là một loại RestorableProperty trong Flutter, chuyên dùng để lưu trữ và khôi phục trạng thái của một Future liên quan đến việc điều hướng (navigation). Đặc biệt hữu ích khi bạn dùng Navigator.push mà có await để chờ kết quả từ màn hình tiếp theo (ví dụ: showDialog để chọn ngày, push sang màn hình chọn sản phẩm rồi trả về sản phẩm đã chọn). Nó giúp ứng dụng của bạn có khả năng "hồi phục" (state restoration) một cách mượt mà sau khi bị hệ điều hành "giết" đi và khởi động lại. Code Ví Dụ Minh Họa: "Cuộc Phiêu Lưu Của Một Lựa Chọn" Để "flex" cái sự mạnh mẽ của RestorableRouteFuture, anh em mình cùng xây dựng một app nhỏ nhé. App này có 2 màn hình: màn hình chính và màn hình chọn một thứ gì đó (ví dụ, chọn một con số). Màn hình chính sẽ chờ kết quả từ màn hình chọn. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( restorationScopeId: 'app_restoration_id', // Rất quan trọng cho restoration title: 'RestorableRouteFuture Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const HomeScreen(), ); } } // Màn hình thứ hai: Chọn một giá trị class SelectionScreen extends StatelessWidget { const SelectionScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Chọn Con Số Yêu Thích')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Chọn một con số để gửi về màn hình chính:'), const SizedBox(height: 20), ElevatedButton( onPressed: () => Navigator.of(context).pop(42), // Trả về số 42 child: const Text('Chọn 42'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(100), // Trả về số 100 child: const Text('Chọn 100'), ), ], ), ), ); } } // Màn hình chính: Chờ đợi kết quả từ SelectionScreen class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> with RestorationMixin { // 1. Khai báo RestorableRouteFuture // Nó sẽ lưu trữ "tương lai" của việc push route final RestorableRouteFuture<int?> _selectionRoute = RestorableRouteFuture<int?>(onPresent: (navigator, arguments) { // Hàm này được gọi khi route cần được "hiện diện" lại return navigator.push<int?>( MaterialPageRoute(builder: (context) => const SelectionScreen())); }); // Một RestorableProperty để lưu kết quả đã chọn final RestorableIntN _selectedNumber = RestorableIntN(null); @override String? get restorationId => 'home_screen_restoration_id'; // ID cho màn hình @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { // 2. Đăng ký RestorableProperty registerForRestoration(_selectionRoute, 'selection_route_future'); registerForRestoration(_selectedNumber, 'selected_number'); // 3. Xử lý kết quả khi RestorableRouteFuture hoàn thành _selectionRoute.addListener(() { if (_selectionRoute.status == RestorableRouteFutureStatus.present) { // Nếu route đang được hiển thị lại, chúng ta chờ kết quả _selectionRoute.value!.then((value) { if (value != null) { setState(() { _selectedNumber.value = value; }); } }); } else if (_selectionRoute.status == RestorableRouteFutureStatus.empty) { // Nếu route đã hoàn thành và không còn "đợi" nữa, // (tức là người dùng đã chọn xong hoặc thoát khỏi màn hình chọn) // chúng ta có thể làm gì đó nếu cần. // Trong trường hợp này, kết quả đã được xử lý ở trên. } }); } // Hàm để mở màn hình chọn void _openSelectionScreen() async { // 4. Sử dụng RestorableRouteFuture để push route // Thay vì dùng Navigator.push trực tiếp, ta dùng _selectionRoute.present() final int? result = await _selectionRoute.present(); if (result != null) { setState(() { _selectedNumber.value = result; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Màn Hình Chính')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _selectedNumber.value == null ? 'Chưa có số nào được chọn.' : 'Số đã chọn: ${_selectedNumber.value}', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 30), ElevatedButton( onPressed: _openSelectionScreen, child: const Text('Đi Chọn Số!'), ), ], ), ), ); } @override void dispose() { _selectionRoute.dispose(); _selectedNumber.dispose(); super.dispose(); } } Cách thử nghiệm: Chạy ứng dụng. Nhấn nút "Đi Chọn Số!". Khi màn hình chọn số hiện ra, đừng chọn gì cả. Đưa ứng dụng vào chạy nền (background). Từ trình quản lý tác vụ của điện thoại (recent apps), "vuốt" để đóng hoàn toàn ứng dụng. Mở lại ứng dụng từ biểu tượng trên màn hình chính. Bạn sẽ thấy ứng dụng quay lại đúng màn hình "Chọn Con Số Yêu Thích" và khi bạn chọn một số, kết quả sẽ được truyền về màn hình chính như bình thường! Thật "vi diệu" đúng không? Mẹo "Hack Não" và Best Practices từ Creyt Chỉ dùng khi cần thiết: RestorableRouteFuture không phải là viên đạn bạc cho mọi thứ. Nó dành cho các luồng điều hướng phức tạp, nơi người dùng mong đợi quay lại đúng điểm dang dở sau khi app bị "reset". Nếu chỉ là một trang tĩnh không có tương tác gì đặc biệt, đừng "over-engineering" làm gì. Luôn đi kèm RestorationMixin: Giống như việc bạn không thể đi "phượt" mà không có xe vậy. RestorableRouteFuture chỉ hoạt động trong một StatefulWidget có RestorationMixin. Hiểu rõ "Future": Nó lưu trữ cái "lời hứa" về một kết quả trong tương lai. Khi app phục hồi, nó sẽ thực hiện lại lời hứa đó (tức là push lại route), và sau đó mới chờ đợi kết quả thực sự. Đừng nhầm lẫn là nó lưu luôn kết quả nhé! restorationScopeId là bắt buộc: Đảm bảo MaterialApp hoặc các widget cha có RestorationScope (hoặc restorationScopeId) để hệ thống biết "ai đang quản lý" các RestorableProperty của bạn. dispose() đừng quên: Giống như dọn dẹp sau một bữa tiệc, hãy dispose() các RestorableProperty khi State của bạn bị loại bỏ để tránh rò rỉ bộ nhớ. Góc Học Thuật Sâu Của Anh Creyt: "Cấu Trúc Hồi Sinh" À, cái này mới là cái "hay ho" nè các đệ tử! RestorableRouteFuture không chỉ đơn giản là "nhớ" một cái route. Nó là một phần của hệ thống State Restoration rộng lớn hơn của Flutter. Khi bạn gọi _selectionRoute.present(), nó sẽ push một route mới vào Navigator. Đồng thời, nó cũng ghi lại vào "Restoration Bucket" một ID và thông tin rằng "có một Future đang chờ kết quả từ route này". Khi ứng dụng bị kill và khởi động lại, hệ thống Restoration sẽ quét qua các RestorableProperty đã được đăng ký. Khi nó thấy _selectionRoute của bạn, nó sẽ kiểm tra xem có một Future nào đang "dang dở" không. Nếu có, nó sẽ gọi hàm onPresent mà bạn đã cung cấp (trong ví dụ là navigator.push<int?>(MaterialPageRoute(...))) để tái tạo lại cái route đó. Sau đó, nó sẽ gắn lại cái Future mới này vào _selectionRoute.value và lắng nghe kết quả. Điều này có nghĩa là, màn hình SelectionScreen của bạn sẽ được tạo lại từ đầu, chứ không phải là một phiên bản "đông lạnh" của màn hình cũ. Cái hay là, từ góc nhìn của HomeScreen, nó vẫn đang "await" một Future như thể chưa có chuyện gì xảy ra. Mượt mà như một dòng code viết bởi AI! Ứng Dụng Thực Tế và Case Nào Nên Dùng? Các ứng dụng lớn, có luồng người dùng phức tạp, đặc biệt là các ứng dụng tài chính, thương mại điện tử, hoặc các ứng dụng có tính năng tạo/chỉnh sửa nội dung dài hơi, là những "khách hàng tiềm năng" của RestorableRouteFuture. Ứng dụng E-commerce: Bạn đang trong quá trình thanh toán nhiều bước (nhập địa chỉ, chọn phương thức thanh toán, xác nhận đơn hàng). Nếu app bị kill, bạn muốn người dùng quay lại đúng bước thanh toán cuối cùng. Ứng dụng Ngân hàng: Đang thực hiện giao dịch chuyển tiền, đến bước xác nhận OTP. App bị kill, bạn muốn người dùng quay lại màn hình nhập OTP. Ứng dụng Mạng xã hội/Ghi chú: Đang viết một bài đăng dài, hoặc tạo một ghi chú quan trọng. Nếu có màn hình popup xác nhận hay chọn tag/category, và app bị kill, bạn muốn quay lại màn hình soạn thảo với popup đó vẫn đang chờ. Thử nghiệm và Hướng dẫn nên dùng cho Case nào: Anh Creyt đã từng "vật lộn" với việc này trong các dự án lớn. Hồi xưa chưa có RestorableRouteFuture, việc khôi phục state của các Future route là một cơn ác mộng. Phải tự lưu vào shared_preferences hay database, rồi tự viết logic để kiểm tra và push lại route. "Cực hình" lắm các đệ tử ơi! Với RestorableRouteFuture, mọi thứ trở nên "dễ thở" hơn rất nhiều. Nên dùng khi: Luồng người dùng quan trọng, không thể mất dữ liệu giữa chừng: Đặc biệt là các luồng tài chính, mua sắm. Có các Future trả về kết quả từ các route khác: showDialog, Navigator.push với await, showModalBottomSheet, v.v. Ứng dụng có thể chạy nền lâu và có nguy cơ bị OS kill: Các ứng dụng nặng hoặc chạy trên thiết bị có ít RAM. Không nên dùng khi: Màn hình không có tương tác hoặc không quan trọng việc khôi phục trạng thái điều hướng: Ví dụ, một màn hình giới thiệu (splash screen) hoặc một màn hình chỉ hiển thị thông tin tĩnh. Việc mất trạng thái là chấp nhận được hoặc thậm chí mong muốn: Đôi khi, việc bắt đầu lại từ đầu lại là một tính năng (ví dụ, sau khi đăng xuất). Bạn chỉ muốn lưu trữ state của các widget đơn lẻ: Lúc đó, các RestorableProperty khác như RestorableString, RestorableInt, v.v., sẽ phù hợp hơn. Nhớ nhé, các đệ tử! Dùng đúng công cụ cho đúng việc, đó mới là phong thái của một lập trình viên "level max"! 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
ReorderableListView: Khi List của bạn biết 'nhảy múa' theo ý Gen Z!
20/03/2026

ReorderableListView: Khi List của bạn biết 'nhảy múa' theo ý Gen Z!

Chào các 'dev' Gen Z! Anh Creyt đây. Hôm nay, chúng ta sẽ cùng nhau 'mổ xẻ' một 'siêu năng lực' của Flutter mà chắc chắn các em sẽ mê tít: đó là khả năng biến những danh sách tĩnh thành những vũ công điêu luyện, sẵn sàng 'nhảy múa' theo từng cú kéo thả của người dùng. Từ khóa 'ReorderableListViewState' nghe có vẻ hàn lâm, nhưng thực ra nó là 'linh hồn' đứng sau widget ReorderableListView huyền thoại đó! 1. ReorderableListViewState là gì? Để làm gì? (Theo style Gen Z) Nói thẳng và thật, ReorderableListViewState không phải là cái tên mà các em sẽ trực tiếp gọi hay tương tác nhiều trong code đâu. Nó giống như 'nhân vật ẩn' đằng sau hậu trường, là cái 'state' nội bộ của widget ReorderableListView – cái 'bộ não' giúp ReorderableListView làm được điều kỳ diệu: cho phép người dùng kéo thả các item để sắp xếp lại thứ tự trong một danh sách! Thử hình dung thế này: Các em có một playlist nhạc trên Spotify, một danh sách công việc trên Trello, hay đơn giản là các sticker yêu thích trong Zalo. Khi các em kéo một bài hát lên đầu, một task xuống cuối, hay sắp xếp lại thứ tự các sticker, đó chính là lúc ReorderableListView đang 'nhảy múa' đấy! Và ReorderableListViewState chính là người đạo diễn thầm lặng, điều phối mọi chuyển động mượt mà đó. Nó sinh ra để làm gì ư? Đơn giản là để nâng tầm trải nghiệm người dùng (UX) lên một tầm cao mới. Thay vì phải xóa đi tạo lại, hay dùng các nút 'lên/xuống' cổ lỗ sĩ, giờ đây người dùng có thể tự tay 'mix & match' lại danh sách theo ý mình, một cách trực quan và cực kỳ 'chill'. 2. Code Ví Dụ Minh Họa Rõ Ràng, Chuẩn Kiến Thức Để ReorderableListView hoạt động, chúng ta cần hai thứ quan trọng: Một danh sách dữ liệu có thể thay đổi (mutable list): Vì khi kéo thả, thứ tự của dữ liệu sẽ thay đổi. Một callback onReorder: Đây là nơi 'bộ não' của chúng ta (code của các em) sẽ nhận thông báo khi người dùng kéo thả xong, và chúng ta phải cập nhật lại danh sách dữ liệu dựa trên vị trí mới. Key cho mỗi item: Cực kỳ quan trọng để Flutter biết chính xác item nào đang được di chuyển. Đây là ví dụ kinh điển nhất để các em dễ hình dung: 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: 'Reorderable List Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const ReorderableListScreen(), ); } } class ReorderableListScreen extends StatefulWidget { const ReorderableListScreen({super.key}); @override State<ReorderableListScreen> createState() => _ReorderableListScreenState(); } class _ReorderableListScreenState extends State<ReorderableListScreen> { List<String> _items = [ 'Ăn sáng', 'Code Flutter', 'Tập gym', 'Ăn trưa', 'Học thuật cùng anh Creyt', 'Đi chơi với crush', 'Ngủ ' ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('To-do List của Gen Z'), ), body: ReorderableListView( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), children: <Widget>[ for (int index = 0; index < _items.length; index += 1) Card( key: Key('$index'), // Cực kỳ quan trọng: mỗi item phải có một Key duy nhất! elevation: 2.0, margin: const EdgeInsets.symmetric(vertical: 4.0), child: ListTile( leading: CircleAvatar( child: Text('${index + 1}'), ), title: Text(_items[index]), trailing: const Icon(Icons.drag_handle), ), ), ], onReorder: (int oldIndex, int newIndex) { setState(() { if (oldIndex < newIndex) { newIndex -= 1; // Điều chỉnh newIndex nếu item bị kéo xuống dưới } final String item = _items.removeAt(oldIndex); // Xóa item ở vị trí cũ _items.insert(newIndex, item); // Chèn item vào vị trí mới }); }, ), ); } } Trong ví dụ trên, _items là danh sách các công việc. Khi người dùng kéo thả, hàm onReorder sẽ được gọi với oldIndex (vị trí ban đầu) và newIndex (vị trí đích). Nhiệm vụ của chúng ta là cập nhật lại _items trong setState để giao diện được vẽ lại theo thứ tự mới. Nhớ kỹ, Key cho mỗi Card là bắt buộc nhé! 3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế Key là VUA: Anh Creyt nhắc lại lần nữa, mỗi widget con trong children của ReorderableListView phải có một Key duy nhất. ValueKey, ObjectKey, hoặc đơn giản là Key('$index') nếu danh sách của em không quá phức tạp và các item không trùng lặp là đủ. Nếu không có Key, Flutter sẽ 'đứng hình' không biết item nào đang được di chuyển, dẫn đến lỗi hoặc hành vi không mong muốn. onReorder không tự cập nhật UI: Nó chỉ là một 'tai mắt' báo cho em biết có sự thay đổi. Việc 'xử lý' thay đổi đó (bằng cách cập nhật data source và gọi setState) là trách nhiệm của lập trình viên. Đừng quên setState! Xử lý newIndex: Khi kéo một item xuống dưới, newIndex có thể 'nhảy' một đơn vị. Đoạn if (oldIndex < newIndex) { newIndex -= 1; } trong onReorder là một 'trick' nhỏ để đảm bảo newIndex luôn trỏ đúng vào vị trí thực tế sau khi item bị xóa khỏi vị trí cũ. Hãy nhớ nó! Tối ưu hiệu năng: Với danh sách cực dài, cân nhắc sử dụng ReorderableListView.builder thay vì ReorderableListView thông thường để tối ưu hóa việc xây dựng widget, tương tự như ListView.builder. Phản hồi trực quan: ReorderableListView đã cung cấp sẵn một số hiệu ứng kéo thả mặc định khá mượt. Tuy nhiên, em có thể tùy chỉnh thêm như thay đổi màu nền, tăng elevation của Card khi đang kéo để người dùng biết họ đang thao tác với item nào. 4. Văn phong học thuật sâu của anh Creyt, dạy dễ hiểu tuyệt đối ReorderableListView là một ví dụ điển hình cho triết lý 'Reactive Programming' của Flutter. Nó không chỉ đơn thuần là một widget hiển thị danh sách, mà là một 'cơ chế' cho phép giao diện người dùng tương tác trực tiếp với dữ liệu một cách linh hoạt. Cái State nội bộ của nó (mà chúng ta gọi là ReorderableListViewState) chịu trách nhiệm lắng nghe các cử chỉ kéo thả (drag gestures), tính toán vị trí mới, và sau đó 'truyền tin' cho chúng ta qua onReorder callback. Điều quan trọng ở đây là sự tách biệt rõ ràng giữa UI (User Interface) và Data (Dữ liệu). ReorderableListView lo phần UI, làm cho việc kéo thả trông thật 'mượt'. Còn chúng ta, qua onReorder, lo phần Data, đảm bảo rằng khi UI thay đổi, dữ liệu underlying cũng phải được cập nhật tương ứng. Mối quan hệ hai chiều này chính là chìa khóa để xây dựng các ứng dụng mạnh mẽ và có khả năng mở rộng. 5. Ví dụ thực tế các ứng dụng/website đã ứng dụng Các em dùng hàng ngày mà không để ý đó thôi: Spotify/Apple Music: Sắp xếp lại thứ tự bài hát trong playlist. Trello/Asana/Jira: Kéo thả các thẻ công việc giữa các cột hoặc trong cùng một cột. Google Keep/Evernote: Sắp xếp lại thứ tự các ghi chú, danh sách. Ứng dụng quản lý ảnh/video: Sắp xếp lại thứ tự ảnh/video trong album trước khi xuất bản. Các ứng dụng mua sắm: Đôi khi cho phép người dùng sắp xếp lại các mục yêu thích hoặc trong giỏ hàng. 6. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào Anh Creyt ngày xưa cũng từng 'trầy vi tróc vẩy' với việc tự implement kéo thả bằng GestureDetector, Draggable, DragTarget... Thật sự là một cơn ác mộng để làm cho nó mượt mà và xử lý đủ mọi trường hợp (như scroll khi kéo, feedback hình ảnh, v.v.). Khi ReorderableListView ra đời, nó giống như một 'ân huệ' từ Flutter Team vậy! Nên dùng ReorderableListView khi: Người dùng cần cá nhân hóa: Khi họ muốn tự tay sắp xếp thứ tự các mục theo ý muốn cá nhân (playlist, danh sách yêu thích, thứ tự hiển thị widget). Quản lý tác vụ/nội dung: Các ứng dụng quản lý công việc, ghi chú, danh sách mua sắm, hoặc các ứng dụng cho phép người dùng sắp xếp lại nội dung (ví dụ: các slide trong một bài thuyết trình). Tăng tính tương tác: Khi muốn làm cho ứng dụng của em trở nên 'sống động' và dễ sử dụng hơn, mang lại cảm giác 'nắm quyền kiểm soát' cho người dùng. Không nên dùng khi: Thứ tự của danh sách được xác định nghiêm ngặt bởi logic nghiệp vụ và người dùng không được phép thay đổi (ví dụ: danh sách kết quả tìm kiếm được sắp xếp theo mức độ liên quan, danh sách sản phẩm theo giá từ thấp đến cao). Danh sách chỉ mang tính hiển thị thông tin một chiều, không cần bất kỳ tương tác sắp xếp nào từ người dùng. Nhớ nhé, ReorderableListView là một công cụ cực kỳ mạnh mẽ để làm cho ứng dụng của em trở nên thân thiện và 'thông minh' hơn. Hãy luyện tập và áp dụng nó vào các project của mình, các em sẽ thấy sự khác biệt rõ rệ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é!

47 Đọc tiếp
RawScrollbar: Khi Thanh Cuộn 'Default Vibe' Không Đủ 'Chất'!
20/03/2026

RawScrollbar: Khi Thanh Cuộn 'Default Vibe' Không Đủ 'Chất'!

Chào các "dev-er" Gen Z, hôm nay anh Creyt sẽ "bung lụa" một khái niệm nghe hơi "raw" nhưng lại cực kỳ "chất" khi bạn muốn "flex" khả năng tùy biến UI của mình trong Flutter: RawScrollbar. 1. RawScrollbar: Khi Thanh Cuộn "Default Vibe" Không Đủ "Chất"! Các bạn hình dung thế này, khi bạn dùng ListView hay GridView trong Flutter, mặc định nó sẽ có một cái thanh cuộn (scrollbar) nhỏ nhỏ ở rìa phải (hoặc dưới) để báo hiệu "ê, còn nữa đó nha, kéo xuống đi!". Thanh cuộn này thường là của Material Design hoặc Cupertino, nó "đúng bài" và "đúng luật" của hệ điều hành. Nói trắng ra là nó "an toàn", "dễ dùng", nhưng đôi khi nó lại "fail vibe" với cái UI "phá cách" mà bạn đang cố gắng xây dựng. Đó là lúc RawScrollbar xuất hiện như một "siêu anh hùng" thầm lặng. Nó không phải là thanh cuộn "mì ăn liền" như Scrollbar thông thường. RawScrollbar giống như việc bạn được cấp cho một "bộ kit lắp ráp scrollbar" vậy. Nó cung cấp cho bạn những thứ cơ bản nhất: cái "ngón tay" để kéo (thumb), cái "đường ray" để nó chạy (track), và cho phép bạn điều khiển mọi thứ từ màu sắc, độ dày, độ bo góc, cho đến hiệu ứng ẩn hiện của nó. Mục đích ư? Để bạn có thể tạo ra một cái scrollbar "độc nhất vô nhị", "không đụng hàng", "match" hoàn hảo với "concept" thiết kế của app bạn. Nói cách khác, nếu Scrollbar mặc định là một bộ lọc Instagram có sẵn, thì RawScrollbar chính là Photoshop với tất cả các layer, công cụ và hiệu ứng để bạn "blend" ra bức ảnh "nghệ" của riêng mình. "Đỉnh của chóp" là ở chỗ đó! 2. Code Ví Dụ: "Biến Hình" Thanh Cuộn Của Bạn Để thấy rõ sức mạnh của RawScrollbar, chúng ta sẽ "độ" một cái thanh cuộn cho một ListView đơn giản. Các bạn cần nhớ, RawScrollbar cần một ScrollController để "bắt sóng" với widget có thể cuộn (như ListView, GridView, SingleChildScrollView). 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: 'RawScrollbar Demo by Creyt', theme: ThemeData.dark(), // Thích vibe tối cho nó ngầu! home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final ScrollController _scrollController = ScrollController(); @override void dispose() { _scrollController.dispose(); // Luôn nhớ giải phóng controller nha! super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('RawScrollbar "Độ" Của Anh Creyt'), ), body: RawScrollbar( controller: _scrollController, thumbColor: Colors.purpleAccent, // Màu của "ngón tay" kéo trackColor: Colors.grey.withOpacity(0.3), // Màu của "đường ray" thickness: 10.0, // Độ dày của scrollbar radius: const Radius.circular(5.0), // Độ bo góc cho "ngón tay" isAlwaysShown: true, // Luôn hiển thị, không ẩn đi fadeDuration: const Duration(milliseconds: 300), // Thời gian mờ dần khi ẩn timeToFade: const Duration(milliseconds: 600), // Thời gian đợi trước khi mờ child: ListView.builder( controller: _scrollController, // Bắt buộc phải truyền controller vào đây nữa nha! itemCount: 50, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.deepPurple[100 * (index % 9)], child: Padding( padding: const EdgeInsets.all(20.0), child: Text( 'Item ${index + 1}: Chào các bạn Gen Z!', style: const TextStyle(fontSize: 18, color: Colors.white), ), ), ); }, ), ), ); } } Trong ví dụ trên, anh Creyt đã "hô biến" một thanh cuộn bình thường thành một thanh màu tím "chanh sả", dày hơn, bo góc nhẹ nhàng, và luôn "lộ diện" để các bạn chiêm ngưỡng. Các bạn có thể "vọc vạch" các thuộc tính thumbColor, trackColor, thickness, radius, isAlwaysShown, fadeDuration, timeToFade để tạo ra những hiệu ứng "độc lạ Bình Dương" của riêng mình! 3. Mẹo Vặt "Hack Life" Với RawScrollbar Hiểu rõ nhu cầu, đừng "overkill": RawScrollbar là "súng hạng nặng" cho những "trận chiến" khó. Nếu Scrollbar (không có Raw) đã đáp ứng được yêu cầu "đổi màu cơ bản" hoặc "chỉ cần hiện lên khi cuộn" thì đừng dùng RawScrollbar làm gì cho "tốn sức". "Don't fix what ain't broken" nha các bạn! Controller là "linh hồn": Luôn nhớ tạo một ScrollController và truyền nó vào cả RawScrollbar lẫn widget cuộn của bạn. Thiếu một trong hai là nó "đơ" như "cây cơ" liền! Thẩm mỹ "đa nền tảng": Vì RawScrollbar cho phép bạn "phá bỏ" mọi quy tắc về design của Material/Cupertino, nên hãy chắc chắn rằng thanh cuộn "custom" của bạn vẫn "hợp gu" và "dễ dùng" trên mọi nền tảng (iOS, Android, Web, Desktop) mà app bạn nhắm tới. Đừng để nó thành "thảm họa" nha! Animation "mượt mà": Sử dụng fadeDuration và timeToFade để tạo hiệu ứng ẩn/hiện "mượt mà" cho thanh cuộn. Nó giúp app bạn trông "pro" hơn nhiều, tránh cảm giác "giật cục" khi thanh cuộn xuất hiện/biến mất. 4. "Creyt's Deep Dive": Phân Tích Kỹ Thuật Sâu Tại sao lại gọi là Raw? Đơn giản là vì nó "trần trụi". Nó không tự động áp dụng bất kỳ phong cách Material hay Cupertino nào cả. Nó chỉ cung cấp cho bạn một khung sườn và các "lỗ hổng" để bạn "đổ" style và logic của riêng mình vào. Điều này khác hẳn với Scrollbar (mà thực chất là MaterialScrollbar hoặc CupertinoScrollbar tùy nền tảng), vốn đã được "đóng gói" sẵn với các quy tắc thiết kế của hệ điều hành. Scrollable và ScrollController là bộ đôi "song kiếm hợp bích" mà RawScrollbar dựa vào. Scrollable là widget chịu trách nhiệm cho việc cuộn (như ListView, GridView). ScrollController là một "tay điều khiển" mà bạn dùng để "nắm đầu" cái Scrollable đó, đọc vị trí cuộn, hoặc thậm chí là "ra lệnh" cho nó cuộn tới một vị trí cụ thể. RawScrollbar chỉ là một "người quan sát" thông minh, nó "nghe lén" ScrollController để biết khi nào thì "ngón tay" (thumb) của nó cần di chuyển và di chuyển bao nhiêu. Nó không tự cuộn được, nó chỉ "phản ánh" trạng thái cuộn thôi. Vậy khi nào cần "tháo gỡ" cái Scrollbar mặc định để dùng RawScrollbar? Khi UI/UX của bạn yêu cầu một thanh cuộn phải có hình dạng "kỳ dị" (ví dụ: hình mũi tên, hình tròn), màu sắc "lạ mắt" (gradient, texture), hoặc chỉ hiện khi có tương tác rất đặc biệt (ví dụ: chỉ hiện khi hover chuột trên desktop, hoặc khi kéo rất mạnh). Nói chung, là khi bạn muốn "đập đi xây lại" một cái scrollbar "có một không hai" mà không muốn bị "ràng buộc" bởi bất kỳ quy tắc design nào. 5. Ứng Dụng Thực Tế: "Ai Đã Dùng Nó?" Thực tế, các ứng dụng lớn thường rất "khó tính" trong việc đồng bộ hóa mọi chi tiết UI/UX để tạo ra một "brand identity" mạnh mẽ. Mặc dù không thể chỉ đích danh "ứng dụng X của Flutter dùng RawScrollbar", nhưng các bạn có thể thấy "tư duy" tùy biến scrollbar này ở rất nhiều nơi: Các ứng dụng chỉnh sửa ảnh/video chuyên nghiệp: Thường có các thanh trượt (slider) và thanh cuộn được thiết kế rất riêng biệt, màu sắc và hình dạng "ăn nhập" hoàn toàn với giao diện tổng thể, không theo bất kỳ quy tắc OS nào. Ví dụ: Figma (trên web), các ứng dụng như Lightroom Mobile có các thanh trượt và scrollbar rất đặc trưng. Các ứng dụng game hoặc creative: Các menu cuộn trong game thường có thanh cuộn được thiết kế theo chủ đề của game, không hề giống thanh cuộn của Android hay iOS. Các hệ thống thiết kế nội bộ của các công ty lớn: Khi họ xây dựng một "design system" riêng, họ sẽ muốn mọi component, kể cả thanh cuộn, đều phải "đúng chuẩn" của họ. RawScrollbar là công cụ lý tưởng để đạt được sự nhất quán đó. 6. Thử Nghiệm Của Anh Creyt & Hướng Dẫn Dùng Anh Creyt đã từng "đổ mồ hôi, sôi nước mắt" khi làm một ứng dụng quản lý dự án cho một công ty thiết kế. Khách hàng yêu cầu thanh cuộn phải có màu xanh lá cây đặc trưng của họ, và phải "ẩn mình" đi khi không dùng, chỉ "lấp ló" hiện ra khi người dùng bắt đầu cuộn. Scrollbar mặc định không thể làm được điều đó một cách "nuột nà". RawScrollbar với khả năng tùy chỉnh thumbColor, trackColor, fadeDuration, và timeToFade chính là "cứu tinh" của anh. Kết quả là khách hàng "ưng cái bụng" lắm! Nên dùng RawScrollbar khi nào? Khi thiết kế UI/UX của bạn "khát khao" một thanh cuộn có "cá tính" riêng: Không muốn đụng hàng, muốn một cái gì đó "signature" của app bạn. Khi bạn cần kiểm soát "từ A đến Z" mọi thứ: Màu sắc, độ dày, độ bo góc, hình dạng (bạn có thể dùng Container hoặc DecoratedBox làm thumb để tạo hình dạng phức tạp hơn). Khi bạn muốn hiệu ứng ẩn/hiện "siêu mượt" và "có chủ đích": Ví dụ, thanh cuộn chỉ hiện khi người dùng giữ chuột trên nó, hoặc hiện rồi mờ dần sau một khoảng thời gian nhất định. Khi bạn đang xây dựng một thư viện UI/UX độc lập: Và muốn các thành phần của mình nhất quán, không phụ thuộc vào styling mặc định của Material/Cupertino. Không nên dùng RawScrollbar khi nào? Khi thanh cuộn mặc định của Material (Scrollbar) đã "đủ xài": Nếu chỉ cần đổi màu thumbColor hoặc trackColor cơ bản, thì Scrollbar cũng có thể làm được và đơn giản hơn nhiều. Khi bạn không có yêu cầu "độc lạ" nào về thanh cuộn: Đừng "làm màu" nếu không cần thiết. Đôi khi, sự đơn giản lại là đỉnh cao của sự tinh tế. Nhớ nha các "dev-er" Gen Z, RawScrollbar là một công cụ mạnh, nhưng hãy dùng nó "đúng nơi, đúng lúc" để app của bạn không chỉ "đẹp" mà còn "hiệu quả" nữa! "Keep it raw, keep it real!" Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

46 Đọc tiếp
RawKeyboardListener: Ninja Bắt Phím, Thao Túng Bàn Phím Flutter
20/03/2026

RawKeyboardListener: Ninja Bắt Phím, Thao Túng Bàn Phím Flutter

Chào các Gen Z tương lai của ngành code! Hôm nay, anh Creyt sẽ dẫn các em đi khám phá một "siêu năng lực" trong Flutter mà không phải ai cũng biết, đó là widget RawKeyboardListener. Nghe cái tên "Raw" là thấy mùi "nguyên thủy", "thô sơ" rồi đúng không? Đúng vậy! Thay vì để các TextField hay TextFormField của các em tự động xử lý input như bình thường, RawKeyboardListener cho phép các em "nghe lén" mọi sự kiện bàn phím trước khi chúng kịp đến tai bất kỳ widget nào khác. Nó là một StatelessWidget nhưng lại sở hữu khả năng "thay đổi trạng thái" của ứng dụng dựa trên bàn phím một cách cực kỳ linh hoạt. Hãy tưởng tượng thế này: RawKeyboardListener giống như một anh bảo vệ siêu thính giác, đứng ngay cổng chính của khu chung cư Flutter nhà mình. Mỗi khi có một anh phím (key) nào đó "gõ cửa", anh bảo vệ này là người đầu tiên nghe thấy tiếng tách, tiếng cạch của phím đó được nhấn xuống (key down) hay nhả ra (key up). Anh ấy không quan tâm anh phím đó mang thông điệp gì (chữ 'A', chữ 'B'), anh ấy chỉ quan tâm hành động nhấn/nhả. Điều này cực kỳ mạnh mẽ khi các em muốn tạo ra những phím tắt "thần thánh", điều khiển game, hoặc bất cứ thứ gì cần phản ứng tức thì với bàn phím vật lý mà không cần phải focus vào một ô nhập liệu nào cả. Cách "Anh Bảo Vệ" Này Hoạt Động (The Guts) Để anh bảo vệ của chúng ta (RawKeyboardListener) làm việc, các em cần cung cấp cho ảnh hai thứ chính: FocusNode: Đây là "đài phát thanh" mà anh bảo vệ dùng để nhận tín hiệu. Một RawKeyboardListener cần một FocusNode để biết khi nào nó nên lắng nghe. Khi widget chứa RawKeyboardListener được focus, nó mới bắt đầu hoạt động. onKey callback: Đây là "quyển sổ ghi chép" của anh bảo vệ. Mỗi khi có sự kiện bàn phím xảy ra, anh ấy sẽ ghi lại vào đây. Callback này sẽ nhận về một đối tượng RawKeyEvent, chứa đầy đủ thông tin về sự kiện đó: phím nào được nhấn, trạng thái phím (nhấn xuống hay nhả ra), thậm chí cả các phím modifier (Shift, Ctrl, Alt) có đang được giữ hay không. Cái hay của RawKeyEvent là nó cho các em biết chính xác "cái phím vật lý" nào đã được nhấn, không phải chỉ là ký tự được sinh ra. Ví dụ, nếu các em nhấn 'Shift' + 'a', một TextField sẽ nhận 'A', nhưng RawKeyboardListener sẽ nhận hai sự kiện: 'Shift Down', 'a Down', 'a Up', 'Shift Up'. Tuyệt vời chưa? Code Ví Dụ Minh Hoạ Rõ Ràng Giờ thì xắn tay áo lên, chúng ta cùng code một ví dụ siêu đơn giản để thấy "anh bảo vệ" này hoạt động thế nào nhé! import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Quan trọng để dùng RawKeyEvent void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Creyt\'s RawKeyboardListener Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const KeyboardListenerScreen(), ); } } class KeyboardListenerScreen extends StatefulWidget { const KeyboardListenerScreen({super.key}); @override State<KeyboardListenerScreen> createState() => _KeyboardListenerScreenState(); } class _KeyboardListenerScreenState extends State<KeyboardListenerScreen> { // 1. Khai báo FocusNode final FocusNode _focusNode = FocusNode(); String _lastKeyEvent = 'Chưa có sự kiện phím nào...'; @override void initState() { super.initState(); // 2. Yêu cầu focus khi widget được tạo // Dùng WidgetsBinding.instance.addPostFrameCallback để đảm bảo context đã sẵn sàng WidgetsBinding.instance.addPostFrameCallback((_) { FocusScope.of(context).requestFocus(_focusNode); }); } @override void dispose() { // 3. Quan trọng: Giải phóng FocusNode khi widget bị hủy _focusNode.dispose(); super.dispose(); } void _handleKeyEvent(RawKeyEvent event) { setState(() { if (event is RawKeyDownEvent) { _lastKeyEvent = 'Phím ${event.logicalKey.debugName} được NHẤN (Down)!'; // Các em có thể kiểm tra phím modifier ở đây if (event.isControlPressed) { _lastKeyEvent += ' (Ctrl đang giữ)'; } if (event.isShiftPressed) { _lastKeyEvent += ' (Shift đang giữ)'; } } else if (event is RawKeyUpEvent) { _lastKeyEvent = 'Phím ${event.logicalKey.debugName} được NHẢ (Up)!'; } }); // Để ý: Nếu các em không muốn sự kiện này được truyền tiếp // cho các widget khác (ví dụ: TextField), các em có thể trả về KeyEventResult.handled // Tuy nhiên, trong ví dụ này, chúng ta chỉ lắng nghe. } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Creyt dạy RawKeyboardListener'), ), body: Center( child: RawKeyboardListener( focusNode: _focusNode, onKey: _handleKeyEvent, child: Container( padding: const EdgeInsets.all(20.0), color: Colors.lightBlue[100], child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Nhấn vào đây (hoặc bất cứ đâu trong Container này) ', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), const Text( 'rồi thử gõ phím xem sao:', textAlign: TextAlign.center, style: TextStyle(fontSize: 18), ), const SizedBox(height: 20), Text( _lastKeyEvent, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 30), const Text( '(Nhớ là phải focus vào widget này thì mới nhận sự kiện nhé!)', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic), ), ], ), ), ), ), ); } } Mẹo Hay (Best Practices) Từ Anh Creyt Nghe anh Creyt dặn dò vài mẹo để dùng RawKeyboardListener như một pro nhé: Quản lý FocusNode: Nhớ kỹ, luôn luôn dispose() cái FocusNode khi State bị hủy (dispose() method) để tránh rò rỉ bộ nhớ. Nó giống như việc các em dùng xong cái mic rồi thì phải tắt đi ấy. Hiểu rõ RawKeyEventType: Phân biệt giữa RawKeyDownEvent (khi phím được nhấn xuống) và RawKeyUpEvent (khi phím được nhả ra). Hầu hết các ứng dụng game hay phím tắt sẽ quan tâm đến KeyDown, nhưng đôi khi các em cần KeyUp để xử lý các hành động "giữ phím" hoặc "thả phím". Focus là chìa khóa: RawKeyboardListener chỉ lắng nghe khi nó (hoặc một trong các con của nó) đang được focus. Đảm bảo các em đã gọi _focusNode.requestFocus() hoặc đặt nó trong một widget có thể nhận focus. Khi nào thì dùng, khi nào thì không?: Dùng khi: Các em cần bắt các phím tắt toàn cục (ví dụ: Ctrl+S để lưu), điều khiển game (WASD), hoặc xử lý các phím không phải là ký tự (F1-F12, Shift, Alt). Không dùng khi: Các em chỉ muốn nhập liệu văn bản thông thường. Khi đó, TextField hoặc TextFormField là lựa chọn tối ưu, chúng đã xử lý mọi thứ rất nuột nà rồi, không cần "bảo vệ" RawKeyboardListener can thiệp đâu. Cân nhắc Shortcuts và Actions: Đối với các phím tắt phức tạp hơn, đặc biệt là trên desktop hoặc web, Flutter cung cấp các widget Shortcuts và Actions để quản lý phím tắt một cách có cấu trúc hơn, dễ bảo trì hơn. RawKeyboardListener là tầng thấp nhất, cho các em sự linh hoạt tối đa nhưng cũng yêu cầu các em xử lý nhiều logic hơn. Ứng Dụng Thực Tế Đã Dùng RawKeyboardListener Anh Creyt đã từng thấy RawKeyboardListener được ứng dụng trong nhiều trường hợp thực tế, cực kỳ hay ho: Game trên Flutter Desktop/Web: Các game đơn giản như rắn săn mồi, tetris, hoặc các game platformer 2D cần phản ứng tức thì với phím mũi tên, WASD để di chuyển nhân vật. Đây là ứng dụng kinh điển của RawKeyboardListener. Ứng dụng đồ họa/thiết kế: Các phần mềm như Figma, Photoshop (nếu có phiên bản Flutter) thường có hàng tá phím tắt (Ctrl+Z, Ctrl+C, Spacebar để panning). RawKeyboardListener là nền tảng để bắt những lệnh này. Ứng dụng soạn thảo văn bản nâng cao: Một số editor tùy chỉnh có thể dùng RawKeyboardListener để phát hiện các tổ hợp phím đặc biệt, ví dụ như Tab để thụt lề, hoặc các phím chức năng để định dạng văn bản. Thử Nghiệm Của Anh Creyt và Hướng Dẫn Sử Dụng Hồi xưa, anh Creyt từng đau đầu với một dự án game Flutter trên web. Ban đầu, anh cứ nghĩ dùng TextField ẩn rồi lắng nghe sự kiện thay đổi là được. Ai dè, TextField nó chỉ nhận ký tự thôi, mấy cái phím mũi tên, Shift, Ctrl nó nuốt chửng mất! Lúc đó mới ngộ ra RawKeyboardListener chính là "chân ái". Nó cho phép anh bắt được từng phím một, dù là phím chức năng hay phím ký tự, và điều khiển nhân vật game mượt mà như bơ. Nên dùng cho case nào? Game Development: Bắt buộc phải có nếu các em muốn làm game có điều khiển bằng bàn phím. Global Hotkeys: Tạo các phím tắt hoạt động bất kể widget nào đang được focus. Ví dụ, Ctrl+S luôn lưu, F5 luôn refresh. Custom Input: Khi các em cần xử lý các tổ hợp phím phức tạp, hoặc các phím không sinh ra ký tự. Accessibility: Trong một số trường hợp, để tạo các tính năng trợ năng đặc biệt dựa trên bàn phím. Thử nghiệm của anh Creyt: Anh đã thử nghiệm dùng nó để tạo một "cheat code" trong ứng dụng. Khi người dùng gõ một chuỗi phím nhất định (ví dụ: Up, Up, Down, Down, Left, Right, Left, Right, B, A), một tính năng ẩn sẽ được kích hoạt. Nghe có vẻ "hacky" nhưng lại cực kỳ hiệu quả và vui nhộn! Vậy đó, RawKeyboardListener không chỉ là một widget, nó là một công cụ mạnh mẽ mở ra cánh cửa đến những trải nghiệm tương tác bàn phím độc đáo trong ứng dụng Flutter của các em. Hãy nắm vững nó và biến những ý tưởng "điên rồ" nhất thành hiện thực 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é!

40 Đọc tiếp