Chuyên mục

Flutter

Flutter tutolrial

174 bài viết
TextSelectionDelegate: Bí Mật Chọn Text 'Đỉnh Của Chóp' Trong Flutter
22/03/2026

TextSelectionDelegate: Bí Mật Chọn Text 'Đỉnh Của Chóp' Trong Flutter

Chào anh em code thủ GenZ! Hôm nay, anh Creyt sẽ cùng các bạn "mổ xẻ" một khái niệm nghe thì hàn lâm nhưng lại cực kỳ thực tế và quan trọng trong Flutter: TextSelectionDelegate. Đừng lo, anh sẽ biến nó thành câu chuyện dễ hiểu nhất, thậm chí còn dí dỏm hơn cả mấy cái meme GenZ nữa. TextSelectionDelegate Là Gì Mà Nghe Ngầu Vậy? Thử nhớ lại xem, mỗi lần bạn lướt TikTok, Insta hay chat trên Zalo, khi bạn giữ ngón tay trên một đoạn caption, comment, hay tin nhắn, tự nhiên nó hiện ra cái menu "Copy", "Cut", "Paste", "Select All" đúng không? Rồi hai cái tay cầm chọn text nó cứ di chuyển mượt mà theo ngón tay bạn như có ma thuật vậy. Anh em có bao giờ tự hỏi, ai là người điều khiển cái "vũ đoàn" này không? À há! Chính xác là TextSelectionDelegate đấy các bạn. Hãy hình dung nó như một "thư ký riêng" siêu năng lực của mỗi ô nhập liệu (text field) trong app của bạn. Nhiệm vụ của "thư ký" này là quản lý tất tần tật các thao tác liên quan đến việc chọn, sao chép, cắt, dán văn bản. Khi bạn ra lệnh "Copy", cô thư ký này sẽ biết phải lấy đoạn văn bản nào. Khi bạn di chuyển tay cầm, cô ấy sẽ điều khiển vùng chọn sao cho mượt mà nhất. Nói một cách hàn lâm hơn một tí, TextSelectionDelegate là một abstract class trong Flutter, định nghĩa một giao diện chuẩn. Nó không phải là một widget để bạn thấy trên màn hình, mà là một cầu nối vô hình giữa giao diện người dùng (những cái tay cầm, thanh công cụ) và logic xử lý văn bản thực sự bên trong (dữ liệu text, vị trí con trỏ, vùng chọn). "Bộ Não" Của Thư Ký Delegate Hoạt Động Ra Sao? TextSelectionDelegate có vài "năng lực" chính mà anh em cần biết: textEditingValue: Nơi nó lưu trữ toàn bộ thông tin về đoạn văn bản hiện tại, vị trí con trỏ, và vùng văn bản đang được chọn. Nó như cái "sổ tay" của thư ký vậy. userUpdateTextEditingValue: Khi có bất kỳ thay đổi nào (ví dụ: bạn gõ chữ, chọn text), nó sẽ thông báo cho hệ thống để cập nhật lại "sổ tay" này. cut(), copy(), paste(), selectAll(): Đây chính là những "lệnh" mà cô thư ký này thực thi khi bạn nhấn vào các nút trên thanh công cụ. bringIntoView(): Đảm bảo rằng phần văn bản đang được chọn hoặc con trỏ luôn hiển thị trên màn hình, không bị khuất. Thường thì, khi bạn dùng các widget "quốc dân" như TextField hay TextFormField, Flutter đã "cài đặt" sẵn một cô thư ký TextSelectionDelegate mặc định (thường là một implement nội bộ của EditableText) cho bạn rồi. Bạn không cần bận tâm đến nó, mọi thứ cứ thế mà "auto-pilot" chạy mượt mà. Nó giống như việc bạn mua điện thoại mới, các tính năng cơ bản đã có sẵn, bạn chỉ việc dùng thôi. Code Ví Dụ Minh Họa: Khi "Thư Ký" Làm Việc Thầm Lặng Ví dụ 1: TextField "thông thường" - Người hùng thầm lặng Đây là cách anh em thường dùng TextField, và TextSelectionDelegate đang làm việc cật lực mà bạn không hề hay biết: import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TextSelectionDelegate Demo', theme: ThemeData(primarySwatch: Colors.blueGrey), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelectionDelegate: Thư Ký Vô Hình'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gõ gì đó vào đây và thử chọn text xem!', style: TextStyle(fontSize: 16), ), const SizedBox(height: 10), TextField( controller: _controller, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Nhập nội dung của bạn...', ), maxLines: null, // Cho phép nhiều dòng keyboardType: TextInputType.multiline, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Khi bạn tương tác với _controller.selection, // thực chất bạn đang 'chỉ đạo' cho TextSelectionDelegate làm việc! final currentSelection = _controller.selection; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Vùng chọn hiện tại: Start ${currentSelection.start}, End ${currentSelection.end}' ), ), ); }, child: const Text('Kiểm tra Vùng Chọn (Qua TextEditingController)'), ), const SizedBox(height: 10), ElevatedButton( onPressed: () { // Tương tự, các thao tác này được delegate xử lý if (_controller.text.isNotEmpty) { _controller.text = 'Hello GenZ!'; // Thay đổi text _controller.selection = TextSelection.collapsed(offset: _controller.text.length); // Đặt con trỏ cuối } }, child: const Text('Reset Text & Con Trỏ'), ), ], ), ), ); } } Trong ví dụ trên, khi bạn tap và giữ vào TextField, những tay cầm chọn text và thanh công cụ (copy, cut, paste) sẽ tự động xuất hiện. Tất cả những tương tác đó đều do TextSelectionDelegate của EditableText (thành phần cốt lõi mà TextField được xây dựng trên đó) xử lý. Bạn chỉ việc dùng TextEditingController để tương tác với text và vùng chọn, còn việc hiển thị và xử lý UI thì đã có "thư ký" lo rồi. Ví dụ 2: "Mổ xẻ" cái Interface của Delegate (Khi nào cần tự làm?) Việc tự implement một TextSelectionDelegate hoàn chỉnh khá phức tạp và thường chỉ dành cho những case rất đặc biệt, khi bạn muốn xây dựng một trình soạn thảo văn bản hoàn toàn tùy chỉnh từ con số 0, không dùng TextField hay TextFormField của Flutter. Ví dụ, bạn muốn tạo một rich text editor với logic chọn text siêu dị, hay tích hợp với một engine text native nào đó. Đây là cái "khung xương" của TextSelectionDelegate để anh em hình dung: abstract class TextSelectionDelegate { /// Trả về giá trị của trường văn bản hiện tại (text, selection, composing region). TextEditingValue get textEditingValue; /// Gọi khi giá trị của trường văn bản thay đổi do người dùng tương tác. void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause); /// Sao chép vùng văn bản đã chọn vào clipboard. void cut(); /// Cắt vùng văn bản đã chọn vào clipboard. void copy(); /// Dán nội dung từ clipboard vào vị trí con trỏ/vùng chọn. void paste(); /// Chọn toàn bộ văn bản trong trường. void selectAll(); /// Đảm bảo rằng vùng chọn hoặc con trỏ hiện tại được hiển thị trên màn hình. void bringIntoView(TextPosition textPosition); } Khi nào bạn cần tự tay "độ" một cô thư ký như thế này? Chỉ khi bạn đang tạo ra một widget chỉnh sửa văn bản cực kỳ độc đáo, ví dụ như một trình soạn thảo code có highlight cú pháp và chọn theo khối, hoặc một trình soạn thảo rich text với các kiểu bôi đen, đánh dấu riêng biệt mà Flutter mặc định không cung cấp. Còn lại, cứ TextField mà phang! Mẹo Ghi Nhớ & Best Practices (Creyt's Tips Từ Thực Tế) "Đừng cố làm lại bánh xe": Nghe anh, 99% trường hợp, cứ dùng TextField hoặc TextFormField. Flutter đã làm rất tốt công việc của TextSelectionDelegate rồi, và việc tự viết lại sẽ tốn rất nhiều thời gian và công sức, chưa kể dễ phát sinh bug nữa. Trừ khi bạn là dân chuyên thích "độ" đồ, còn không thì cứ dùng đồ có sẵn cho nhanh. "Hiểu người quản lý": TextEditingController là bạn thân nhất của bạn khi làm việc với text input. Nó là cầu nối chính để bạn tương tác với textEditingValue (kiểm soát text, vị trí con trỏ, vùng chọn). Hãy làm chủ nó! "Khi nào thì cần nghĩ tới Delegate?": Chỉ khi bạn đang xây dựng một "thế giới text" hoàn toàn mới, độc lập với hệ sinh thái của EditableText mặc định. Tức là, bạn đang tạo ra một widget chỉnh sửa văn bản mà không thể dựa vào các thành phần có sẵn của Flutter. "Debug như một thám tử": Nếu thấy chọn text bị lỗi, nhảy lung tung, hay thanh công cụ không hiện, hãy kiểm tra TextEditingValue trong TextEditingController của bạn. Rất có thể có gì đó không đúng với selection hoặc text ở đó. Ứng Dụng Thực Tế: "Thư Ký" Ở Khắp Mọi Nơi TextSelectionDelegate (hoặc các cơ chế tương tự) có mặt ở khắp mọi nơi bạn thấy có thể tương tác với văn bản: WhatsApp, Messenger, Instagram DMs: Các ô nhập liệu tin nhắn mà bạn dùng hàng ngày. Google Docs, Notion (phiên bản Flutter Web/Desktop): Các trình soạn thảo văn bản phức tạp, nơi bạn có thể bôi đen, cắt, dán thoải mái. VS Code (nếu có phiên bản Flutter Desktop/Web): Các editor code cũng cần cơ chế chọn text cực kỳ chính xác và linh hoạt. Bất kỳ ứng dụng nào có TextField hoặc TextFormField đều đang âm thầm sử dụng "thư ký" này để mang lại trải nghiệm mượt mà cho người dùng. Thử Nghiệm & Hướng Dẫn Nên Dùng Cho Case Nào (Lời Khuyên Từ Creyt) Nên dùng default TextField/TextFormField: Cho hầu hết các trường hợp nhập liệu thông thường (tên, email, mật khẩu, tin nhắn ngắn, ghi chú đơn giản, form đăng ký, v.v.). Đây là lựa chọn mặc định và tối ưu nhất. Nên xem xét custom delegate (nhưng hãy cân nhắc kỹ!): Khi bạn cần một trình soạn thảo rich text từ đầu, với các kiểu chọn văn bản không chuẩn (ví dụ: chọn theo khối, chọn theo từ khóa đặc biệt, hoặc các kiểu bôi đen có định dạng riêng). Khi bạn cần tích hợp với một engine text editor native hoặc một thư viện text processing tùy chỉnh rất sâu mà Flutter không hỗ trợ sẵn. Tóm lại, nếu bạn đang "độ" một cái gì đó rất khác biệt so với một TextField thông thường, và bạn thực sự hiểu rõ mình đang làm gì. Đây là một con đường nhiều chông gai và đòi hỏi kiến thức sâu về cách Flutter render và tương tác với text. Thử nghiệm nhỏ: Anh em cứ thử nghịch TextEditingController để thay đổi selection qua code xem sao. Ví dụ, đặt con trỏ ở giữa một đoạn text, hoặc chọn tự động một từ. Bạn sẽ thấy nó điều khiển được cả vị trí con trỏ và vùng chọn đó. Đó chính là một phần công việc mà TextSelectionDelegate phải làm đấy, nó nhận tín hiệu từ TextEditingController và "vẽ" lại vùng chọn trên UI. Vậy đó, TextSelectionDelegate là một người hùng thầm lặng nhưng cực kỳ quan trọng, đảm bảo trải nghiệm tương tác văn bản của người dùng luôn mượt mà và trực quan. Hãy hiểu nó, nhưng đừng vội vàng tự tay implement nó nếu không thực sự cần thiết nhé anh em! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

41 Đọc tiếp
TextSelectionControls: Làm chủ Chọn Văn Bản trong Flutter
22/03/2026

TextSelectionControls: Làm chủ Chọn Văn Bản trong Flutter

