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

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

Author

Admin System

@root

Ngày xuất bản

22 Mar, 2026

Lượt xem

1 Lượt

"TextSpan"

Chào các đồng chí Gen Z, hôm nay chúng ta sẽ cùng Creyt khám phá một "phép thuật" nhỏ nhưng có võ trong Flutter, đó là TextSpan. Nghe cái tên có vẻ hơi "học thuật" nhưng tin thầy đi, nó dễ như ăn kẹo mà lại biến UI của bạn thành "level max" ngay lập tức.

1. TextSpan Là Gì Mà Lại "Hot" Thế?

Để dễ hình dung, các bạn cứ tưởng tượng thế này: Bạn có một bức tường trống và muốn trang trí nó. Nếu dùng Text widget thông thường, thì giống như bạn chỉ có một cuộn giấy dán tường to đùng, dán hết cả bức tường một kiểu duy nhất. Chán òm!

Nhưng với TextSpan, bạn như có trong tay một bộ sưu tập sticker đủ loại, đủ màu sắc, đủ hình dáng, thậm chí có cả sticker phát sáng hay sticker có thể chạm vào để mở nhạc. Bạn có thể dán mỗi miếng sticker vào một vị trí, tạo nên một tác phẩm nghệ thuật đa dạng, sống động ngay trên bức tường chữ của mình.

Nói một cách "coder" hơn, TextSpan không phải là một widget độc lập mà là một thành phần cấu tạo bên trong RichText widget. Nó cho phép bạn định nghĩa các đoạn văn bản (hay "span" - đoạn nhỏ) với các thuộc tính styling (font size, color, weight, v.v.) và hành vi (như onTap - khi chạm vào) khác nhau, tất cả trong cùng một khối văn bản duy nhất. Mục đích là để "mix & match" nhiều style và tương tác trên cùng một dòng chữ.

2. Code Ví Dụ Minh Họa: "Thấy Tận Mắt, Rờ Tận Tay"

Giờ thì không nói nhiều nữa, chúng ta cùng xem TextSpan nó "ảo diệu" thế nào qua ví dụ code sau:

import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart'; // Quan trọng để dùng TapGestureRecognizer

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TextSpan Demo của thầy Creyt',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TextSpanScreen(),
    );
  }
}

