
Chào các dân chơi hệ dev! Anh Creyt lại lên sóng rồi đây. Hôm nay, chúng ta sẽ cùng mổ xẻ một cái tên nghe hơi “nghiêm túc” nhưng lại cực kỳ xịn xò trong Flutter: WidgetSpan. Nghe tên là thấy có "widget" và "span" rồi đúng không? Đừng lo, anh sẽ giải thích cho các em hiểu nó bá đạo cỡ nào!
1. WidgetSpan là gì? Để làm gì mà oách vậy?
Thử tưởng tượng thế này: em có một bức tường toàn chữ là chữ, khô khan như tiền lương cuối tháng vậy. Bình thường, cái Text widget của chúng ta chỉ biết hiển thị chữ thôi, đúng không? Muốn chèn thêm một cái icon mặt cười, một cái nút bấm, hay một cái avatar nhỏ xíu vào giữa dòng chữ thì sao? Bó tay à?
Đó chính là lúc WidgetSpan xuất hiện như một "cửa sổ thần kỳ" trên bức tường chữ đó! Nói một cách hàn lâm hơn, WidgetSpan là một class con của InlineSpan – cái này là "anh em họ" với TextSpan mà các em hay dùng để đổi màu, đổi font cho từng phần text ấy. Nhưng thay vì chỉ đổi kiểu chữ, WidgetSpan cho phép em nhúng bất kỳ Widget nào vào giữa một chuỗi văn bản.
Mục đích của nó? Đơn giản là để biến những đoạn văn bản tĩnh thành những tác phẩm nghệ thuật UI động, đầy đủ hình ảnh, icon, thậm chí là các widget tương tác ngay giữa dòng. Nó giải quyết bài toán "tôi muốn có cái này ngay cạnh cái chữ kia mà không cần phải dùng Row hay Column phức tạp". Chính xác là để tạo ra những "rich text" (văn bản đa dạng) mà chỉ Text đơn thuần không thể làm được.
À mà nhớ nha, WidgetSpan không đứng một mình đâu, nó luôn cần một "người anh cả" là RichText để phát huy sức mạnh. RichText chính là cái "khung" cho phép em kết hợp nhiều loại InlineSpan (bao gồm TextSpan và WidgetSpan) lại với nhau.
2. Code Ví Dụ Minh Họa: Xem "cửa sổ thần kỳ" hoạt động này!
Giờ thì lý thuyết đã đủ, chúng ta cùng "thực chiến" để xem WidgetSpan làm được gì nhé. Đây là một ví dụ kinh điển:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WidgetSpan 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('WidgetSpan by Creyt'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: const TextStyle(
color: Colors.black,
fontSize: 20,
height: 1.5, // Điều chỉnh chiều cao dòng để widget không bị cắt
),
children: <InlineSpan>[
const TextSpan(text: 'Chào bạn, đây là một đoạn văn bản thú vị với '),
WidgetSpan(
child: Icon(
Icons.star,
color: Colors.amber,
size: 24,
),
alignment: PlaceholderAlignment.middle, // Căn giữa icon theo chiều dọc
baseline: TextBaseline.alphabetic, // Quan trọng để căn chỉnh đúng
),
const TextSpan(text: ' một ngôi sao lấp lánh và một nút bấm '),
WidgetSpan(
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bạn vừa nhấn nút!')),
);
},
icon: const Icon(Icons.thumb_up, size: 16),
label: const Text('Thích'),
style: ElevatedButton.styleFrom(
minimumSize: Size.zero, // Loại bỏ padding mặc định
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
tapTargetSize: MaterialTapTargetSize.shrinkWrap, // Giảm kích thước vùng chạm
),
),
alignment: PlaceholderAlignment.middle,
baseline: TextBaseline.alphabetic,
),
const TextSpan(text: ' ngay trong dòng chữ. Thật vi diệu!'),
],
),
),
),
),
);
}
}
Trong ví dụ này, các em thấy không? Chúng ta có thể chèn một Icon và thậm chí là một ElevatedButton.icon có thể nhấn được, ngay giữa đoạn Text! Không cần Row, không cần Column phức tạp để sắp xếp. Quá là tiện lợi!