Chào mấy đứa GenZ mê code! Hôm nay, anh Creyt sẽ dẫn mấy đứa đi "mổ xẻ" một thứ nghe có vẻ khô khan nhưng lại là "linh hồn" của trải nghiệm người dùng khi tương tác với văn bản: TextSelectionControls trong Flutter. 1. TextSelectionControls là gì và để làm gì? "Tưởng tượng mà xem, khi mấy đứa "quẹt" ngón tay trên màn hình điện thoại để chọn một đoạn văn bản, rồi bỗng dưng hiện ra cái thanh công cụ "Copy, Cut, Paste" huyền thoại, kèm theo hai cái "chấm tròn" hay "thanh đứng" nhỏ xíu ở hai đầu đoạn văn bản để mình kéo ra kéo vào ấy. Đấy! Tất cả những thứ đó, từ cái thanh công cụ đến mấy cái "tay cầm" bé xinh kia, đều là do TextSelectionControls "điều khiển" và "vẽ" ra đó!" Nói một cách "học thuật" hơn, TextSelectionControls trong Flutter là một lớp trừu tượng (abstract class) chịu trách nhiệm cung cấp các thành phần giao diện người dùng (UI) và hành vi liên quan đến việc chọn văn bản. Cụ thể, nó quản lý: Selection Handles: Các điểm neo (thường là hình tròn hoặc thanh) ở đầu và cuối vùng chọn văn bản, cho phép người dùng điều chỉnh phạm vi chọn. Selection Toolbar: Thanh công cụ popup chứa các hành động như Copy, Cut, Paste, Select All, Share, v.v., xuất hiện khi văn bản được chọn. Mục đích chính của nó là cho phép các developer như chúng ta tùy chỉnh hoàn toàn giao diện và hành vi của quá trình chọn văn bản. Thay vì cứ dùng cái mặc định "na ná" nhau của hệ điều hành, mấy đứa có thể "phù phép" để nó mang đậm dấu ấn riêng của app mình, thậm chí thêm thắt các chức năng "độc quyền" nữa! "Nó giống như cái remote điều khiển tivi vậy đó, nhưng thay vì chỉ có nút chuyển kênh và tăng giảm âm lượng, mấy đứa có thể biến nó thành một cái joystick game, thêm nút "hack", nút "buff" tùy ý, miễn sao người dùng thấy sướng là được!" 2. Code Ví Dụ Minh Hoạ Rõ Ràng Để mấy đứa dễ hình dung, anh Creyt sẽ chỉ cho cách tạo một bộ TextSelectionControls "chất chơi" riêng, đổi màu, đổi icon, thêm nút "Highlight" nữa cho nó "ngầu"! Chúng ta sẽ kế thừa từ MaterialTextSelectionControls (để tận dụng các logic mặc định của Material Design) và override các phương thức cần thiết. import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; // For TextSelectionHandleType // 1. Tạo một TextSelectionControls tùy chỉnh của riêng anh Creyt class MyCustomTextSelectionControls extends MaterialTextSelectionControls { /// Override phương thức buildToolbar để tùy chỉnh thanh công cụ. @override Widget buildToolbar( BuildContext context, Rect globalEditableRegion, Offset? lastTapDownPosition, TextSelectionDelegate delegate, ValueNotifier<bool> textRectsExist, ) { // Đây là nơi anh em mình "phù phép" cái thanh toolbar mặc định. // Thay vì trả về cái toolbar mặc định, mình sẽ tùy biến nó một chút. // Chúng ta sẽ dùng TextSelectionToolbar để giữ nguyên vị trí và hiệu ứng mặc định, // nhưng thay đổi nội dung bên trong. return TextSelectionToolbar( anchor: globalEditableRegion.center, // Vị trí neo cho toolbar children: <Widget>[ // Nút Copy với icon và màu sắc "Creyt-style" MaterialButton( onPressed: () => delegate.copySelection(SelectionChangedCause.toolbar), child: const Icon(Icons.copy_all, color: Colors.purple, size: 20), minWidth: 40, // Giảm kích thước nút height: 40, padding: EdgeInsets.zero, ), // Nút Paste với icon và màu sắc "Creyt-style" MaterialButton( onPressed: () => delegate.pasteSelection(SelectionChangedCause.toolbar), child: const Icon(Icons.paste_sharp, color: Colors.green, size: 20), minWidth: 40, height: 40, padding: EdgeInsets.zero, ), // Nút Cut với icon và màu sắc "Creyt-style" MaterialButton( onPressed: () => delegate.cutSelection(SelectionChangedCause.toolbar), child: const Icon(Icons.content_cut_sharp, color: Colors.red, size: 20), minWidth: 40, height: 40, padding: EdgeInsets.zero, ), // Thêm một nút tùy chỉnh "Highlight" - Đây mới là điểm nhấn! MaterialButton( onPressed: () { // Logic xử lý khi nhấn Highlight final String selectedText = delegate.textEditingValue.text.substring( delegate.textEditingValue.selection.start, delegate.textEditingValue.selection.end, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Anh Creyt đã highlight: "$selectedText"')), ); delegate.hideToolbar(); // Ẩn toolbar sau khi xử lý }, child: const Icon(Icons.highlight, color: Colors.blue, size: 20), minWidth: 40, height: 40, padding: EdgeInsets.zero, ), ], ); } /// Override phương thức buildHandle để tùy chỉnh các tay cầm (handles). @override Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { // Đây là nơi anh em mình "phù phép" mấy cái tay cầm (handles). // Mặc định thì nó là hình tròn, giờ mình thử đổi màu và thêm icon nhẹ nhàng. final Color handleColor = Theme.of(context).primaryColor; // Lấy màu chủ đạo của app return SizedBox( width: 24, // Kích thước handle height: 24, child: Center( child: Container( width: 20, height: 20, decoration: BoxDecoration( color: handleColor.withOpacity(0.8), // Màu handle tùy chỉnh shape: BoxShape.circle, // Giữ hình tròn ), child: Icon( // Đổi icon tùy theo loại handle (trái/phải) type == TextSelectionHandleType.left ? Icons.arrow_back_ios_new : Icons.arrow_forward_ios_new, size: 12, color: Colors.white, ), ), ), ); } } // 2. Ứng dụng TextSelectionControls tùy chỉnh vào MaterialApp class TextSelectionControlsExampleApp extends StatelessWidget { const TextSelectionControlsExampleApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Custom Text Selection', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.deepPurple, // Đây là chỗ quan trọng để áp dụng TextSelectionControls tùy chỉnh textSelectionTheme: TextSelectionThemeData( selectionControls: MyCustomTextSelectionControls(), // Áp dụng controls của mình selectionColor: Colors.deepPurple.withOpacity(0.3), // Màu nền khi chọn text cursorColor: Colors.deepPurple, // Màu con trỏ ), ), home: const HomePage(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelectionControls Demo của anh Creyt'), ), body: const Padding( padding: EdgeInsets.all(16.0), child: Column( children: [ TextField( decoration: InputDecoration( labelText: 'Thử gõ và chọn văn bản ở đây!', border: OutlineInputBorder(), ), maxLines: 5, controller: TextEditingController( text: 'Chào mấy đứa GenZ mê code! Đây là một đoạn văn bản ví dụ để mấy đứa ', ), ), SizedBox(height: 20), SelectableText( 'Có thể thử chọn, copy, paste, và xem cái thanh công cụ ' + 'tùy chỉnh của anh Creyt nó hoạt động như thế nào. ' + 'Hãy tự mình trải nghiệm và cảm nhận sự khác biệt nhé! ' + 'Flutter là một framework UI mạnh mẽ cho phép bạn xây dựng ứng dụng ' + 'đa nền tảng từ một codebase duy nhất. Học Flutter không khó, ' + 'chỉ cần có đam mê và một người thầy "chất" như anh Creyt là okela!', style: TextStyle(fontSize: 16), ), ], ), ), ); } } void main() { runApp(const TextSelectionControlsExampleApp()); } Trong ví dụ trên: MyCustomTextSelectionControls kế thừa MaterialTextSelectionControls để có sẵn các logic cơ bản. Anh Creyt override buildToolbar để tạo ra một TextSelectionToolbar với các MaterialButton tùy chỉnh, thêm nút "Highlight" "độc quyền" và đổi icon. Anh Creyt override buildHandle để thay đổi màu sắc và thêm icon vào các tay cầm chọn văn bản. Cuối cùng, áp dụng MyCustomTextSelectionControls này vào textSelectionTheme của MaterialApp để nó có hiệu lực trên toàn bộ ứng dụng. 3. Mẹo (Best Practices) từ anh Creyt "Trong lập trình, không phải cái gì tùy biến cũng là tốt, quan trọng là tùy biến đúng lúc, đúng chỗ!" Khi nào thì "đụng chạm": Chỉ nên tùy biến TextSelectionControls khi thực sự cần thiết, ví dụ để phù hợp với branding của ứng dụng (màu sắc, font chữ, icon), hoặc khi cần thêm các chức năng đặc biệt (như nút "Dịch", "Tìm kiếm trong từ điển", "Gửi nhanh qua Zalo"...). Đừng tùy biến chỉ vì muốn khác biệt mà không có lý do chính đáng, dễ làm người dùng bối rối. Giữ sự nhất quán: Nếu đã tùy biến, hãy đảm bảo nó nhất quán trên toàn bộ ứng dụng. Người dùng ghét sự "nửa vời" hoặc mỗi chỗ một kiểu. Điều này giúp trải nghiệm người dùng mượt mà và dễ đoán. Cân nhắc hiệu năng: Việc vẽ các widget phức tạp cho toolbar và handles có thể ảnh hưởng nhỏ đến hiệu năng, đặc biệt trên các thiết bị cũ hoặc khi có quá nhiều văn bản. Giữ cho UI đơn giản, hiệu quả và tránh các animation quá "nặng đô" cho các thành phần này. Accessibility (Khả năng tiếp cận): Đảm bảo các nút tùy chỉnh vẫn dễ sử dụng cho mọi người, bao gồm cả những người dùng có nhu cầu đặc biệt. Kích thước nút, độ tương phản màu sắc, và mô tả ngữ nghĩa (semantic labels) là những yếu tố quan trọng cần lưu ý. Kế thừa từ cái có sẵn: Thay vì viết lại từ đầu, hãy kế thừa từ MaterialTextSelectionControls hoặc CupertinoTextSelectionControls và chỉ override những phần mình muốn thay đổi. "Đừng cố gắng tự chế bánh xe khi đã có cái bánh xe chất lượng cao rồi, chỉ cần sơn phết lại thôi là đủ "chất" rồi!" 4. Ví dụ thực tế các ứng dụng/website đã ứng dụng "Mấy đứa thấy đó, mấy cái app "xịn xò" thường không chỉ dừng lại ở chức năng cơ bản đâu, họ luôn tìm cách tối ưu từng chút một để người dùng mê mẩn!" Ứng dụng soạn thảo văn bản chuyên nghiệp (Notion, Google Docs, Obsidian): Các ứng dụng này thường có thanh chọn văn bản "siêu cấp" với vô vàn tùy chọn không chỉ Copy/Paste mà còn định dạng (in đậm, in nghiêng, gạch chân), thêm link, comment, chuyển đổi heading, v.v. Ứng dụng học ngoại ngữ (Duolingo, Elsa Speak): Khi người dùng chọn một từ hoặc cụm từ, thanh công cụ có thể hiện thêm nút "Dịch", "Tra từ điển", "Nghe phát âm", giúp việc học trở nên tiện lợi hơn rất nhiều. Ứng dụng đọc sách điện tử (Kindle, Google Books): Khi chọn một đoạn văn bản trong sách, người dùng thường thấy các tùy chọn như "Highlight" (đánh dấu), "Ghi chú", "Tìm kiếm trên mạng", "Chia sẻ đoạn trích". Các trình duyệt web tùy chỉnh (Brave, Opera): Một số trình duyệt có thể thêm nút "Mở trong tab mới", "Chia sẻ nhanh" cho các đường link được chọn, hoặc "Tìm kiếm hình ảnh" khi chọn một hình ảnh. 5. Thử nghiệm và Hướng dẫn nên dùng cho case nào "Anh Creyt đã từng "nghịch" đủ kiểu với cái này rồi, và đây là kinh nghiệm xương máu để mấy đứa không bị "lạc trôi"!" Nên dùng TextSelectionControls tùy chỉnh khi: Branding mạnh mẽ: Khi ứng dụng của mấy đứa có một bộ nhận diện thương hiệu (brand identity) rất mạnh và muốn mọi chi tiết UI, dù là nhỏ nhất, cũng phải "thuần" brand, từ màu sắc, hình dạng của handles đến icon trên toolbar. Thêm chức năng độc đáo: Khi cần bổ sung các hành động đặc thù mà các nút mặc định không có. Ví dụ như nút "Highlight" trong ví dụ của anh Creyt, hoặc "Dịch", "Tra từ điển", "Chia sẻ nhanh"... Cải thiện trải nghiệm người dùng trong các ứng dụng chuyên biệt: Ví dụ, một trình soạn thảo code có thể thêm nút "Refactor", "Format code" ngay trên thanh chọn văn bản để tăng năng suất cho developer. Cải thiện khả năng tiếp cận (Accessibility): Đôi khi, các controls mặc định có thể quá nhỏ hoặc khó nhìn đối với một số người dùng. Tùy chỉnh có thể giúp tạo ra các controls lớn hơn, tương phản tốt hơn, hoặc có biểu tượng dễ hiểu hơn. Không nên dùng TextSelectionControls tùy chỉnh khi: Ứng dụng đơn giản, không có yêu cầu đặc biệt: Nếu ứng dụng chỉ cần các chức năng Copy/Paste cơ bản và không có yêu cầu về branding hay tính năng đặc thù, việc tùy chỉnh chỉ làm tốn thời gian và có thể gây ra lỗi không đáng có. Thời gian phát triển gấp rút: Việc tùy chỉnh UI luôn tốn thời gian và công sức kiểm thử. Nếu dự án đang "chạy deadline", hãy ưu tiên các chức năng cốt lõi trước. Tùy chỉnh mà không có kế hoạch rõ ràng: Việc tùy tiện thay đổi mà không có mục tiêu cụ thể dễ dẫn đến giao diện "lộn xộn", khó dùng và làm người dùng cảm thấy khó chịu. "Tùy biến mà không có chiến lược thì khác gì tự bắn vào chân mình đâu mấy đứa!" "Tóm lại, TextSelectionControls là một công cụ mạnh mẽ, nhưng hãy sử dụng nó một cách khôn ngoan. Hãy tự hỏi: 'Cái này có thật sự làm cho người dùng sướng hơn không, hay chỉ làm cho mình vui thôi?' Chúc mấy đứa code vui!" 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
TextSelection Flutter: Cầm Remote Chọn Chữ, Copy Cả Thế Giới!
22/03/2026

TextSelection Flutter: Cầm Remote Chọn Chữ, Copy Cả Thế Giới!