class TextSpanScreen extends StatelessWidget {
  const TextSpanScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TextSpan của thầy Creyt'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              RichText(
                text: TextSpan(
                  text: 'Chào bạn, đây là một ',
                  style: const TextStyle(
                    fontSize: 18,
                    color: Colors.black87,
                    fontFamily: 'Roboto',
                  ),
                  children: <TextSpan>[
                    TextSpan(
                      text: 'ví dụ siêu cool ',
                      style: const TextStyle(
                        fontWeight: FontWeight.bold,
                        color: Colors.deepPurple,
                      ),
                    ),
                    TextSpan(
                      text: 'về ',
                      style: const TextStyle(
                        fontStyle: FontStyle.italic,
                        color: Colors.grey,
                      ),
                    ),
                    TextSpan(
                      text: 'TextSpan ',
                      style: const TextStyle(
                        color: Colors.red,
                        fontSize: 20,
                        decoration: TextDecoration.underline,
                      ),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('Bạn vừa chạm vào TextSpan đó nha!')), 
                          );
                        },
                    ),
                    TextSpan(
                      text: 'trong Flutter. ',
                    ),
                    TextSpan(
                      text: 'Click vào đây ',
                      style: const TextStyle(
                        color: Colors.blue,
                        decoration: TextDecoration.underline,
                      ),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('Mở link gì đó đi bạn!')), 
                          );
                          // Trong ứng dụng thực tế, bạn có thể dùng url_launcher để mở URL:
                          // launchUrl(Uri.parse('https://creyt.dev')); 
                        },
                    ),
                    TextSpan(
                      text: 'để xem điều bất ngờ!',
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 30),
              // Một ví dụ khác với hashtag và @mention
              RichText(
                text: TextSpan(
                  text: 'Đừng quên theo dõi ',
                  style: const TextStyle(
                    fontSize: 16,
                    color: Colors.black54,
                  ),
                  children: <TextSpan>[
                    TextSpan(
                      text: '#CreytDev ',
                      style: const TextStyle(
                        fontWeight: FontWeight.w600,
                        color: Colors.teal,
                      ),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('Tìm Creyt trên mạng xã hội!')), 
                          );
                        },
                    ),
                    TextSpan(
                      text: 'và ',
                    ),
                    TextSpan(
                      text: '@FlutterGenz ',
                      style: const TextStyle(
                        fontWeight: FontWeight.w600,
                        color: Colors.orangeAccent,
                      ),
                      recognizer: TapGestureRecognizer()
                        ..onTap = () {
                          ScaffoldMessenger.of(context).showSnackBar(
                            const SnackBar(content: Text('Khám phá thêm về Flutter cho Gen Z!')), 
                          );
                        },
                    ),
                    TextSpan(
                      text: 'nhé!',
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Giải thích code:

  • Chúng ta dùng RichText widget làm "container" chính. Nó nhận một TextSpan làm thuộc tính text.
  • TextSpan gốc (root TextSpan) sẽ định nghĩa textstyle mặc định cho toàn bộ khối văn bản.
  • Bên trong children của TextSpan gốc, chúng ta có thể thêm nhiều TextSpan con khác. Mỗi TextSpan con này có thể có textstyle riêng, ghi đè lên style của cha nếu được định nghĩa.
  • Điểm đặc biệt là thuộc tính recognizer. Ở đây, chúng ta dùng TapGestureRecognizer để bắt sự kiện chạm (tap) vào một phần văn bản cụ thể. Khi chạm, nó sẽ gọi hàm onTap và bạn có thể thực hiện bất kỳ hành động nào, như hiển thị SnackBar hay mở một URL.
Illustration

3. Mẹo Hay & Best Practices Từ Creyt

  • Đừng lạm dụng: Nếu bạn chỉ cần một đoạn văn bản với một kiểu chữ duy nhất, hãy dùng Text('Hello', style: TextStyle(...)) cho gọn gàng và hiệu quả hơn. RichText với TextSpan có một chút "overhead" (chi phí xử lý) nhỏ hơn Text đơn giản.
  • Quản lý recognizer cẩn thận: Khi dùng TapGestureRecognizer (hoặc các GestureRecognizer khác), nếu widget của bạn là StatefulWidget, bạn nên khởi tạo recognizer trong initState và nhớ dispose nó trong dispose để tránh memory leak. Trong StatelessWidget như ví dụ trên, Flutter sẽ tự quản lý khá tốt cho các trường hợp đơn giản, nhưng với các logic phức tạp hơn, cân nhắc StatefulWidgetdispose thủ công.
  • Kế thừa Style (Inherited styles): TextSpan có thuộc tính style. Nếu bạn không định nghĩa style cho một TextSpan con, nó sẽ tự động kế thừa style từ TextSpan cha gần nhất. Tận dụng điều này để giảm thiểu việc lặp lại code style.
  • Accessibility (Khả năng tiếp cận): Đảm bảo các phần text có thể tương tác (như link, button text) có đủ độ tương phản màu sắc và được các công cụ hỗ trợ đọc màn hình (screen reader) nhận diện đúng. Điều này giúp ứng dụng của bạn thân thiện hơn với mọi người dùng.

4. Ứng Dụng Thực Tế: "TextSpan Đã Có Mặt Ở Đâu?"

Bạn có thể bất ngờ khi biết TextSpan (hoặc các kỹ thuật tương tự ở các nền tảng khác) xuất hiện ở khắp mọi nơi:

  • Mạng xã hội (Facebook, Twitter, Instagram): Khi bạn thấy một bài đăng có @mention người khác, #hashtag, hoặc link web được highlight và có thể click được, đó chính là một ứng dụng kinh điển của việc định dạng văn bản giàu có.
  • Ứng dụng Chat (Zalo, Messenger): Tin nhắn có link, số điện thoại, email được tự động nhận diện và biến thành clickable text.
  • Điều khoản sử dụng/Chính sách bảo mật: Các văn bản dài thường có những đoạn từ khóa quan trọng hoặc link đến các chính sách con được định dạng khác biệt và có thể click.
  • Ứng dụng đọc tin tức/blog: Tiêu đề, trích dẫn, hoặc các đoạn text đặc biệt trong bài viết được định dạng riêng để thu hút sự chú ý.

5. Thử Nghiệm & Nên Dùng Cho Case Nào?

Nên dùng TextSpan khi:

  • Bạn cần một đoạn văn bản duy nhất nhưng lại muốn mỗi phần của nó có một style riêng biệt (màu sắc, kích thước, font, in đậm, gạch chân, v.v.). Đây là "sân nhà" của nó.
  • Bạn muốn một phần của văn bản có thể tương tác được (click để mở link, hiển thị tooltip, v.v.) mà không cần phải tách thành các widget riêng biệt.
  • Bạn đang xây dựng các tính năng như tự động highlight từ khóa tìm kiếm, hiển thị các tag, hoặc tạo các rich text editor cơ bản.

Không nên dùng TextSpan khi:

  • Bạn chỉ cần một đoạn văn bản với một style duy nhất. Dùng Text('Hello', style: TextStyle(...)) là đủ và hiệu quả hơn rất nhiều.
  • Khi bạn cần các khối văn bản độc lập hoàn toàn và muốn kiểm soát layout của chúng bằng các widget như Row, Column. TextSpan chỉ làm việc bên trong một khối văn bản duy nhất, không phải để sắp xếp các khối text riêng lẻ.

Thuộc Series: Flutter

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!