3. Mẹo (Best Practices) để ghi nhớ và dùng thực tế
RichTextlà bạn thân củaWidgetSpan: Luôn nhớ,WidgetSpanchỉ hoạt động bên trongRichText(hoặc các widget sử dụngRichTextngầm nhưTextkhi cóTextSpanphức tạp). Đừng cố gắng nhét nó vàoTextđơn giản nhé.alignmentvàbaselinelà "chìa khóa" của sự đẹp: Hai thuộc tính này trongWidgetSpancực kỳ quan trọng để căn chỉnh widget của em sao cho nó "ăn nhập" với dòng chữ xung quanh.alignment: Xác định cách widget được căn chỉnh theo chiều dọc so với dòng text. Các giá trị nhưPlaceholderAlignment.middle,PlaceholderAlignment.bottom,PlaceholderAlignment.topsẽ giúp em đặt widget ở giữa, dưới hoặc trên dòng text.baseline: Giúp Flutter biết điểm căn chỉnh chính xác của widget so với đường baseline của chữ. Thường thìTextBaseline.alphabetichoặcTextBaseline.ideographiclà những lựa chọn tốt nhất. Cứ thử và cảm nhận sự khác biệt nhé!
- Cẩn thận với hiệu suất: Dù mạnh mẽ, nhưng việc nhúng quá nhiều widget phức tạp vào một
RichTextlớn có thể ảnh hưởng đến hiệu suất rendering. MỗiWidgetSpanlà một widget con riêng biệt, và Flutter phải tính toán layout cho từng cái. Dùng khi cần, đừng lạm dụng như "thần dược" nhé. - Accessibility (Khả năng tiếp cận): Khi nhúng các widget tương tác (như nút bấm), hãy đảm bảo rằng người dùng khiếm thị hoặc dùng trình đọc màn hình vẫn có thể tương tác và hiểu được nội dung. Cung cấp
semanticsLabelnếu cần. - Keep It Simple, Stupid (KISS): Đôi khi, giải pháp dùng
RowhoặcColumnđể sắp xếpTextvà các widget riêng biệt lại dễ quản lý và debug hơn. Chỉ dùngWidgetSpankhi em thực sự muốn một widget nằm trong cùng một dòng với văn bản, như một phần không thể tách rời của dòng chữ.
4. Ví dụ thực tế các ứng dụng/website đã ứng dụng
Các em có thấy các ứng dụng chat, mạng xã hội, hay các trình soạn thảo văn bản hiện đại không? Chúng nó dùng cái này suốt đấy!
- Mạng xã hội (Twitter, Facebook): Khi em thấy các hashtag (
#Flutter), mention (@Creyt), hay các emoji được hiển thị ngay trong dòng text của một bài đăng, đó chính là một biến thể củaWidgetSpan(hoặc các kỹ thuật tương tự) đang hoạt động. Các link có thể nhấn được cũng là một dạngTextSpanđặc biệt. - Ứng dụng chat (Zalo, Telegram): Chèn emoji, icon trạng thái, hoặc thậm chí là các sticker nhỏ ngay giữa cuộc hội thoại. Đó là cách họ làm cho đoạn chat của em sinh động hơn.
- Trình soạn thảo văn bản (Notion, Medium): Khi em viết bài và có thể chèn một block code, một hình ảnh, hoặc một video ngay giữa đoạn văn, đó là một phiên bản "nâng cấp" của việc nhúng nội dung vào văn bản.
WidgetSpantrong Flutter là một bước đi theo hướng đó, cho phép em kiểm soát từng phần nhỏ hơn. - Game UI: Hiển thị thông tin người chơi như level, huy hiệu, hoặc chỉ số nhỏ gọn ngay trong đoạn mô tả nhân vật hoặc vật phẩm.
5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Anh Creyt đã từng thử nghiệm WidgetSpan trong nhiều dự án, từ việc tạo ra một trình soạn thảo rich text đơn giản cho đến việc hiển thị các tag tương tác trong danh sách sản phẩm. Kinh nghiệm cho thấy:
Nên dùng WidgetSpan khi:
- Cần nhúng icon, emoji, hoặc một hình ảnh nhỏ ngay giữa một câu, một đoạn văn bản để minh họa hoặc tạo điểm nhấn.
- Muốn tạo các "chip" hoặc "tag" nhỏ có thể tương tác (ví dụ: nhấn vào để lọc nội dung) ngay trong dòng mô tả sản phẩm/bài viết.
- Hiển thị các chỉ số, trạng thái nhỏ gọn (ví dụ: số lượng like kèm icon trái tim, trạng thái online/offline bằng chấm màu) ngay cạnh tên người dùng hoặc tiêu đề.
- Tạo hiệu ứng "mention" trong các ứng dụng mạng xã hội hoặc chat, nơi tên người dùng được highlight và có thể nhấn vào.
Không nên lạm dụng hoặc cân nhắc giải pháp khác khi:
- Mục đích chính là sắp xếp các widget theo chiều dọc hoặc ngang: Nếu em chỉ muốn đặt một icon bên cạnh một đoạn text, và icon đó không cần phải "nằm" trong dòng text một cách chặt chẽ, thì
RowhoặcColumnsẽ đơn giản và dễ quản lý hơn nhiều. - Nhúng các widget phức tạp, có kích thước lớn, hoặc có nhiều tương tác riêng biệt: Ví dụ, nhúng cả một
ListViewhay mộtImagelớn vàoWidgetSpanlà một ý tưởng tồi. Nó sẽ làm cho layout củaRichTexttrở nên khó đoán và có thể gây lỗi hiển thị hoặc hiệu suất kém. - Cần kiểm soát layout chi tiết cho từng phần:
WidgetSpansẽ cố gắng căn chỉnh widget của em theo dòng text. Nếu em cần kiểm soát vị trí, kích thước một cách độc lập hơn, thì nên tách ra thành các widget riêng và sắp xếp bằngRow,Column,Stack.
Nhớ nhé, WidgetSpan là một công cụ mạnh mẽ, nhưng như mọi công cụ khác, nó cần được dùng đúng lúc, đúng chỗ. Đừng biến nó thành "búa tạ" để đóng đinh, hãy dùng nó như một "dao mổ" tinh xảo. Cứ thử nghiệm, phá cách, nhưng phải hiểu rõ bản chất của nó. Chúc các em code ra những con app "đỉ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é!