"TextSelection" là gì mà GenZ nào cũng phải biết? Nghe nè mấy đứa, có bao giờ mấy đứa lướt TikTok, thấy cái caption nào đó hay quá, muốn copy gửi crush hoặc làm quote story không? Hay đọc một bài báo, muốn highlight một câu thần chú để nhớ? Đó, cái hành động "chạm giữ, kéo kéo con trỏ xanh xanh đỏ đỏ, rồi bấm Sao chép" đó chính là TextSelection đó! Đơn giản mà "quyền năng" vãi chưởng luôn. Trong cái thế giới app "mượt mà" của GenZ, TextSelection nó giống như việc mình cho người dùng cái "remote control" để điều khiển nội dung vậy. Chữ nghĩa trên màn hình không còn là "đồ trưng bày" nữa, mà nó trở thành "dữ liệu tương tác" – có thể chọn, có thể copy, có thể cắt, có thể dán. Không có nó, app của mấy đứa sẽ giống như cái tivi bị mất remote, nhìn thì đẹp nhưng không làm ăn được gì nhiều đâu. Với Flutter, việc "trao quyền" TextSelection cho người dùng dễ như ăn kẹo, không cần phải "đau não" code từng tí một đâu. Mình đi sâu vào xem nó "triển" như nào nhé! Hướng dẫn "Triển Chiêu" TextSelection trong Flutter Flutter cung cấp cho chúng ta vài "chiêu" để "triển" TextSelection một cách "ngon lành cành đào": 1. Cơ bản nhất: SelectableText – "Chữ có thể chọn" Đây là "chiêu thức" đơn giản nhất khi mấy đứa muốn hiển thị một đoạn văn bản chỉ để đọc, nhưng lại muốn người dùng có thể chọn và copy nó. Nó giống như việc mình viết một cuốn sách, ai cũng đọc được, nhưng nếu muốn trích dẫn thì cứ tự nhiên chọn rồi copy. Code Ví Dụ: 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: 'TextSelection 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('TextSelection với SelectableText'), ), body: const Center( child: Padding( padding: EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'Đây là một đoạn văn bản bình thường, không thể chọn.', style: TextStyle(fontSize: 18), ), SizedBox(height: 20), SelectableText( 'Đây là đoạn văn bản "siêu cấp pro", có thể chọn và copy thoải mái! Thử bấm giữ và kéo xem nào.', textAlign: TextAlign.center, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.deepPurple), // Có thể tùy chỉnh hành vi chọn tại đây (ví dụ: onSelectionChanged) onSelectionChanged: (TextSelection selection, SelectionChangedCause? cause) { print('Vùng chọn đã thay đổi: ${selection.textInside(this.toString())}'); }, ), SizedBox(height: 20), Text( 'Nhớ là SelectableText chỉ dành cho văn bản "chỉ đọc" thôi nha mấy đứa!', style: TextStyle(fontStyle: FontStyle.italic), ), ], ), ), ), ); } } Giải thích: Đơn giản là thay Text('...') bằng SelectableText('...'). Thế là xong! Người dùng có thể bấm giữ và kéo để chọn văn bản. Mấy đứa còn có thể dùng onSelectionChanged để "hóng" xem người dùng đang chọn cái gì nữa đó. "Vui phết"! 2. Nâng cao hơn: TextField và TextFormField – "Chữ để nhập và chỉnh sửa" Khi mấy đứa muốn người dùng không chỉ chọn mà còn nhập liệu, chỉnh sửa (kiểu như chat box, ô tìm kiếm), thì TextField hoặc TextFormField là "chân ái". Mấy widget này mặc định đã có tính năng TextSelection "xịn sò" rồi, không cần làm gì thêm. Code Ví Dụ: 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: 'TextFormField Selection Demo', theme: ThemeData( primarySwatch: Colors.green, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { final TextEditingController _controller = TextEditingController(text: 'Thầy Creyt đẹp trai quá!'); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextSelection với TextFormField'), ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'Nhập gì đó vào đây, rồi thử chọn, cắt, copy, dán xem!', style: TextStyle(fontSize: 18), textAlign: TextAlign.center, ), const SizedBox(height: 20), TextFormField( controller: _controller, decoration: const InputDecoration( labelText: 'Cảm nghĩ về thầy Creyt?', border: OutlineInputBorder(), prefixIcon: Icon(Icons.edit), ), style: const TextStyle(fontSize: 16), // Mặc định TextSelection đã được bật. Mấy đứa có thể custom selectionControls nếu muốn thay đổi UI của các nút copy/paste. // selectionControls: MaterialTextSelectionControls(), // Dùng mặc định của Material Design onChanged: (text) { print('Nội dung đang nhập: $text'); }, ), const SizedBox(height: 20), ElevatedButton( onPressed: () { // Lấy vùng text đang được chọn (nếu có) final TextSelection selection = _controller.selection; if (selection.isValid && selection.textInside(_controller.text).isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Bạn vừa chọn: "${selection.textInside(_controller.text)}"')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Chưa có gì được chọn cả!')), ); } }, child: const Text('Xem Text Đang Chọn'), ), ], ), ), ), ); } } Giải thích: TextField và TextFormField sinh ra là để xử lý nhập liệu, nên việc chọn, cắt, copy, dán văn bản là tính năng cốt lõi của tụi nó. Mấy đứa không cần cấu hình gì thêm đâu, cứ dùng là nó tự động có. Nếu muốn "chơi trội" hơn, mấy đứa có thể custom cái selectionControls để thay đổi giao diện của mấy cái nút "Copy", "Paste" đó, nhưng thường thì dùng mặc định là "chuẩn bài" rồi. "Mẹo Vặt" Của Thầy Creyt: Dùng TextSelection cho "Chất" "Tối ưu" trải nghiệm người dùng (UX): Đừng biến việc chọn văn bản thành một "cuộc thi nhanh tay lẹ mắt". Đảm bảo vùng chọn dễ thấy, các "tay cầm" (selection handles) dễ kéo. Flutter mặc định đã làm khá tốt điều này, nhưng nếu mấy đứa custom UI thì nhớ để ý nha. "Khi nào thì cấm chọn?": Không phải cái gì cũng cho chọn đâu nha. Ví dụ, mấy cái mã OTP, mật khẩu, hay thông tin nhạy cảm của người dùng thì nên cấm TextSelection. Đừng để người dùng vô tình copy rồi làm lộ thông tin. "Hiệu suất" cho văn bản "siêu dài": Nếu mấy đứa có một đoạn SelectableText dài "dằng dặc" (kiểu như một cuốn tiểu thuyết), thì đôi khi việc render và xử lý vùng chọn có thể hơi "ngốn" tài nguyên. Hãy cân nhắc xem có thật sự cần SelectableText cho toàn bộ đoạn đó không, hay chỉ một phần thôi. "Phản hồi" khi chọn: Dùng onSelectionChanged để cung cấp phản hồi cho người dùng, ví dụ như hiển thị số ký tự đã chọn, hoặc một popup nhỏ với các tùy chọn khác (như chia sẻ, tìm kiếm...). "Ngầu" hơn nữa là tích hợp với các tính năng dịch thuật tức thì khi người dùng chọn một đoạn văn bản tiếng nước ngoài. "Học Hỏi" Từ Các Ứng Dụng "Đỉnh Cao" Zalo/Messenger/Facebook: Mấy cái app chat này là "bậc thầy" của TextSelection. Mấy đứa có thể bấm giữ tin nhắn để copy, hoặc trong ô nhập liệu thì thoải mái chọn, cắt, dán. Họ còn có thêm các tùy chọn như "Trả lời", "Chuyển tiếp" khi bạn chọn tin nhắn nữa đó. Đây là cách họ biến TextSelection từ một tính năng cơ bản thành một "công cụ" tương tác mạnh mẽ. Các trình duyệt web (Chrome, Safari): Đây là nơi TextSelection "lên ngôi". Mấy đứa đọc báo, xem tin tức, muốn lưu lại một đoạn nào đó thì cứ việc chọn, copy. Họ còn có tính năng "tìm kiếm nhanh" hoặc "chia sẻ" trực tiếp từ vùng chọn nữa. "Bá đạo" chưa? Các ứng dụng đọc sách/ghi chú: Kindle, Google Docs, Notion... đều dùng TextSelection để người dùng highlight, ghi chú, hoặc tìm kiếm từ khóa trong văn bản. Đây là những ví dụ điển hình về việc TextSelection được tích hợp sâu vào trải nghiệm đọc và làm việc. "Thử Nghiệm & Ứng Dụng Thực Tế": Khi nào "Show Hàng"? Dùng SelectableText khi nào? Hiển thị điều khoản sử dụng, chính sách bảo mật mà người dùng có thể muốn copy một phần. Các đoạn quote, trích dẫn, câu nói hay trong app của mấy đứa. Thông báo, hướng dẫn sử dụng mà người dùng có thể muốn sao chép để tra cứu sau. Nội dung bài viết, tin tức (nếu không có chức năng chỉnh sửa). Dùng TextField/TextFormField khi nào? Tất nhiên là khi mấy đứa cần ô nhập liệu rồi! Từ ô tìm kiếm, ô chat, đến form đăng ký, đăng nhập. Bất cứ nơi nào người dùng cần nhập và chỉnh sửa văn bản. Trong các ứng dụng ghi chú, soạn thảo văn bản. Khi nào thì không nên dùng hoặc cần cân nhắc đặc biệt? Nội dung nhạy cảm: Đã nói ở trên, mật khẩu, OTP, mã thẻ tín dụng... đừng bao giờ cho phép chọn và copy dễ dàng. Hiệu suất: Với các đoạn văn bản cực kỳ dài và phức tạp, hãy cân nhắc cách render hoặc chia nhỏ nội dung để tránh giật lag. Giao diện quá phức tạp: Đôi khi, việc có quá nhiều tính năng chọn/copy có thể làm rối giao diện. Hãy giữ mọi thứ đơn giản và trực quan nhất có thể. Thấy chưa, TextSelection không chỉ là một cái tính năng "nhỏ nhặt" đâu, nó là một phần quan trọng để làm cho app của mấy đứa "sống động" và "thân thiện" hơn với người dùng đó. Cứ "nghịch" nhiều vào, rồi mấy đứa sẽ thấy nó "lợi hại" đến mức nào! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

42 Đọc tiếp
TextHeightBehavior: Bí kíp cân chỉnh chữ đẹp như điêu khắc!
22/03/2026

TextHeightBehavior: Bí kíp cân chỉnh chữ đẹp như điêu khắc!

TextHeightBehavior: Stylist riêng cho từng con chữ của bạn! Chào các chiến thần code Gen Z! Anh Creyt biết mấy đứa hay gặp cái cảnh, nhìn cái UI trên Figma thì đẹp lung linh, đến lúc code ra Flutter thì tự nhiên có mấy cái khoảng trống 'vô hình' nó cứ đẩy chữ ra xa khỏi cái icon, hay cái border mình muốn. Cảm giác như chữ của mình nó bị 'dư thừa mỡ' ở trên đầu và dưới chân vậy đó. Khó chịu cực! Đó chính là lúc TextHeightBehavior - vị cứu tinh của những UI 'pixel-perfect' - xuất hiện. Hãy hình dung thế này: Nếu TextStyle.height là việc bạn chọn 'chiều cao' tổng thể cho từng dòng chữ (kiểu như bạn muốn một dòng chữ cao bao nhiêu mét trên sân khấu), thì TextHeightBehavior chính là cây kéo thần, thước đo chuẩn để bạn 'cắt tỉa' cái 'phần mỡ thừa' ở trên đỉnh đầu và dưới gót chân của cái 'chiều cao' đó, đảm bảo chữ của bạn 'ăn ảnh' nhất, không bị 'lệch sóng' với các element khác. TextHeightBehavior là gì và để làm gì? Đơn giản mà nói, TextHeightBehavior là một thuộc tính của widget Text và RichText trong Flutter, giúp bạn kiểm soát cách mà khoảng trống dọc (hay còn gọi là 'leading' trong typography) được tính toán và áp dụng xung quanh văn bản. Nó có hai 'công tắc' chính: applyHeightToFirstAscent: Cái này điều khiển xem khoảng trống thừa ở phía trên dòng chữ đầu tiên có được tính vào hay không. Đặt false nếu bạn muốn dòng chữ đầu tiên 'sát sạt' với cạnh trên của container chứa nó. applyHeightToLastDescent: Tương tự, cái này điều khiển xem khoảng trống thừa ở phía dưới dòng chữ cuối cùng có được tính vào hay không. Đặt false nếu bạn muốn dòng chữ cuối cùng 'sát sạt' với cạnh dưới của container. Khi bạn đặt TextStyle.height (ví dụ height: 1.5), tức là bạn muốn chiều cao dòng lớn hơn chiều cao thực tế của font chữ. Khoảng trống dư ra đó sẽ được chia đều trên và dưới glyph. TextHeightBehavior cho phép bạn 'triệt tiêu' phần khoảng trống đó ở đầu và cuối khối text, giúp text của bạn 'ngồi đúng chỗ' hơn, đặc biệt khi căn chỉnh với các widget khác có kích thước cố định. Code Ví Dụ Minh Họa (đừng quên dùng Container có màu nền để dễ hình dung 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( home: Scaffold( appBar: AppBar(title: const Text('TextHeightBehavior Demo')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // --- Ví dụ 1: Không dùng TextHeightBehavior (hoặc dùng mặc định) --- const Text('Text với height: 1.5 (mặc định)'), Container( color: Colors.red.withOpacity(0.2), // Màu nền để dễ nhìn bounding box child: const Text( 'Chào Gen Z!', style: TextStyle(fontSize: 30, height: 1.5), // height > 1.0 sẽ tạo thêm khoảng trống ), ), const SizedBox(height: 20), // --- Ví dụ 2: Dùng TextHeightBehavior để loại bỏ khoảng trống trên/dưới --- const Text('Text với height: 1.5 & TextHeightBehavior(false, false)'), Container( color: Colors.green.withOpacity(0.2), child: const Text( 'Chào Gen Z!', style: TextStyle( fontSize: 30, height: 1.5, ), textHeightBehavior: TextHeightBehavior( applyHeightToFirstAscent: false, // Loại bỏ khoảng trống trên dòng đầu applyHeightToLastDescent: false, // Loại bỏ khoảng trống dưới dòng cuối ), ), ), const SizedBox(height: 20), // --- Ví dụ 3: So sánh với Icon để thấy sự căn chỉnh --- const Text('So sánh text và icon (mặc định)'), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( color: Colors.blue.withOpacity(0.2), child: const Text( 'Icon align', // Text này có thể trông hơi lệch so với icon style: TextStyle( fontSize: 24, height: 1.2, ), ), ), const Icon(Icons.star, size: 24, color: Colors.blue), ], ), const SizedBox(height: 20), const Text('So sánh text và icon (với TextHeightBehavior)'), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( color: Colors.purple.withOpacity(0.2), child: const Text( 'Icon align', style: TextStyle( fontSize: 24, height: 1.2, ), textHeightBehavior: TextHeightBehavior( applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), ), ), const Icon(Icons.star, size: 24, color: Colors.purple), ], ), ], ), ), ), ); } } Mẹo vặt từ anh Creyt (Best Practices): Luôn dùng Container với màu nền: Đây là 'chiêu' thần thánh để bạn dễ dàng nhìn thấy cái 'bounding box' (khung bao quanh) thực sự của widget Text. Khi đó, mấy cái khoảng trống 'ma' nó ẩn mình sẽ lộ diện ngay! false thường là chân ái: Trong hầu hết các trường hợp bạn muốn text 'ngồi sát sạt' với các element khác, hoặc muốn kiểm soát chính xác khoảng cách, việc đặt applyHeightToFirstAscent: false và applyHeightToLastDescent: false sẽ mang lại kết quả mong muốn. Hiểu TextStyle.height: Nhớ rằng TextHeightBehavior chỉ điều chỉnh cách áp dụng cái height đó vào đầu và cuối khối text. Nếu height của bạn bằng 1.0, thì thường ít thấy sự khác biệt vì không có nhiều khoảng trống thừa để điều chỉnh. Tối ưu cho UI 'Pixel Perfect': Khi designer của bạn khó tính đến từng pixel, đây chính là công cụ bạn cần để 'chốt hạ' mọi yêu cầu về căn chỉnh. Ứng dụng thực tế: Ai đã dùng TextHeightBehavior? Thực ra, bất kỳ ứng dụng nào có giao diện người dùng được thiết kế tỉ mỉ, chặt chẽ đều ít nhiều 'đụng chạm' đến việc kiểm soát khoảng cách text. Ví dụ điển hình: Các ứng dụng tin tức (Google News, Feedly): Tiêu đề, mô tả ngắn gọn của bài viết thường được căn chỉnh rất sát với ảnh thumbnail hoặc các dòng phụ đề để tối ưu không gian hiển thị và tạo sự gọn gàng, chuyên nghiệp. Ứng dụng mạng xã hội (Instagram, Twitter): Tên người dùng, hashtag, caption thường được đặt cạnh icon profile, icon tương tác. Nếu không kiểm soát khoảng trống, chúng sẽ trông lệch lạc, thiếu thẩm mỹ. Màn hình đăng nhập/đăng ký: Các label của input field, nút bấm, hay các dòng text thông báo nhỏ cần được căn chỉnh chính xác để tạo cảm giác chuyên nghiệp, dễ đọc. Thử nghiệm và Nên dùng cho case nào? Thử nghiệm: Anh Creyt khuyến khích mấy đứa cứ mạnh dạn thử nghiệm! Khi nào thấy text của mình trông 'lạc quẻ' hoặc có khoảng trống kỳ lạ ở trên/dưới mà không biết từ đâu ra, hãy thử bật/tắt applyHeightToFirstAscent và applyHeightToLastDescent. Đặc biệt là khi bạn đang dùng TextStyle.height để điều chỉnh khoảng cách dòng. Nên dùng khi: Bạn đang xây dựng một UI 'pixel-perfect' và gặp vấn đề với khoảng trống thừa trên/dưới text mà không giải thích được. Bạn cần căn chỉnh text với các widget khác (như Icon, Image) trong một Row hoặc Column và muốn chúng 'ngồi thẳng hàng' một cách tuyệt đối, không bị cái 'khoảng trống ma' đẩy đi. Khi bạn dùng TextStyle.height để tăng khoảng cách dòng (line height) nhưng không muốn khoảng cách đó ảnh hưởng đến vị trí tổng thể của khối text, tức là bạn chỉ muốn tăng khoảng cách giữa các dòng bên trong chứ không muốn khối text tự nhiên 'phình to' ra ở trên và dưới. Khi bạn có nhiều dòng text và muốn kiểm soát chặt chẽ khoảng cách giữa các khối text hoặc giữa text và các border của container. Nhớ nhé, TextHeightBehavior không phải là cái gì quá phức tạp, nó chỉ là một công cụ nhỏ nhưng 'có võ' giúp bạn làm chủ typography trong Flutter. Dùng đúng lúc, đúng chỗ, UI của bạn sẽ 'lên một tầm cao mới' ngay! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

35 Đọc tiếp
Flutter: Bắt Sóng Từng Phím Gõ Với TextEditingControllerListener
22/03/2026

Flutter: Bắt Sóng Từng Phím Gõ Với TextEditingControllerListener

Chào các con chiên của Creyt! Hôm nay, chúng ta sẽ "bóc phốt" một thằng cha ít được nhắc tên nhưng lại cực kỳ quyền lực trong thế giới Flutter: đó là TextEditingControllerListener. Nghe cái tên thì dài ngoằng như sớ táo quân, nhưng thật ra nó là một "thám tử tư" cực kỳ mẫn cán, chuyên nghe lén mọi thứ bạn gõ vào ô nhập liệu (TextField). 1. TextEditingControllerListener là "Thằng" Nào? Để Làm Gì? Để hiểu thằng TextEditingControllerListener này, trước hết phải nói về TextEditingController cái đã. Tưởng tượng TextField của Flutter như một cái hộp thư thoại, nơi bạn để lại tin nhắn. Còn TextEditingController chính là cái "máy nghe" của hộp thư đó. Mọi ký tự bạn gõ vào TextField đều được TextEditingController thu nhận và quản lý. Thế còn Listener? À, nó chính là "tai mắt" của bạn, được gắn vào cái "máy nghe" kia (TextEditingController). Nhiệm vụ của nó là nghe ngóng liên tục, và khi TextEditingController báo "Ê, có đứa vừa gõ gì đó kìa!" thì thằng Listener sẽ lập tức "nhảy dựng" lên, thông báo cho bạn biết để bạn có thể phản ứng. Kiểu như bạn là một "stalker" chuyên nghiệp, không bỏ sót một động thái nào của đối tượng trên mạng xã hội vậy. Nó giúp bạn: Phản ứng tức thì: Ngay khi người dùng gõ/xóa một ký tự, bạn có thể biết ngay. Cập nhật UI động: Thay đổi giao diện dựa trên nội dung nhập liệu (ví dụ: nút "Gửi" sáng lên khi có chữ). Kiểm tra dữ liệu real-time: Báo lỗi ngay lập tức nếu người dùng nhập sai định dạng. Nói tóm lại, nó là công cụ để bạn biến cái TextField tĩnh thành một cái TextField "biết suy nghĩ" và "biết phản ứng" theo từng phím gõ của người dùng. Ngầu lòi chưa? 2. Bắt Sóng Từng Phím Gõ: Code Ví Dụ Từ A-Z Giờ thì lý thuyết đủ rồi, dân IT mà, phải code mới sướng tay! Đây là cách bạn gắn một cái "tai mắt" vào TextEditingController: import 'package:flutter/material.dart'; class MyInputScreen extends StatefulWidget { const MyInputScreen({Key? key}) : super(key: key); @override State<MyInputScreen> createState() => _MyInputScreenState(); } class _MyInputScreenState extends State<MyInputScreen> { // 1. Khai báo TextEditingController late TextEditingController _textController; String _currentInput = ''; // Để lưu trữ giá trị hiện tại của TextField @override void initState() { super.initState(); // 2. Khởi tạo Controller _textController = TextEditingController(); // 3. Gắn Listener vào Controller _textController.addListener(_onTextChanged); } // Hàm được gọi mỗi khi nội dung TextField thay đổi void _onTextChanged() { // In ra giá trị hiện tại của TextField để kiểm tra print('Giá trị hiện tại: ${_textController.text}'); // Cập nhật UI (nếu cần) bằng setState setState(() { _currentInput = _textController.text; // Cập nhật biến để hiển thị }); } @override void dispose() { // RẤT QUAN TRỌNG: Giải phóng Controller khi Widget không còn được sử dụng // Nếu không, nó sẽ gây rò rỉ bộ nhớ (memory leak)! _textController.removeListener(_onTextChanged); // Gỡ listener trước khi dispose _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('TextEditingController Listener Demo'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextField( controller: _textController, // Gắn Controller vào TextField decoration: const InputDecoration( labelText: 'Gõ gì đó vào đây, Creyt đang nghe lén...', // Thêm label cho dễ hiểu border: OutlineInputBorder(), ), ), const SizedBox(height: 20), // Hiển thị giá trị đang được gõ Text( 'Bạn đang gõ: "$_currentInput"', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Text( 'Số ký tự: ${_currentInput.length}', style: const TextStyle(fontSize: 16, color: Colors.grey[700]), ), ], ), ), ); } } // Để chạy ví dụ này, bạn có thể dùng MaterialApp và Home là MyInputScreen void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: MyInputScreen(), ); } } Trong đoạn code trên: Chúng ta khai báo _textController và khởi tạo nó trong initState(). Dòng _textController.addListener(_onTextChanged); là chìa khóa! Nó bảo Flutter rằng: "Mỗi khi có thay đổi trong _textController, hãy gọi hàm _onTextChanged cho tôi." Trong _onTextChanged(), chúng ta truy cập _textController.text để lấy giá trị hiện tại và dùng setState() để cập nhật UI, hiển thị nội dung bạn đang gõ. Và đừng bao giờ quên dispose()! Nó giống như việc bạn tắt máy nghe lén khi không dùng nữa để đỡ tốn pin và không bị "nhờn" bộ nhớ. 3. "Mẹo Vặt" Của Dân Chuyên: Dùng Sao Cho Chuẩn? Sức mạnh đi kèm trách nhiệm, các con ạ. Dùng TextEditingControllerListener cũng có vài "mẹo" để không bị ăn "gạch": dispose() là chân ái: Cái này Creyt phải nhắc đi nhắc lại. Nếu không dispose() TextEditingController, nó sẽ tiếp tục tồn tại trong bộ nhớ ngay cả khi widget đã bị loại bỏ, dẫn đến rò rỉ bộ nhớ (memory leak). Hậu quả là app của bạn chạy ngày càng chậm, lag, và cuối cùng thì... crash. Luôn luôn _textController.removeListener(_onTextChanged); trước khi _textController.dispose(); để đảm bảo sạch sẽ. Performance không đùa được đâu: Mỗi lần bạn gõ một ký tự, hàm _onTextChanged sẽ được gọi. Nếu trong hàm này bạn thực hiện các tác vụ nặng (ví dụ: gọi API, tính toán phức tạp), app của bạn sẽ lag "tung chảo". Giải pháp: Hãy cân nhắc dùng debouncing hoặc throttling. Tức là, thay vì phản ứng tức thì, bạn đợi một khoảng thời gian nhỏ (ví dụ 300-500ms) sau khi người dùng ngừng gõ rồi mới thực hiện tác vụ. Điều này đặc biệt hữu ích cho các tính năng tìm kiếm real-time. setState hay "state management" khác?: Với các tác vụ đơn giản, cục bộ như đếm ký tự hay bật/tắt nút, setState trong listener là đủ. Nhưng nếu bạn cần quản lý trạng thái phức tạp hơn, ảnh hưởng đến nhiều widget hoặc cần chia sẻ dữ liệu, hãy nghĩ đến các giải pháp quản lý trạng thái chuyên nghiệp hơn như Provider, Bloc/Cubit, Riverpod, v.v. Chúng sẽ giúp code của bạn sạch sẽ, dễ bảo trì hơn. 4. Ứng Dụng Thực Tế: "Bóc Phốt" Các App Lớn Dùng Nó Thế Nào Bạn có thể thấy TextEditingControllerListener (hoặc cơ chế tương tự) ở khắp mọi nơi mà không hề hay biết: Thanh tìm kiếm (Google, Shopee, Tiki): Khi bạn gõ vào ô tìm kiếm, danh sách gợi ý hiện ra ngay lập tức. Đó chính là nhờ cơ chế "nghe lén" này, nó gửi từ khóa bạn gõ lên server để lấy gợi ý. Kiểm tra định dạng email/mật khẩu (Facebook, Instagram): Bạn nhập email, nếu sai định dạng @ hay .com, nó báo lỗi đỏ lòm ngay lập tức. Mật khẩu yếu, nó cũng "nhắc nhở" bạn tăng cường sức mạnh cho mật khẩu. Đếm ký tự (Twitter/X, Messenger): Khi bạn viết tweet hay tin nhắn, nó hiển thị số ký tự còn lại hoặc đã gõ. Dễ hiểu rồi ha. Auto-suggest/Autocomplete: Khi bạn gõ tên thành phố, nó tự động gợi ý các thành phố khác. Tiện lợi vô cùng! 5. "Creyt's Test Lab": Khi Nào Dùng, Khi Nào Nên Né? Thằng TextEditingControllerListener này là con dao hai lưỡi, dùng đúng thì bá đạo, dùng sai thì "toang". Nên dùng khi: Cập nhật UI cục bộ, tức thì: Ví dụ: hiển thị số ký tự, bật/tắt nút "Gửi", đổi màu viền input khi nhập đúng/sai. Xử lý logic đơn giản, không tốn tài nguyên: Các tác vụ không đòi hỏi tính toán phức tạp hay gọi mạng liên tục. Phản hồi nhanh cho người dùng: Giúp cải thiện trải nghiệm người dùng bằng cách cung cấp feedback ngay lập tức. Nên né (hoặc cần cân nhắc kỹ) khi: Tác vụ nặng, tốn tài nguyên: Nếu mỗi lần gõ mà bạn lại gọi API hay xử lý dữ liệu lớn, hãy dùng debouncing hoặc chuyển logic ra ngoài, chỉ dùng listener để kích hoạt sự kiện. Quản lý trạng thái toàn cục (global state): Nếu sự thay đổi của một TextField ảnh hưởng đến nhiều phần khác nhau của ứng dụng, hoặc các màn hình khác, thì setState trong listener sẽ không đủ "đô". Lúc này, các giải pháp state management chuyên nghiệp sẽ là lựa chọn tốt hơn nhiều. Nói tóm lại, TextEditingControllerListener là một công cụ mạnh mẽ, nhưng hãy dùng nó một cách thông minh và có trách nhiệm. Nó là người bạn đồng hành tuyệt vời cho những tác vụ nhỏ, nhanh gọn, giúp app của bạn trở nên sống động và tương tác hơn. Nhớ kỹ những gì Creyt đã dặn dò nhé, các con của thầy! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

39 Đọc tiếp
TabBarTheme: Stylist Riêng Cho Thanh Điều Hướng App Flutter
22/03/2026

TabBarTheme: Stylist Riêng Cho Thanh Điều Hướng App Flutter

TabBarTheme: "Nhà Thiết Kế Nội Thất" Đẳng Cấp Cho Thanh Điều Hướng Của Bạn Chào các chiến hữu của Creyt! Hôm nay, chúng ta sẽ cùng "mổ xẻ" một khái niệm tuy nhỏ mà có võ, giúp app Flutter của bạn "lột xác" về mặt thẩm mỹ: TabBarTheme. Nghe cái tên thì có vẻ học thuật, nhưng cứ tưởng tượng thế này: Nếu TabBar (cái thanh điều hướng mà bạn hay thấy ở trên đầu hoặc dưới cùng của app, có nhiều tab để chuyển đổi nội dung ấy) là một cái kệ sách đa năng, mỗi ngăn là một chủ đề thì TabBarTheme chính là nhà thiết kế nội thất kiêm stylist riêng cho cái kệ sách đó vậy. Nói một cách genZ hơn, thay vì bạn phải ngồi chỉnh màu sắc, kiểu chữ, hay cái gạch chân của từng tab một (như kiểu mỗi lần mua cái ghế lại phải đi sơn lại, mua cái bàn cũng thế), TabBarTheme sẽ cho phép bạn "set kèo" một lần duy nhất cho toàn bộ các TabBar trong ứng dụng của mình. Nó giúp app của bạn trông "có gu" hơn, "ton-sur-ton" hơn, không bị "mỗi đứa một kiểu" nhìn rất "lạc quẻ". Tóm lại, TabBarTheme sinh ra là để: Đồng bộ hóa giao diện: Đảm bảo tất cả các TabBar trong app của bạn có một phong cách nhất quán, từ màu chữ, màu icon, đến kiểu indicator (cái gạch chân hoặc khối màu khi tab được chọn). Đây là yếu tố then chốt để xây dựng một trải nghiệm người dùng (UX) mượt mà và chuyên nghiệp. Tiết kiệm thời gian và code: Thay vì lặp đi lặp lại các thuộc tính styling cho từng TabBar riêng lẻ, bạn chỉ cần định nghĩa một lần trong ThemeData và áp dụng cho toàn bộ. Dễ dàng thay đổi và bảo trì: Khi sếp "đổi ý" về màu sắc branding, bạn chỉ cần sửa một chỗ duy nhất, thay vì phải "lặn lội" vào từng file để tìm và sửa thủ công. Code Ví Dụ Minh Họa: "Lên Đồ" Cho TabBar Để TabBarTheme phát huy tác dụng, chúng ta sẽ đặt nó vào trong ThemeData của MaterialApp. Đây chính là "sổ tay thiết kế tổng thể" của cả căn nhà ứng dụng bạn đấy. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TabBarTheme Demo by Anh Creyt', theme: ThemeData( // Đây là nơi "phù phép" cho TabBar của bạn! tabBarTheme: TabBarTheme( indicatorColor: Colors.deepOrange, // Màu gạch chân khi tab được chọn labelColor: Colors.deepOrange, // Màu chữ/icon khi tab được chọn unselectedLabelColor: Colors.grey, // Màu chữ/icon khi tab không được chọn labelStyle: const TextStyle( fontWeight: FontWeight.bold, // Chữ đậm khi được chọn fontSize: 16, ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.normal, // Chữ thường khi không được chọn fontSize: 14, ), indicatorSize: TabBarIndicatorSize.tab, // Kích thước gạch chân (phủ hết tab) dividerColor: Colors.transparent, // Loại bỏ đường kẻ ngang mặc định overlayColor: MaterialStateProperty.resolveWith<Color?>( (Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return Colors.deepOrange.withOpacity(0.1); // Hiệu ứng khi di chuột } return null; }, ), // indicator: BoxDecoration( // Bạn có thể tùy chỉnh indicator phức tạp hơn // borderRadius: BorderRadius.circular(10), // color: Colors.deepOrange.withOpacity(0.2), // ), tabAlignment: TabAlignment.fill, // Các tab sẽ giãn ra để lấp đầy không gian ), useMaterial3: true, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Ứng dụng của Anh Creyt'), bottom: TabBar( controller: _tabController, tabs: const [ Tab(icon: Icon(Icons.home), text: 'Trang Chủ'), Tab(icon: Icon(Icons.favorite), text: 'Yêu Thích'), Tab(icon: Icon(Icons.settings), text: 'Cài Đặt'), ], ), ), body: TabBarView( controller: _tabController, children: const [ Center(child: Text('Nội dung Trang Chủ', style: TextStyle(fontSize: 24))), Center(child: Text('Nội dung Yêu Thích', style: TextStyle(fontSize: 24))), Center(child: Text('Nội dung Cài Đặt', style: TextStyle(fontSize: 24))), ], ), ); } } Trong ví dụ trên, chúng ta đã biến hóa TabBar từ một "cô bé lọ lem" thành một "nàng công chúa" với màu sắc cam nổi bật khi được chọn, chữ đậm hơn, và một hiệu ứng hover nhẹ nhàng. Đặc biệt, dividerColor: Colors.transparent giúp loại bỏ cái đường kẻ mờ mờ khó chịu mặc định của TabBar đấy! Mẹo Vặt Từ Anh Creyt (Best Practices) "Sổ Tay" Tổng Thể: Luôn định nghĩa TabBarTheme (và các theme khác) ở một nơi tập trung, thường là trong ThemeData của MaterialApp. Bạn có thể tách riêng ra một file theme.dart để quản lý dễ dàng hơn khi app lớn lên. "Thần Chú" Hot Reload: Khi đang "phù phép" với theme, đừng ngại chỉnh sửa và dùng hot reload. Flutter sẽ "biến hình" ngay lập tức, giúp bạn xem trước kết quả siêu tốc. "Hai Bộ Cánh" Sáng Tối: Kết hợp TabBarTheme với ThemeMode để tạo ra trải nghiệm Light Mode và Dark Mode "chuẩn chỉnh". Mỗi ThemeData (cho sáng và tối) sẽ có một TabBarTheme riêng, app của bạn sẽ tự động "thay áo" cho phù hợp với sở thích của người dùng. "Đừng Ngại Sáng Tạo" Với indicator: Thay vì chỉ dùng indicatorColor, bạn hoàn toàn có thể dùng thuộc tính indicator với một BoxDecoration để tạo ra những hiệu ứng gạch chân "độc lạ bình dương" hơn, ví dụ như bo góc, đổ bóng, hoặc thậm chí là một hình ảnh nhỏ. tabAlignment "Thần Kỳ": Thuộc tính này giúp bạn kiểm soát cách các tab được phân bổ trong TabBar. TabAlignment.fill là lựa chọn phổ biến để các tab "lấp đầy" chiều rộng, còn TabAlignment.start hoặc TabAlignment.center sẽ giữ các tab ở kích thước tự nhiên của chúng. Ứng Dụng Thực Tế: "Thế Giới Phẳng" Của TabBarTheme Bạn có thể thấy TabBarTheme (hoặc các cơ chế tương tự trong các framework khác) ở khắp mọi nơi trên các ứng dụng bạn dùng hàng ngày: Instagram/Facebook: Thanh điều hướng dưới cùng (Bottom Navigation Bar) hoặc các tab ở trên cùng của trang cá nhân đều có sự nhất quán về màu sắc, icon khi chọn/không chọn. TabBarTheme là công cụ giúp các nhà phát triển Flutter đạt được sự đồng bộ này. Các ứng dụng thương mại điện tử (Shopee, Tiki, Lazada): Thanh điều hướng chính luôn được "chăm chút" để phù hợp với màu sắc thương hiệu, giúp người dùng dễ dàng nhận diện và điều hướng. Google Chrome (trên di động): Các tab trình duyệt được quản lý và hiển thị một cách thống nhất, với các trạng thái khác nhau (tab đang chọn, tab nền). Ngay cả các website dạng Single Page Application (SPA) xây dựng bằng React, Vue hay Angular cũng có các cơ chế styling tập trung tương tự để đảm bảo các thành phần điều hướng trông "có hồn" và thống nhất. Thử Nghiệm Và Nên Dùng Cho Case Nào? Anh Creyt nhớ "ngày xưa" khi Flutter còn "non trẻ" hoặc khi làm dự án "mì ăn liền" cho khách hàng, nhiều lúc anh phải ngồi chỉnh tay từng cái TabBar một. Mỗi lần khách hàng "khó tính" đòi đổi màu branding cái là "mồ hôi hột" chảy ròng ròng vì phải Ctrl+F từng file. Từ khi Theme nói chung và TabBarTheme nói riêng ra đời, cuộc đời lập trình viên "nở hoa" hơn hẳn. Bạn nên "triển" TabBarTheme ngay và luôn cho các trường hợp sau: Ứng dụng có nhiều TabBar: Đây là lúc TabBarTheme tỏa sáng nhất. Thay vì phải "dán" các thuộc tính labelColor, indicatorColor... vào từng TabBar riêng lẻ, bạn chỉ cần "set up" một lần duy nhất tại ThemeData. Yêu cầu branding mạnh mẽ: Nếu ứng dụng của bạn cần thể hiện rõ màu sắc, font chữ, hoặc một phong cách thiết kế đặc trưng của thương hiệu, TabBarTheme là "công cụ vàng" để đảm bảo thanh điều hướng luôn đúng "tông". Hỗ trợ Dark/Light Mode: Như đã nói ở phần mẹo vặt, TabBarTheme là một phần của ThemeData, nên nó sẽ tự động "nhảy số" khi người dùng chuyển đổi giữa chế độ sáng và tối, mang lại trải nghiệm liền mạch. Dự án quy mô lớn, cần khả năng mở rộng: Khi ứng dụng của bạn phát triển, việc quản lý theme tập trung sẽ giúp code sạch sẽ, dễ bảo trì và mở rộng hơn rất nhiều. Đó, các chiến hữu thấy không? TabBarTheme không chỉ là một cái tên khô khan, nó là một "nghệ sĩ" thầm lặng giúp app của chúng ta trông "ngon lành cành đào" hơn rất nhiều. Hãy tận dụng nó để "nâng tầm" sản phẩm của mình nhé! Chúc các bạn code vui vẻ! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

36 Đọc tiếp
TabBarIndicatorSize: Nâng tầm TabBar Flutter của bạn!
22/03/2026

TabBarIndicatorSize: Nâng tầm TabBar Flutter của bạn!

Chào các "coder nhí" của Creyt! Hôm nay chúng ta sẽ "mổ xẻ" một chi tiết nhỏ nhưng có võ trong thế giới Flutter UI: TabBarIndicatorSize. Nghe tên thì hơi "academic" nhưng thực ra nó cực kỳ gần gũi và quan trọng để giao diện của bạn trông "có gu" hơn. TabBarIndicatorSize là cái gì mà nghe "ngầu" vậy? Để dễ hình dung, các bạn cứ tưởng tượng cái TabBar trong app của mình như một cái bảng điều khiển trên máy bay vậy. Mỗi cái nút trên bảng là một Tab (ví dụ: Trang chủ, Cài đặt, Profile). Khi bạn chọn một nút, sẽ có một cái đèn báo hiệu "Mày đang ở đây này!". Cái đèn báo hiệu đó chính là cái Indicator. Vậy TabBarIndicatorSize chính là cái công tắc chỉnh kích thước cho cái đèn báo hiệu đó! Nó giúp bạn quyết định xem cái đèn đó sẽ to bằng cả cái nút (tab) hay chỉ bé tí xíu bằng đúng cái chữ (label) trên nút thôi. Đơn giản vậy thôi đó! Nó dùng để làm gì? Đơn giản là để giao diện của bạn đẹp hơn, trực quan hơn và "ăn khớp" hơn với thiết kế tổng thể. Một chi tiết nhỏ nhưng lại quyết định cái "vibe" của cả cái app đó! Flutter cung cấp cho chúng ta 3 giá trị chính để chơi với TabBarIndicatorSize: TabBarIndicatorSize.tab: Indicator sẽ có chiều rộng bằng toàn bộ chiều rộng của Tab. TabBarIndicatorSize.label: Indicator sẽ có chiều rộng bằng chiều rộng của nội dung (label) bên trong Tab. TabBarIndicatorSize.automatic: Thường thì nó sẽ hoạt động giống tab, tùy thuộc vào cách Flutter tính toán. Code Ví Dụ Minh Họa: "Thấy tận mắt, sờ tận tay" Giờ thì chúng ta cùng nhau "thực chiến" để xem nó hoạt động như thế nào nhé. Creyt sẽ tạo một ứng dụng Flutter đơn giản với TabBar và thử nghiệm các loại TabBarIndicatorSize khác nhau. import 'package:flutter/material.h'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TabBarIndicatorSize Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TabBarIndicatorSizeScreen(), ); } } class TabBarIndicatorSizeScreen extends StatefulWidget { const TabBarIndicatorSizeScreen({super.key}); @override State<TabBarIndicatorSizeScreen> createState() => _TabBarIndicatorSizeScreenState(); } class _TabBarIndicatorSizeScreenState extends State<TabBarIndicatorSizeScreen> with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Creyt dạy TabBarIndicatorSize'), elevation: 0, bottom: TabBar( controller: _tabController, tabs: const [ Tab(icon: Icon(Icons.home), text: 'Trang Chủ'), Tab(icon: Icon(Icons.settings), text: 'Cài Đặt'), Tab(icon: Icon(Icons.person), text: 'Profile'), ], indicatorColor: Colors.deepOrange, // Màu của indicator indicatorWeight: 4.0, // Độ dày của indicator labelColor: Colors.deepOrange, // Màu chữ khi tab được chọn unselectedLabelColor: Colors.grey, // Màu chữ khi tab không được chọn // Đây là chỗ chúng ta chơi với TabBarIndicatorSize! indicatorSize: TabBarIndicatorSize.label, // <= THAY ĐỔI Ở ĐÂY: .tab hoặc .label ), ), body: TabBarView( controller: _tabController, children: const [ Center(child: Text('Nội dung Tab Trang Chủ', style: TextStyle(fontSize: 24, color: Colors.deepOrange))), Center(child: Text('Nội dung Tab Cài Đặt', style: TextStyle(fontSize: 24, color: Colors.deepOrange))), Center(child: Text('Nội dung Tab Profile', style: TextStyle(fontSize: 24, color: Colors.deepOrange))), ], ), ); } } Giải thích code: Chúng ta tạo một DefaultTabController (hoặc TabController như trong ví dụ) để quản lý các tab. Trong TabBar, tabs là danh sách các Tab của chúng ta. indicatorColor và indicatorWeight giúp chúng ta tô màu và chỉnh độ dày cho cái "đèn báo" để dễ nhìn hơn. Và quan trọng nhất, dòng indicatorSize: TabBarIndicatorSize.label, chính là nơi bạn "xoay chuyển tình thế". Hãy thử đổi TabBarIndicatorSize.label thành TabBarIndicatorSize.tab và chạy lại app để xem sự khác biệt nhé! Mẹo hay từ Creyt (Best Practices) để "chiến" với TabBarIndicatorSize "Nhất quán là sức mạnh": Đừng hôm nay dùng label, mai lại dùng tab trong cùng một app. Hãy chọn một phong cách và đi theo nó xuyên suốt để UI của bạn trông chuyên nghiệp và dễ hiểu. "Đọc được là thắng": Dù bạn chọn kích thước nào, hãy đảm bảo rằng indicator không che mất nội dung của tab hoặc gây khó chịu khi nhìn. Đôi khi, một indicator quá to lại làm rối mắt. "Hiểu ngữ cảnh": Nếu các tab của bạn chủ yếu là icon hoặc các text ngắn, có độ dài tương đương nhau, TabBarIndicatorSize.tab thường là lựa chọn an toàn, tạo cảm giác liền mạch. "Tinh tế với label": Nếu các tab của bạn có text dài ngắn khác nhau và bạn muốn indicator "ôm" sát lấy chữ, TabBarIndicatorSize.label sẽ giúp UI trông gọn gàng và tinh tế hơn. Nhưng hãy cẩn thận, nếu text quá ngắn, indicator cũng sẽ rất ngắn, có thể khó nhìn. "Kết hợp thần công": Đừng quên kết hợp indicatorSize với indicatorColor và indicatorWeight. Giống như bạn mặc một bộ đồ đẹp, phải có phụ kiện đi kèm mới "chuẩn bài"! Ứng dụng thực tế: "Ai đã dùng rồi?" Các bạn có để ý không, rất nhiều ứng dụng "khủng" mà chúng ta dùng hàng ngày đều sử dụng TabBar và tùy chỉnh indicator của nó: TikTok / Instagram: Các tab điều hướng chính (Home, Explore, Reels, Profile) ở dưới đáy thường sử dụng indicator rất tinh tế, đôi khi là label hoặc một biến thể tab được tùy chỉnh sâu để tạo điểm nhấn cho tab hiện tại. Shopee / Lazada: Trong các trang danh mục sản phẩm hoặc quản lý đơn hàng, các tab "Đang giao", "Đã giao", "Hủy"... thường có indicator giúp người dùng dễ dàng theo dõi trạng thái. Google Play Store / App Store: Các tab phân loại ứng dụng (Games, Apps, Books) cũng có indicator để chỉ ra phần đang được xem. Thử nghiệm và Nên dùng cho case nào? Creyt luôn khuyến khích các bạn "vọc vạch" và tự mình thử nghiệm. Đừng ngại thay đổi các giá trị trong code và chạy thử trên emulator hoặc điện thoại thật. Chỉ khi tự mình trải nghiệm, các bạn mới thực sự hiểu được sự khác biệt và tìm ra cái nào phù hợp nhất với thiết kế của mình. Nên dùng TabBarIndicatorSize.tab khi: Các tab của bạn chủ yếu là icon hoặc text rất ngắn, và bạn muốn một indicator rõ ràng, chiếm trọn không gian của tab. Bạn muốn tạo cảm giác mạnh mẽ, rõ ràng cho từng lựa chọn. Ví dụ: Một thanh điều hướng dưới đáy (BottomNavigationBar) được tùy biến thành TabBar. Nên dùng TabBarIndicatorSize.label khi: Các tab của bạn có nội dung text thay đổi về độ dài và bạn muốn indicator chỉ "ôm" lấy phần chữ để tạo sự gọn gàng, tinh tế. Bạn muốn một thiết kế tối giản, hiện đại. Ví dụ: Các tab lọc sản phẩm theo tên (Phổ biến, Mới nhất, Giá tăng dần) trong một ứng dụng mua sắm. Lời khuyên từ Creyt: Hãy bắt đầu với TabBarIndicatorSize.label nếu bạn muốn sự tinh tế và gọn gàng. Nếu thấy khó nhìn hoặc cần sự rõ ràng hơn, hãy chuyển sang TabBarIndicatorSize.tab. Quan trọng là phải thử nghiệm và phù hợp với tổng thể UI/UX của app bạn! Vậy đó, TabBarIndicatorSize tuy là một chi tiết nhỏ nhưng lại là "vũ khí bí mật" giúp bạn "đánh bóng" giao diện TabBar của mình lên một tầm cao mới. Hãy áp dụng ngay vào dự án của mình và khoe với Creyt nhé! Happy coding! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

38 Đọc tiếp
TableRowInkWell: Biến Bảng Biểu Thành Sân Chơi Cảm Ứng Của Gen Z!
22/03/2026

TableRowInkWell: Biến Bảng Biểu Thành Sân Chơi Cảm Ứng Của Gen Z!

Chào các 'con giời' của Creyt! Hôm nay, chúng ta sẽ 'phá băng' một khái niệm nghe thì hơi 'nghiêm túc' nhưng thực ra lại cực kỳ 'chill' và hữu ích trong Flutter: TableRowInkWell. Nghe cái tên đã thấy 'hàn lâm' rồi đúng không? Nhưng đừng lo, Creyt sẽ biến nó thành món ăn dễ nuốt nhất, đảm bảo xong bài này là các bạn 'flex' code 'mượt mà' luôn! Tưởng tượng mà xem, bạn có một cái bảng dữ liệu khô khan như báo cáo tài chính cuối năm của công ty bố bạn vậy. Mỗi hàng là một dòng thông tin, nhìn vào là muốn 'ngủ gật'. Thế rồi, 'đùng một phát', bạn muốn chạm vào một hàng nào đó, và nó không chỉ 'sáng lên' mà còn 'gợn sóng' một cách duyên dáng, như thể bạn vừa ném một viên sỏi xuống mặt hồ tĩnh lặng vậy. Đó chính là lúc TableRowInkWell ra tay, biến cái bảng 'nhạt nhẽo' thành một 'sân chơi cảm ứng' đầy hấp dẫn! 1. TableRowInkWell là gì và để làm gì? (Giải thích Gen Z) TableRowInkWell – nghe cái tên là thấy 'InkWell' rồi, mà 'InkWell' thì các bạn 'sành điệu' Flutter đã biết nó là 'thánh' của mấy cái hiệu ứng 'ripple' (gợn sóng) khi bạn chạm vào. Nó biến một widget 'chết' thành một widget 'sống', có hồn và có phản ứng. Nhưng TableRowInkWell thì sao? Đơn giản thôi: Nó là 'InkWell phiên bản nâng cấp' dành riêng cho các TableRow bên trong một Table widget. Thay vì phải 'nhét' từng cái InkWell vào từng TableCell con con, vừa tốn công, vừa dễ 'lệch pha' hiệu ứng, thì TableRowInkWell cho phép bạn 'bọc' cả một TableRow lại, biến nguyên một hàng thành một 'nút bấm' khổng lồ, cảm ứng được. Khi bạn chạm vào bất kỳ đâu trên hàng đó, toàn bộ hàng sẽ 'gợn sóng' một cách đồng bộ và đẹp mắt. Vậy để làm gì à? Để biến những cái bảng 'vô tri vô giác' thành những bảng 'có cảm xúc', tương tác được. Ví dụ, bạn có bảng danh sách sản phẩm, chạm vào một hàng để xem chi tiết sản phẩm đó. Hay bảng lịch sử giao dịch ngân hàng, chạm vào để xem chi tiết từng giao dịch. Nó 'upgrade' trải nghiệm người dùng lên một tầm cao mới, đỡ 'chán đời' hơn hẳn! 2. Code Ví Dụ Minh Hoạ Rõ Ràng, Chuẩn Kiến Thức Nói lý thuyết suông thì 'nhạt' lắm, giờ anh Creyt 'triển' ngay code ví dụ cho các bạn thấy 'phép thuật' của TableRowInkWell nó 'vi diệu' đến mức nào nhé. Chúng ta sẽ tạo một cái bảng đơn giản với vài dòng dữ liệu, và biến mỗi dòng thành một 'điểm chạm'! 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: 'TableRowInkWell Demo của Creyt', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const TableInkWellScreen(), ); } } class TableInkWellScreen extends StatefulWidget { const TableInkWellScreen({super.key}); @override State<TableInkWellScreen> createState() => _TableInkWellScreenState(); } class _TableInkWellScreenState extends State<TableInkWellScreen> { String _selectedItem = 'Chưa chọn gì'; void _handleRowTap(String itemName) { setState(() { _selectedItem = itemName; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Bạn vừa chọn: $itemName'), duration: const Duration(milliseconds: 800), ), ); print('Row tapped: $itemName'); // In ra debug console } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bảng Tương Tác Của Creyt'), backgroundColor: Colors.deepPurple, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Text( 'Mục đã chọn: $_selectedItem', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), Table( border: TableBorder.all(color: Colors.grey.shade400, width: 1.0), columnWidths: const { 0: FlexColumnWidth(1), 1: FlexColumnWidth(2), 2: FlexColumnWidth(1), }, children: [ // Hàng tiêu đề const TableRow( decoration: BoxDecoration(color: Colors.deepPurpleAccent), children: [ TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('ID', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('Tên Sản Phẩm', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('Giá', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white)), ))), ], ), // Hàng dữ liệu 1 TableRow( decoration: TableRowInkWell( onTap: () => _handleRowTap('Laptop Gaming'), // Màu hiệu ứng khi chạm. Thử đổi màu xem sao nhé! overlayColor: MaterialStateProperty.all(Colors.blue.withOpacity(0.2)), borderRadius: BorderRadius.circular(8), // Bo góc cho hiệu ứng ), children: const [ TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('001'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('Laptop Gaming ASUS'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('30.000.000đ'), ))), ], ), // Hàng dữ liệu 2 TableRow( decoration: TableRowInkWell( onTap: () => _handleRowTap('Điện Thoại Mới'), // Thử dùng onLongPress xem sao! onLongPress: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Long Pressed: Điện Thoại Mới'), duration: const Duration(milliseconds: 800), ), ); print('Long pressed: Điện Thoại Mới'); }, overlayColor: MaterialStateProperty.all(Colors.green.withOpacity(0.2)), ), children: const [ TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('002'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('iPhone 15 Pro Max'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('28.000.000đ'), ))), ], ), // Hàng dữ liệu 3 TableRow( decoration: TableRowInkWell( onTap: () => _handleRowTap('Tai Nghe Bluetooth'), overlayColor: MaterialStateProperty.all(Colors.red.withOpacity(0.2)), ), children: const [ TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('003'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('Tai Nghe Sony WH-1000XM5'), ))), TableCell(child: Center(child: Padding( padding: EdgeInsets.all(8.0), child: Text('7.000.000đ'), ))), ], ), ], ), ], ), ), ); } } Trong ví dụ trên, anh Creyt đã tạo một Table đơn giản. Mỗi TableRow dữ liệu (từ ID 001 đến 003) đều được 'bọc' bởi một TableRowInkWell thông qua thuộc tính decoration. Khi bạn chạm vào bất kỳ hàng nào, hàm _handleRowTap sẽ được gọi, hiển thị một SnackBar và in ra console tên sản phẩm bạn vừa chọn. Đặc biệt, anh còn 'thêm thắt' các overlayColor khác nhau để các bạn thấy hiệu ứng gợn sóng có thể 'biến hoá' thế nào! 3. Một Vài Mẹo (Best Practices) Để Ghi Nhớ Hoặc Dùng Thực Tế Để 'chơi' TableRowInkWell một cách 'pro' nhất, các bạn cần nhớ vài 'chiêu' sau: Dùng Khi Cần Tương Tác Toàn Hàng: Đừng 'tham lam' dùng InkWell cho từng TableCell nếu bạn muốn cả hàng phản ứng. TableRowInkWell sinh ra là để làm việc này một cách 'elegant' nhất. Tùy Biến overlayColor: Đây là 'linh hồn' của hiệu ứng gợn sóng. Hãy chọn màu sắc phù hợp với 'brand identity' của app bạn. Thường thì dùng Colors.yourColor.withOpacity(0.1-0.3) để tạo hiệu ứng nhẹ nhàng, không bị 'chói'. onTap và onLongPress: Tận dụng cả hai callback này để cung cấp nhiều tương tác hơn. onTap thường dùng để xem chi tiết, onLongPress có thể dùng để mở menu ngữ cảnh (context menu) hoặc các tùy chọn nâng cao. borderRadius: Nếu bảng của bạn có bo góc, đừng quên thêm borderRadius vào TableRowInkWell để hiệu ứng gợn sóng cũng được bo góc theo, tạo sự 'đồng điệu' về mặt thị giác. Accessibility (Khả năng Tiếp Cận): Đừng quên rằng việc thêm tương tác cũng cần đi đôi với việc thông báo cho người dùng biết. Đảm bảo các hành động này rõ ràng và dễ hiểu, đặc biệt với người dùng có nhu cầu đặc biệt. 4. Ví Dụ Thực Tế Các Ứng Dụng/Website Đã Ứng Dụng Trong thế giới 'thực chiến' của các app 'xịn sò', TableRowInkWell (hoặc các cơ chế tương tự) được dùng 'như cơm bữa' đấy các bạn ạ: Ứng dụng Ngân hàng/Tài chính: Xem lịch sử giao dịch. Chạm vào một dòng để mở chi tiết giao dịch (số tiền, thời gian, người gửi/nhận). Ứng dụng Thương mại điện tử: Danh sách đơn hàng. Chạm vào một dòng đơn hàng để xem chi tiết sản phẩm đã mua, trạng thái đơn hàng. Ứng dụng Quản lý Dự án/Task: Danh sách công việc. Chạm vào một dòng task để mở cửa sổ chỉnh sửa hoặc xem thông tin chi tiết. Các Dashboard Dữ liệu: Hiển thị danh sách các mục và cho phép người dùng tương tác để lọc, sắp xếp hoặc xem dữ liệu liên quan. 5. Thử Nghiệm Đã Từng Và Hướng Dẫn Nên Dùng Cho Case Nào Anh Creyt đã 'chinh chiến' qua nhiều dự án, và kinh nghiệm xương máu cho thấy TableRowInkWell là 'cứu tinh' trong các trường hợp sau: Khi bạn muốn toàn bộ hàng trong bảng là một vùng tương tác duy nhất. Ví dụ: Bạn có một danh sách email, chạm vào bất kỳ đâu trên hàng email đó để mở nội dung email. Thay vì phải 'cố đấm ăn xôi' đặt InkWell vào từng Text widget trong TableCell, TableRowInkWell giải quyết vấn đề này một cách 'thanh lịch' hơn rất nhiều. Khi bạn cần hiệu ứng Material Design 'chuẩn chỉnh'. Cái hiệu ứng gợn sóng 'thần thánh' của InkWell là một phần không thể thiếu của Material Design. TableRowInkWell mang trọn vẹn tinh hoa đó vào bảng biểu của bạn. Tránh dùng khi: Bạn chỉ muốn một phần nhỏ của TableCell (ví dụ: một icon hoặc một button nhỏ) là tương tác. Trong trường hợp đó, việc đặt InkWell hoặc GestureDetector trực tiếp vào widget con trong TableCell sẽ hiệu quả và dễ kiểm soát hơn. Đừng biến cả hàng thành nút bấm nếu chỉ một icon nhỏ cần tương tác, nó sẽ gây nhầm lẫn cho người dùng. Tóm lại, TableRowInkWell là một 'công cụ' cực kỳ mạnh mẽ để biến những cái bảng 'vô hồn' trở nên 'có sức sống', 'mượt mà' và 'thân thiện' hơn với người dùng. Hãy 'thực hành' ngay với ví dụ của anh Creyt và 'tự tin' áp dụng nó vào các dự án của mình nhé. Nhớ là, 'code' phải đi đôi với 'thực hành' thì mới 'lên tay' được! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

40 Đọc tiếp
TableRow trong Flutter: Bàn tiệc data của Gen Z
21/03/2026

TableRow trong Flutter: Bàn tiệc data của Gen Z

Ê Gen Z! Nghe đây, hôm nay Creyt sẽ "khui" cho mấy đứa một khái niệm mà nghe tưởng khô khan nhưng lại "ngon lành cành đào" trong Flutter: TableRow. Nghe cái tên chắc mấy đứa cũng đoán ra rồi ha? TableRow dịch nôm na là "Hàng trong Bảng". Nhưng nó làm gì, và tại sao mình lại cần nó trong cái vũ trụ Flutter đầy màu sắc này? 1. TableRow là gì và để làm gì? (aka "Cái đĩa cơm trên bàn tiệc data") Mấy đứa cứ hình dung thế này: trong thế giới lập trình, đôi khi mình cần hiển thị dữ liệu theo kiểu "bảng biểu" cho nó có tổ chức, dễ nhìn. Giống như cái bảng điểm thi đấu game, bảng xếp hạng idol, hay bảng kê khai tài sản của mấy đứa sau khi "cày" game xuyên màn đêm vậy đó. Trong Flutter, để tạo ra một cái bảng, mình dùng widget Table. Và TableRow chính là "linh hồn" của cái Table đó. Nếu Table là cái bàn ăn hoành tráng mà mấy đứa ngồi vào để "xử lý" data, thì mỗi TableRow chính là một cái "đĩa cơm" được đặt ngay ngắn trên bàn. Mỗi cái đĩa này sẽ chứa các "món ăn" (các widget con như Text, Icon, Container...) xếp cạnh nhau, tạo thành một hàng dữ liệu hoàn chỉnh. Nói dễ hiểu hơn, TableRow giúp mấy đứa: Sắp xếp dữ liệu ngang hàng: Các widget con sẽ tự động được xếp cạnh nhau trong một hàng. Tạo cấu trúc rõ ràng: Giúp người dùng dễ dàng đọc và hiểu dữ liệu. Tùy chỉnh từng hàng: Mấy đứa có thể "trang trí" riêng cho từng hàng, ví dụ tô màu nền khác nhau, thêm viền cho nó thêm phần "chanh sả". Nó không phải là "ngôi sao" độc lập đâu nha, nó luôn phải sống trong "mái nhà" là widget Table. Nhớ kỹ: Table chứa một list các TableRow. 2. Code Ví Dụ Minh Họa: "Đĩa cơm" của Creyt Để mấy đứa dễ hình dung, giờ mình cùng "xắn tay áo" code một cái bảng điểm nhỏ nhắn xinh xắn nhé. Creyt sẽ làm một bảng điểm các môn họ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: 'Bảng Điểm Của Creyt', theme: ThemeData( primarySwatch: Colors.deepPurple, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bảng Điểm Siêu Cấp Pro'), ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Table( // columnWidths: const { // 0: FlexColumnWidth(2), // Cột 1 rộng gấp đôi // 1: FlexColumnWidth(1), // Cột 2 bình thường // 2: FlexColumnWidth(1), // Cột 3 bình thường // }, border: TableBorder.all( color: Colors.deepPurple.shade200, width: 2, style: BorderStyle.solid, ), children: <TableRow>[ // Hàng tiêu đề (Header Row) TableRow( decoration: BoxDecoration( color: Colors.deepPurple.shade100, ), children: const <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: Text( 'Môn Học', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), textAlign: TextAlign.center, ), ), Padding( padding: EdgeInsets.all(8.0), child: Text( 'Điểm', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), textAlign: TextAlign.center, ), ), Padding( padding: EdgeInsets.all(8.0), child: Text( 'Xếp Loại', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), textAlign: TextAlign.center, ), ), ], ), // Hàng dữ liệu 1 TableRow( decoration: const BoxDecoration( color: Colors.white, ), children: const <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: Text('Lập Trình Flutter', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('9.0', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('A+', textAlign: TextAlign.center), ), ], ), // Hàng dữ liệu 2 (có màu nền khác để dễ nhìn) TableRow( decoration: BoxDecoration( color: Colors.deepPurple.shade50, ), children: const <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: Text('Cấu Trúc Dữ Liệu', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('8.5', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('A', textAlign: TextAlign.center), ), ], ), // Hàng dữ liệu 3 TableRow( decoration: const BoxDecoration( color: Colors.white, ), children: const <Widget>[ Padding( padding: EdgeInsets.all(8.0), child: Text('Giải Thuật Nâng Cao', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('7.8', textAlign: TextAlign.center), ), Padding( padding: EdgeInsets.all(8.0), child: Text('B+', textAlign: TextAlign.center), ), ], ), ], ), ), ), ); } } Trong ví dụ trên: Mình có một Table widget. border: Tạo đường viền cho toàn bộ bảng. children: Đây là nơi chứa các TableRow của chúng ta. Mỗi TableRow lại có một children khác, chứa các widget con (ở đây là Padding bọc Text) để tạo thành các ô dữ liệu (cell) trong hàng đó. decoration trong TableRow giúp mình tô màu nền riêng cho từng hàng, làm cho bảng "xanh đỏ tím vàng" hơn, dễ đọc hơn. 3. Mẹo Vặt "Hack Não" & Best Practices từ Creyt "Cha nào con nấy": Luôn nhớ TableRow là con của Table. Nó không thể sống sót một mình đâu nha. Đồng bộ số lượng: Tất cả các TableRow trong một Table phải có SỐ LƯỢNG WIDGET CON (số cột) BẰNG NHAU. Nếu hàng trên có 3 cột, hàng dưới cũng phải có 3 cột. Nếu không, Flutter sẽ "giận dỗi" báo lỗi đấy. Kiểm soát độ rộng cột: Mấy đứa có thể dùng columnWidths trong Table để điều chỉnh độ rộng của từng cột. Ví dụ, FlexColumnWidth cho phép mấy đứa chia tỷ lệ độ rộng như chia "kẹo" vậy. Hoặc IntrinsicColumnWidth sẽ tự động co giãn cột theo nội dung dài nhất, "thông minh" ra phết. Thử uncomment cái đoạn columnWidths trong code ví dụ để xem sự khác biệt nhé! Trang trí "đĩa cơm": Dùng decoration property của TableRow để thêm màu nền, border cho từng hàng. Rất tiện lợi để tạo các hàng xen kẽ màu sắc (zebra stripes) cho bảng thêm phần chuyên nghiệp. Padding là bạn: Đừng quên thêm Padding cho các widget con bên trong TableRow để nội dung không bị dính sát vào nhau, nhìn "ngộp" lắm. 4. Ứng Dụng Thực Tế: TableRow "tung hoành" ở đâu? Mấy đứa có thể thấy các kiểu bảng biểu này "nhan nhản" trong các app/website mà mấy đứa dùng hàng ngày: App quản lý tài chính: Bảng kê giao dịch, sao kê ngân hàng, báo cáo thu chi hàng tháng. App thể thao: Bảng xếp hạng đội bóng, lịch thi đấu, thống kê cầu thủ. App thương mại điện tử: Bảng so sánh thông số kỹ thuật sản phẩm, bảng giá. App học tập: Bảng thời khóa biểu, bảng điểm học kỳ. Dashboard quản trị: Các biểu đồ, bảng thống kê dữ liệu kinh doanh, lượng truy cập. Tóm lại, bất cứ khi nào cần hiển thị dữ liệu theo dạng "lưới" có cấu trúc hàng-cột đơn giản, TableRow sẽ là một "chiến binh" đắc lực. 5. Khi nào nên "triệu hồi" TableRow và khi nào nên "cất kiếm"? Nên dùng TableRow khi: Hiển thị dữ liệu tĩnh: Bảng không cần tương tác nhiều (kiểu như bấm vào sắp xếp, lọc dữ liệu). Cần kiểm soát chi tiết từng ô/hàng: Mấy đứa muốn mỗi ô có widget riêng, mỗi hàng có màu sắc, trang trí khác nhau một cách linh hoạt. Bảng nhỏ và vừa: Với số lượng hàng không quá lớn, TableRow rất hiệu quả và dễ quản lý. Trộn lẫn các loại widget: Dễ dàng đặt Text, Icon, Image, Button... vào chung một ô. Nên "cất kiếm" và tìm giải pháp khác khi: Bảng cần tương tác cao: Nếu mấy đứa muốn có tính năng sắp xếp (sort), lọc (filter), phân trang (pagination) cho bảng dữ liệu, hãy nghĩ ngay đến DataTable hoặc PaginatedDataTable của Flutter. Chúng được thiết kế riêng cho những tác vụ này và sẽ tiết kiệm rất nhiều công sức cho mấy đứa. Dữ liệu quá lớn (Big Data): Hàng ngàn, hàng chục ngàn hàng dữ liệu? Table và TableRow không được tối ưu cho việc này. Khi đó, ListView.builder kết hợp với các widget tùy chỉnh cho từng hàng sẽ là lựa chọn tốt hơn để tối ưu hiệu suất (lazy loading). Cần layout dạng lưới phức tạp hơn: Nếu không phải là bảng "thẳng thớm" mà là các ô có kích thước không đều, chồng chéo, hoặc layout phức tạp hơn, hãy xem xét GridView, Wrap hoặc thậm chí CustomScrollView với SliverGrid. Thấy chưa? TableRow không chỉ là một cái tên khô khan, nó là một công cụ cực kỳ hữu ích để mấy đứa "trình bày" dữ liệu một cách gọn gàng, đẹp mắt trong app Flutter của mình. Nắm vững nó, mấy đứa sẽ có thêm một "vũ khí" lợi hại trong hành trình chinh phục thế giới lập trì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é!

51 Đọc tiếp
Flutter TableColumnWidth: Kỹ năng 'cắt đất' cho bảng dữ liệu của bạn!
21/03/2026

Flutter TableColumnWidth: Kỹ năng 'cắt đất' cho bảng dữ liệu của bạn!

Này các Gen Z tương lai của làng code! Hôm nay, anh Creyt sẽ cùng các em 'mổ xẻ' một khái niệm nghe có vẻ khô khan nhưng lại cực kỳ quan trọng khi các em muốn làm chủ cái 'bảng tính Excel thu nhỏ' trong app Flutter của mình: đó là TableColumnWidth. 1. TableColumnWidth là cái gì mà nghe 'drama' thế? Đơn giản mà nói, TableColumnWidth giống như cái 'quyền sổ đỏ' mà các em dùng để phân chia đất đai cho từng cột trong một cái bàn (widget Table) vậy. Thay vì cứ để ông trời (hay cụ thể hơn là Flutter) tự động phân bổ đất đai theo ý ổng, thì mình, với tư cách là 'chủ đầu tư', có thể chủ động 'cắm cọc' định hình chiều rộng cho từng cột. Khi các em có dữ liệu dạng bảng, việc các cột cứ 'co giãn' vô tội vạ nhìn ngứa mắt lắm, đúng không? TableColumnWidth sinh ra để giải quyết nỗi 'đau đầu' đó, giúp bảng của các em trông gọn gàng, chuyên nghiệp và dễ đọc hơn nhiều. Nó là một abstract class (một khuôn mẫu trừu tượng), và chúng ta sẽ dùng các 'đứa con' cụ thể của nó để thực hiện nhiệm vụ 'chia đất': FixedColumnWidth: Kiểu 'đất nền' cố định. Cột này tao chốt 100 pixel, khỏi bàn! Dù nội dung dài hay ngắn, nó vẫn cứ giữ nguyên chiều rộng đó. Thích hợp cho các cột có nội dung biết trước kích thước như icon, nút bấm nhỏ. FlexColumnWidth: Kiểu 'chia phần trăm theo tỷ lệ vàng'. Cột này được chia 2 phần, cột kia 1 phần, tổng là 3 phần. Chia đều ra mà sống! Giống như Expanded trong Row/Column hay flex trong CSS ấy. Nó rất linh hoạt, giúp bảng của em tự động co giãn theo kích thước màn hình. FractionColumnWidth: Kiểu 'đất nền' theo phần trăm tổng. Cột này chiếm 30% tổng chiều rộng của bảng, chuẩn chỉ! Dễ hình dung, dễ kiểm soát khi em muốn tỷ lệ chính xác. IntrinsicColumnWidth: Kiểu 'co giãn theo nội dung tự nhiên'. Mày cứ co lại hết cỡ theo nội dung nhỏ nhất đi, để tao xem kích thước 'thật' của mày là bao nhiêu. Thường dùng để làm cho cột có chiều rộng vừa đủ với nội dung dài nhất trong cột đó, nhưng đôi khi có thể gây hiệu suất không tốt nếu bảng quá lớn. MinColumnWidth & MaxColumnWidth: Hai thằng này thường đi đôi với nhau, như 'anh em cây khế' vậy. Cột này ít nhất phải 50, nhiều nhất không quá 200. Mày cứ liệu mà sống! Chúng cho phép em đặt giới hạn trên và dưới cho chiều rộng cột, kết hợp với các loại khác để có sự linh hoạt mà vẫn giữ được trật tự. 2. Code Ví Dụ Minh Hoạ: Cầm tay chỉ việc 'cắm cọc' đất Để TableColumnWidth hoạt động, chúng ta sẽ dùng nó bên trong thuộc tính columnWidths của widget Table. 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: 'TableColumnWidth Demo', theme: ThemeData(primarySwatch: Colors.blueGrey), home: const TableColumnWidthScreen(), ); } } class TableColumnWidthScreen extends StatelessWidget { const TableColumnWidthScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Anh Creyt dạy TableColumnWidth'), centerTitle: true, ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Ví dụ 1: Kết hợp Fixed và Flex ColumnWidth', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const { 0: FixedColumnWidth(80.0), // Cột 0 cố định 80px 1: FlexColumnWidth(2), // Cột 1 chiếm 2 phần 2: FlexColumnWidth(1), // Cột 2 chiếm 1 phần }, children: _buildTableRows(Colors.blueAccent), ), const SizedBox(height: 30), const Text( 'Ví dụ 2: Sử dụng Fraction ColumnWidth', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const { 0: FractionColumnWidth(0.2), // Cột 0 chiếm 20% tổng chiều rộng 1: FractionColumnWidth(0.5), // Cột 1 chiếm 50% 2: FractionColumnWidth(0.3), // Cột 2 chiếm 30% }, children: _buildTableRows(Colors.greenAccent), ), const SizedBox(height: 30), const Text( 'Ví dụ 3: Intrinsic ColumnWidth - "Co giãn theo nội dung"', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), // IntrinsicColumnWidth thường dùng trong Table để các cột có chiều rộng // vừa đủ với nội dung dài nhất trong cột đó. Tuy nhiên, nó có thể // tốn hiệu năng nếu bảng quá lớn vì phải đo lường nhiều lần. Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const { 0: IntrinsicColumnWidth(), // Cột 0 co theo nội dung dài nhất 1: IntrinsicColumnWidth(), // Cột 1 co theo nội dung dài nhất 2: IntrinsicColumnWidth(), // Cột 2 co theo nội dung dài nhất }, children: [ TableRow( decoration: BoxDecoration(color: Colors.orange.shade100), children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('ID')), Padding(padding: EdgeInsets.all(8.0), child: Text('Tên sản phẩm siêu dài')), Padding(padding: EdgeInsets.all(8.0), child: Text('Giá')), ], ), TableRow( children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('1')), Padding(padding: EdgeInsets.all(8.0), child: Text('Áo thun')), Padding(padding: EdgeInsets.all(8.0), child: Text('150k')), ], ), TableRow( children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('2')), Padding(padding: EdgeInsets.all(8.0), child: Text('Quần jeans rách gối cá tính cực chất')), Padding(padding: EdgeInsets.all(8.0), child: Text('300k')), ], ), ], ), const SizedBox(height: 30), const Text( 'Ví dụ 4: Kết hợp Min/Max ColumnWidth với Flex', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Table( border: TableBorder.all(color: Colors.grey.shade300), columnWidths: const { 0: FixedColumnWidth(60.0), // ID cố định 1: MinColumnWidth(FixedColumnWidth(100.0), FlexColumnWidth(2)), // Tối thiểu 100, sau đó flex 2 2: MaxColumnWidth(FixedColumnWidth(150.0), FlexColumnWidth(1)), // Tối đa 150, sau đó flex 1 }, children: _buildTableRows(Colors.purpleAccent), ), ], ), ), ); } List<TableRow> _buildTableRows(Color headerColor) { return [ TableRow( decoration: BoxDecoration(color: headerColor.withOpacity(0.2)), children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('STT', style: TextStyle(fontWeight: FontWeight.bold))), Padding(padding: EdgeInsets.all(8.0), child: Text('Mô tả sản phẩm', style: TextStyle(fontWeight: FontWeight.bold))), Padding(padding: EdgeInsets.all(8.0), child: Text('Số lượng', style: TextStyle(fontWeight: FontWeight.bold))), ], ), TableRow( children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('1')), Padding(padding: EdgeInsets.all(8.0), child: Text('Bàn phím cơ RGB xịn xò')), Padding(padding: EdgeInsets.all(8.0), child: Text('1')), ], ), TableRow( children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('2')), Padding(padding: EdgeInsets.all(8.0), child: Text('Chuột gaming không dây siêu nhẹ')), Padding(padding: EdgeInsets.all(8.0), child: Text('2')), ], ), TableRow( children: const [ Padding(padding: EdgeInsets.all(8.0), child: Text('3')), Padding(padding: EdgeInsets.all(8.0), child: Text('Màn hình cong 240Hz cho game thủ pro')), Padding(padding: EdgeInsets.all(8.0), child: Text('1')), ], ), ]; } } 3. Mẹo (Best Practices) để 'cắm cọc' đất không bị 'quy hoạch treo' Start Simple, Then Scale: Ban đầu, cứ dùng FixedColumnWidth hoặc FlexColumnWidth cho dễ. Khi nào thấy cần độ phức tạp hơn thì mới nghĩ đến Fraction hay Min/Max. FlexColumnWidth là 'Bạn thân' của Responsive: Khi em muốn bảng của mình tự động điều chỉnh theo kích thước màn hình (ví dụ, trên điện thoại và trên tablet), FlexColumnWidth là lựa chọn số 1. Nó sẽ chia đều không gian còn lại theo tỷ lệ em đặt ra. Cẩn trọng với IntrinsicColumnWidth: Thằng này hữu ích khi em muốn cột tự động co giãn vừa đúng với nội dung dài nhất. Tuy nhiên, nó có thể làm giảm hiệu suất đáng kể nếu bảng có quá nhiều hàng hoặc cột, vì Flutter phải đo lường kích thước của từng ô để tìm ra kích thước tối ưu. Chỉ dùng khi thực sự cần thiết và bảng không quá lớn. Kết hợp là 'nghệ thuật': Em có thể kết hợp các loại TableColumnWidth lại với nhau. Ví dụ, cột đầu tiên là FixedColumnWidth cho số thứ tự, các cột tiếp theo là FlexColumnWidth để nội dung tự co giãn. Hoặc dùng MinColumnWidth(FixedColumnWidth(50), FlexColumnWidth(1)) để đảm bảo cột không bao giờ nhỏ hơn 50px nhưng vẫn có thể co giãn linh hoạt. Index của cột: Nhớ rằng columnWidths nhận một Map<int, TableColumnWidth>, trong đó int là index của cột (bắt đầu từ 0). Nếu em không định nghĩa cho một cột nào đó, nó sẽ mặc định dùng FlexColumnWidth(1). 4. Ứng dụng thực tế: Bảng biểu ở khắp mọi nơi Các em thấy TableColumnWidth được dùng ở đâu không? Nhiều lắm chứ! Bất cứ đâu có dữ liệu dạng bảng mà cần sự gọn gàng, có cấu trúc đều có thể áp dụng: Ứng dụng quản lý tài chính: Hiển thị danh sách giao dịch, sao kê ngân hàng, danh mục đầu tư (cột ngày, mô tả, số tiền). Ứng dụng thương mại điện tử: Giỏ hàng (cột ảnh sản phẩm, tên, số lượng, giá), bảng so sánh sản phẩm. Dashboard quản trị: Thống kê số liệu, danh sách người dùng, báo cáo bán hàng. Ứng dụng danh bạ/quản lý liên hệ: Hiển thị tên, số điện thoại, email. Thực ra, những ứng dụng như Excel, Google Sheets, hay các trang web hiển thị bảng dữ liệu đều phải có cơ chế tương tự để quản lý chiều rộng cột, chỉ là tên gọi và cách triển khai khác thôi. 5. Thử nghiệm và lời khuyên từ Creyt Anh Creyt đã từng 'vật lộn' với layout bảng biểu không biết bao nhiêu lần rồi. Có những lúc bảng dữ liệu trông như 'bãi chiến trường' vì các cột cứ nhảy múa lung tung. Hồi đó, anh hay 'hardcode' mọi thứ bằng SizedBox hoặc Container với width cố định, nhưng đổi màn hình cái là 'toang' ngay. Sau này, khi hiểu sâu hơn về TableColumnWidth, việc 'cắm cọc' cho các cột trở nên dễ dàng hơn nhiều. Anh thường dùng FlexColumnWidth cho hầu hết các cột chứa nội dung động, và FixedColumnWidth cho các cột 'ăn chắc mặc bền' như icon, checkbox, hoặc số thứ tự. FractionColumnWidth thì dành cho những bảng cần tỷ lệ chính xác như biểu đồ mini trong bảng. Khi nào nên dùng? Khi em cần một bảng dữ liệu có cấu trúc rõ ràng và dễ đọc. Khi em muốn kiểm soát chính xác chiều rộng từng cột, không muốn Flutter tự động tính toán. Khi em muốn bảng của mình trông chuyên nghiệp, không bị 'vỡ layout' trên các kích thước màn hình khác nhau (kết hợp với FlexColumnWidth). Khi nào nên cân nhắc giải pháp khác? Nếu dữ liệu của em không thực sự cần định dạng bảng (ví dụ, chỉ là một danh sách đơn giản), ListView.builder hoặc Column với các Row lồng nhau có thể đơn giản và hiệu quả hơn. Với IntrinsicColumnWidth, nếu bảng của em có hàng trăm hoặc hàng ngàn dòng, hãy cân nhắc kỹ hoặc tìm cách tối ưu hóa (ví dụ, phân trang dữ liệu) để tránh giật lag. Nhớ nhé, TableColumnWidth không chỉ là một công cụ, nó là một phần của 'nghệ thuật sắp đặt' UI. Nắm vững nó, các em sẽ có những bảng dữ liệu 'chill phết' và 'đỉnh của chóp'! Thuộc Series: Flutter Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

41 Đọc tiếp
TableCell Flutter: Sếp của từng ô dữ liệu, cân team thẳng tắp!
21/03/2026

TableCell Flutter: Sếp của từng ô dữ liệu, cân team thẳng tắp!

Yo, fam! Anh Creyt lại 'lên sóng' đây. Hôm nay mình sẽ 'bung lụa' với một 'nhân vật' có vẻ ít được nhắc tên, nhưng lại cực kỳ 'quyền lực' khi bạn muốn 'layout' mấy cái bảng biểu trông 'phê pha' trên app Flutter của mình: TableCell. 1. TableCell là gì? Để làm gì? (Giải thích kiểu Gen Z) Này mấy đứa, tưởng tượng thế này cho anh dễ hiểu nhé. Khi mấy đứa làm cái 'bảng điểm danh' hay 'bảng phân công nhiệm vụ' của team, mỗi cái ô nhỏ nhỏ chứa tên đứa nào đó, hay nhiệm vụ gì đó, chính là một TableCell đấy. Nói theo kiểu 'dân chơi' công nghệ, Table trong Flutter nó như một cái bảng tính Excel thu nhỏ vậy. TableRow chính là từng dòng trong cái bảng đó, và TableCell? Chính xác, nó là từng ô dữ liệu độc lập trong mỗi dòng. Nhưng mà TableCell nó 'cool' hơn mấy cái ô bình thường ở chỗ: nó cho phép mình can thiệp sâu vào cái 'thái độ' của nội dung bên trong nó. Tức là, dù cả dòng đang muốn 'chill' ở giữa, thằng TableCell này vẫn có thể 'tuyên bố độc lập' và bảo: 'Không, tao muốn nội dung của tao phải 'đứng nghiêm' ở trên cùng cơ!' Hoặc 'tao muốn 'nằm dài' ở dưới cùng cho nó 'phá cách'.' Nghe 'ngầu' chưa? Nói tóm lại, TableCell là một widget 'bao bọc' (wrapper) cho bất kỳ widget nào khác mà bạn muốn đặt vào một ô cụ thể trong Table. Sức mạnh của nó nằm ở việc bạn có thể điều chỉnh vị trí dọc (vertical alignment) của nội dung bên trong ô đó một cách độc lập, không bị ảnh hưởng bởi cài đặt chung của cả dòng. 2. Code Ví Dụ Minh Hoạ Rõ Ràng Để mấy đứa dễ hình dung, anh Creyt sẽ 'phù phép' một cái bảng đơn giản, có mấy ô 'ngang ngược' muốn 'đứng riêng một kiểu' cho xem: 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: 'TableCell Demo by Creyt', home: Scaffold( appBar: AppBar( title: const Text('TableCell - Cân chỉnh từng ô'), backgroundColor: Colors.deepPurple, ), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Table( border: TableBorder.all(color: Colors.grey.shade400, width: 1.0), columnWidths: const <int, TableColumnWidth>{ 0: IntrinsicColumnWidth(), // Cột 0 tự động co giãn theo nội dung 1: FlexColumnWidth(), // Cột 1 chiếm phần còn lại 2: FixedColumnWidth(100.0), // Cột 2 cố định 100px }, children: <TableRow>[ // Dòng tiêu đề TableRow( decoration: BoxDecoration(color: Colors.deepPurple.shade100), children: <Widget>[ TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('STT', style: TextStyle(fontWeight: FontWeight.bold)), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('Nội dung Chính', style: TextStyle(fontWeight: FontWeight.bold)), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('Trạng thái', style: TextStyle(fontWeight: FontWeight.bold)), ), ), ], ), // Dòng dữ liệu 1: Mọi thứ căn giữa (mặc định của TableRow) TableRow( children: <Widget>[ TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('1'), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('Đây là một đoạn nội dung khá dài trong ô, xem nó căn chỉnh thế nào nhé!'), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.check_circle, color: Colors.green), ), ), ], ), // Dòng dữ liệu 2: Ô thứ 2 'ngang ngược' căn trên TableRow( children: <Widget>[ TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('2'), ), ), TableCell( verticalAlignment: TableCellVerticalAlignment.top, // <-- Đây này! child: Padding( padding: const EdgeInsets.all(8.0), child: Text( 'Nội dung này muốn 'đứng nghiêm' ở trên cùng. Dù cả dòng có cao đến mấy, nó vẫn 'chót vót' trên cao. Đây là lúc TableCell thực sự tỏa sáng đó mấy đứa!', style: TextStyle(fontStyle: FontStyle.italic), ), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.warning, color: Colors.orange), ), ), ], ), // Dòng dữ liệu 3: Ô thứ 2 'lười biếng' căn dưới TableRow( children: <Widget>[ TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('3'), ), ), TableCell( verticalAlignment: TableCellVerticalAlignment.bottom, // <-- Và đây nữa! child: Padding( padding: const EdgeInsets.all(8.0), child: Text( 'Còn nội dung này thì 'ngủ nướng' ở dưới đáy ô. Mấy đứa thấy sự khác biệt chưa? Chỉ một ô thôi mà có thể 'tự quyết định số phận' của mình.', style: TextStyle(color: Colors.blueGrey), ), ), ), TableCell( child: Padding( padding: const EdgeInsets.all(8.0), child: Icon(Icons.error, color: Colors.red), ), ), ], ), ], ), ), ), ), ); } } Trong ví dụ trên, mấy đứa sẽ thấy rõ sự khác biệt ở TableRow thứ hai và thứ ba. Dù các ô khác trong dòng vẫn theo 'quy tắc chung' của TableRow (mặc định là middle hoặc theo defaultVerticalAlignment của Table), thì cái TableCell ở cột thứ hai lại 'ngang ngược' tự điều chỉnh verticalAlignment của mình thành top hoặc bottom. 3. Mẹo (Best Practices) để ghi nhớ hoặc dùng thực tế Khi nào dùng TableCell? Dùng khi bạn cần điều khiển vị trí dọc của nội dung trong MỘT Ô CỤ THỂ, mà không muốn ảnh hưởng đến các ô khác trong cùng một hàng. Nếu bạn muốn cả hàng cùng 'ngẩng mặt' lên trên hay 'cắm mặt' xuống dưới, thì dùng verticalAlignment của TableRow sẽ gọn gàng hơn. TableCell không phải là 'thùng rác': Đừng vứt mỗi Text hay Icon trần trụi vào TableCell. Hãy bọc nó trong Padding hoặc Container để tạo khoảng cách, đường viền cho đẹp mắt, tránh bị 'dính chùm' vào nhau trông rất 'kém sang'. Kết hợp với TableColumnWidth: Để cái bảng của bạn không bị 'méo mó', hãy dùng columnWidths trong Table để định rõ kích thước các cột. IntrinsicColumnWidth (tự co giãn theo nội dung), FixedColumnWidth (cố định), FlexColumnWidth (linh hoạt) là những 'bạn thân' của TableCell đấy. Ít dùng, nhưng chất lượng: Trong nhiều trường hợp, bạn có thể dùng Row và Column để tạo layout dạng lưới. Nhưng khi bạn thực sự cần một cấu trúc bảng biểu với các ô có thể 'tự chủ' về căn chỉnh dọc, thì Table và TableCell là lựa chọn 'đỉnh của chóp'. 4. Ứng dụng thực tế các app/website đã ứng dụng Thực ra, TableCell (hoặc các khái niệm tương tự trong các framework khác) được dùng rất nhiều ở những nơi cần hiển thị dữ liệu dạng bảng. Mấy đứa có thể thấy nó ở: Các ứng dụng tài chính/chứng khoán: Hiển thị giá cổ phiếu, danh mục đầu tư với các cột dữ liệu số, biểu đồ mini, icon tăng/giảm, và đôi khi cần căn chỉnh đặc biệt cho từng loại dữ liệu. App quản lý dự án/Task manager: Bảng phân công công việc, tiến độ dự án, nơi mỗi ô có thể chứa tên người, trạng thái (dropdown), deadline, và cần căn chỉnh khác nhau cho mỗi loại input. Các trang so sánh sản phẩm (e-commerce): Khi bạn thấy một bảng so sánh các tính năng của điện thoại, laptop, mỗi ô có thể chứa text, icon check/cross, hình ảnh nhỏ, và việc căn chỉnh từng ô giúp bảng trông gọn gàng, dễ đọc hơn. Bất kỳ app nào có 'Dashboard' hiển thị thống kê: Thường có các bảng nhỏ gọn, hiển thị số liệu, biểu đồ mini. TableCell giúp kiểm soát tốt hơn các thành phần này. 5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào Anh Creyt đã từng 'vật lộn' với việc tạo bảng trong Flutter khá nhiều. Ban đầu, anh cũng hay dùng Row lồng Column (hoặc ngược lại) để tạo lưới, và nó hoạt động tốt cho các layout đơn giản. Nhưng khi gặp phải trường hợp một dòng có nhiều loại nội dung khác nhau (ví dụ: một ô là text ngắn, một ô là ảnh cao, một ô là nút bấm), và mình muốn cái text ngắn đó phải 'đứng thẳng hàng' lên trên cùng của ô, trong khi các ô khác cứ 'nhởn nhơ' ở giữa, thì Row lồng Column bắt đầu 'khóc thét'. Đó là lúc Table và TableCell 'ra tay cứu độ'. TableCell với thuộc tính verticalAlignment của nó chính là 'vũ khí bí mật' để giải quyết vấn đề đó một cách thanh lịch. Nó giúp cái bảng của bạn trông 'chuyên nghiệp' hơn, không còn cảnh nội dung 'lộn xộn' trong các ô có chiều cao không đồng đều. Lời khuyên của anh Creyt: Dùng Table và TableCell khi: Bạn có dữ liệu thực sự mang tính 'bảng' (tabular data) và cần sự kiểm soát chặt chẽ về cách các ô và nội dung bên trong được căn chỉnh dọc, đặc biệt khi các ô trong cùng một hàng có chiều cao khác nhau. Tránh dùng Table và TableCell khi: Bạn chỉ muốn tạo một layout dạng lưới đơn giản mà không có ý định hiển thị dữ liệu theo hàng/cột cụ thể, hoặc không cần điều khiển căn chỉnh dọc quá chi tiết cho từng ô. Khi đó, GridView, Row lồng Column, hay Wrap có thể là lựa chọn tốt hơn. Nhớ nhé mấy đứa, mỗi công cụ sinh ra đều có 'sứ mệnh' riêng. Hiểu rõ 'sứ mệnh' của TableCell sẽ giúp mấy đứa 'code' Flutter 'ngon lành' hơn rất nhiều đó! Cứ 'quẩy' đi, có gì thắc mắc cứ 'hú' anh Creyt 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é!

41 Đọc tiếp
TableBorder Flutter: Bảng biểu 'xấu lạ' hóa 'siêu phẩm' visual!
21/03/2026

TableBorder Flutter: Bảng biểu 'xấu lạ' hóa 'siêu phẩm' visual!

Chào anh em Gen Z mê code! Anh Creyt đây, hôm nay chúng ta sẽ cùng "phẫu thuật thẩm mỹ" cho cái widget mà nhiều khi anh em thấy nó hơi… "nhạt nhẽo": Table trong Flutter. Cụ thể hơn, chúng ta sẽ "lên đồ" cho nó bằng TableBorder. TableBorder: "Makeup Artist" cho bảng biểu của bạn Anh em cứ hình dung thế này: một cái bảng (Table) mà không có đường viền (border) thì nó giống như một tờ giấy trắng tinh, anh em viết chữ lên đấy thì vẫn đọc được thôi, nhưng nhìn nó cứ "trôi tuột", không có điểm nhấn, không phân chia rõ ràng. Thậm chí, anh em nhìn vào còn thấy… chóng mặt nữa là đằng khác. Đấy là lúc TableBorder xuất hiện như một "makeup artist" chuyên nghiệp. Nó không chỉ đơn thuần là vẽ một cái khung xung quanh bảng, mà nó còn cho phép anh em "tô điểm" từng đường nét bên trong: viền trên, viền dưới, viền trái, viền phải, và đặc biệt là các đường kẻ ngang, kẻ dọc "nội bộ" chia cắt từng ô dữ liệu. Vậy TableBorder là gì và để làm gì? Trong Flutter, TableBorder là một class chuyên dùng để định nghĩa các đường viền cho widget Table. Nó giúp anh em: Tăng tính dễ đọc: Phân tách rõ ràng từng hàng, từng cột, giúp người dùng dễ dàng theo dõi và so sánh dữ liệu. Tạo cấu trúc trực quan: Biến một mớ dữ liệu lộn xộn thành một bố cục có tổ chức, chuyên nghiệp. Thẩm mỹ hơn: Đôi khi, một đường viền tinh tế lại làm cho giao diện của anh em "sang chảnh" hơn hẳn. Nói cách khác, TableBorder là công cụ để anh em biến cái bảng "raw" thành một "data grid" đẹp mắt, dễ hiểu, y như cách anh em kẻ ô ly vào vở để viết cho thẳng hàng vậy. Code Ví Dụ Minh Hoạ: TableBorder "biến hình" như thế nào? Anh em xem ví dụ này để thấy TableBorder "phù phép" ra sao nhé. Chúng ta sẽ bắt đầu với một cái bảng cơ bản, sau đó áp dụng các kiểu TableBorder khác nhau. import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'TableBorder Demo', theme: ThemeData(primarySwatch: Colors.blueGrey), home: Scaffold( appBar: AppBar(title: const Text('TableBorder Flutter by Creyt')), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Bảng Cơ Bản (TableBorder.all)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), // Ví dụ 1: TableBorder.all - Tất cả các viền đều giống nhau Table( border: TableBorder.all( color: Colors.blueAccent, width: 2.0, style: BorderStyle.solid, ), children: _buildTableRows(), ), const SizedBox(height: 30), const Text('Bảng Nâng Cao (Custom TableBorder)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), // Ví dụ 2: Custom TableBorder - Kiểm soát từng đường viền Table( border: TableBorder( top: const BorderSide(color: Colors.red, width: 3.0), bottom: const BorderSide(color: Colors.green, width: 3.0), left: const BorderSide(color: Colors.purple, width: 1.0, style: BorderStyle.dashed), right: const BorderSide(color: Colors.purple, width: 1.0, style: BorderStyle.dashed), horizontalInside: const BorderSide(color: Colors.grey, width: 0.5), verticalInside: const BorderSide(color: Colors.orange, width: 1.5, style: BorderStyle.dotted), ), children: _buildTableRows(), ), const SizedBox(height: 30), const Text('Bảng Đối Xứng (TableBorder.symmetric)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), // Ví dụ 3: TableBorder.symmetric - Viền đối xứng Table( border: TableBorder.symmetric( inside: const BorderSide(color: Colors.teal, width: 1.0), outside: const BorderSide(color: Colors.deepOrange, width: 2.5), ), children: _buildTableRows(), ), ], ), ), ), ), ); } List<TableRow> _buildTableRows() { return [ TableRow( decoration: BoxDecoration(color: Colors.blueGrey.shade100), children: const [ TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Tên', style: TextStyle(fontWeight: FontWeight.bold)))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Tuổi', style: TextStyle(fontWeight: FontWeight.bold)))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Thành Phố', style: TextStyle(fontWeight: FontWeight.bold)))), ], ), TableRow( children: const [ TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('An'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('22'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Hà Nội'))), ], ), TableRow( decoration: BoxDecoration(color: Colors.blueGrey.shade50), children: const [ TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Bình'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('25'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Đà Nẵng'))), ], ), TableRow( children: const [ TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Cường'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('20'))), TableCell(child: Padding(padding: EdgeInsets.all(8.0), child: Text('HCM'))), ], ), ]; } } Trong ví dụ trên: TableBorder.all(): Là cách nhanh nhất để áp dụng một kiểu đường viền đồng nhất cho tất cả các cạnh (ngoài và trong) của bảng. Anh em chỉ cần định nghĩa color, width, style một lần là xong. TableBorder() (constructor mặc định): Cho phép anh em kiểm soát từng cạnh một: top, bottom, left, right, horizontalInside (đường kẻ ngang bên trong), verticalInside (đường kẻ dọc bên trong). Mỗi cạnh sẽ nhận một đối tượng BorderSide riêng, nơi anh em tùy chỉnh màu sắc, độ dày và kiểu đường kẻ (solid, dotted, dashed - à mà Flutter hiện tại chỉ hỗ trợ solid thôi nhé, dotted/dashed cần thư viện ngoài hoặc vẽ custom, nhưng BorderStyle vẫn có các enum đó để tương thích với web/CSS). TableBorder.symmetric(): Dùng khi anh em muốn các đường viền bên ngoài (outside) có một kiểu, và các đường viền bên trong (inside) có một kiểu khác, nhưng vẫn đối xứng. Mẹo Vặt (Best Practices) từ Giảng Viên Creyt "Nhất quán là sức mạnh": Khi định nghĩa BorderSide, hãy cố gắng tái sử dụng các BorderSide object hoặc các giá trị màu/độ dày. Đừng mỗi chỗ một kiểu, nhìn cái bảng nó sẽ "loạn thị" ngay. Ví dụ, nếu tất cả horizontalInside đều màu xám nhạt, hãy tạo một const BorderSide kDefaultHorizontalBorder = BorderSide(color: Colors.grey, width: 0.5); để dùng lại. "Đơn giản là bạn": Đừng cố gắng làm cho mọi đường viền đều khác biệt. Đôi khi, một cái bảng với đường viền ngoài đậm, đường viền trong nhạt là đã đủ đẹp và dễ đọc rồi. "Less is more" mà. "Test với màu mè": Nếu anh em không chắc đường viền của mình đang ở đâu hoặc có hiển thị đúng không, hãy tạm thời set color: Colors.red, width: 3.0 cho BorderSide đó. Nó sẽ "nhảy" ra ngay cho anh em thấy. "Hòa mình vào Theme": Thay vì dùng Colors.red, Colors.blue tùy tiện, hãy cố gắng lấy màu từ Theme.of(context).colorScheme hoặc các ColorScheme đã định nghĩa để bảng biểu của anh em trông "ăn nhập" với tổng thể ứng dụng hơn. TableBorder.all cho "mì ăn liền": Khi anh em cần nhanh gọn lẹ một cái bảng có đường viền đều đặn, TableBorder.all() là cứu tinh. Còn khi cần "độ" từng chi tiết, mới dùng TableBorder() constructor đầy đủ nhé. Ứng Dụng Thực Tế: TableBorder có mặt ở đâu? Anh em cứ nghĩ đến bất kỳ đâu cần hiển thị dữ liệu có cấu trúc dạng lưới là TableBorder có thể "nhảy" vào: Dashboard và Báo Cáo: Các ứng dụng quản lý tài chính, phân tích dữ liệu (như Google Analytics, các app quản lý kho hàng, CRM) thường dùng bảng để hiển thị các chỉ số, thống kê. TableBorder giúp phân chia rõ ràng các cột "doanh thu", "lợi nhuận", "số đơn hàng"... So Sánh Sản Phẩm: Trên các trang thương mại điện tử (Shopee, Lazada, Tiki), khi anh em xem bảng so sánh tính năng giữa các sản phẩm, đó chính là Table với TableBorder đang "làm nhiệm vụ" đấy. Lịch Biểu, Thời Khóa Biểu: Các ứng dụng lịch, quản lý công việc đôi khi dùng Table để hiển thị các khung giờ, sự kiện trong ngày/tuần, và đường viền giúp phân biệt các khoảng thời gian. Bảng Xếp Hạng (Game): Một số game có bảng xếp hạng người chơi, điểm số, TableBorder giúp bảng này trông "nghiêm túc" và dễ đọc hơn. Thử Nghiệm và Khi Nào Nên Dùng? Thử nghiệm: "Zebra Striping" (Sọc ngựa vằn): Anh em thử kết hợp TableBorder với decoration của TableRow (như trong ví dụ anh Creyt đã làm với BoxDecoration(color: Colors.blueGrey.shade100)). Thay đổi màu nền của các hàng xen kẽ để tạo hiệu ứng sọc, vừa đẹp mắt vừa dễ đọc. Không viền nhưng vẫn phân tách: Thử set BorderSide(width: 0.0) hoặc BorderStyle.none cho một số cạnh. Đôi khi, việc không có viền lại tạo ra hiệu ứng "khoảng trắng" (whitespace) tốt hơn, nhưng vẫn có các đường viền khác để giữ cấu trúc. Nên dùng TableBorder cho case nào? Khi dữ liệu có tính chất "số liệu, thống kê": Cần sự chính xác, rõ ràng trong từng ô. Khi cần "so sánh trực quan": Người dùng cần đặt các giá trị cạnh nhau để đối chiếu. Khi thiết kế yêu cầu "tính trang trọng, cấu trúc chặt chẽ": Ví dụ, báo cáo tài chính, danh sách điểm số, v.v. Không nên lạm dụng: Nếu anh em chỉ cần hiển thị một danh sách đơn giản, không có nhiều cột và không cần phân tách quá chặt chẽ, ListView hoặc một Column với các Row thông thường sẽ phù hợp và hiệu quả hơn. Table và TableBorder có "chi phí" render cao hơn một chút vì nó phải tính toán vị trí và kích thước của các ô để căn chỉnh. Vậy đó, anh em. TableBorder tuy nhỏ mà có võ, biến những con số khô khan thành tác phẩm nghệ thuật dễ đọc, dễ nhìn. Hãy thực hành và sáng tạo với nó nhé! Anh Creyt tin anh em sẽ "biến hình" được những cái bảng "xấu lạ" thành "siêu phẩm" visual! 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é!

32 Đọc tiếp