Widget trong Flutter là gì? Hiểu StatelessWidget và StatefulWidget


Nếu phải chọn một khái niệm quan trọng nhất của Flutter, đó là widget. Không phải vì widget khó, mà vì mọi thứ trong Flutter đều là widget: chữ, ảnh, khoảng cách, canh lề, nút, màn hình, cả app. Hiểu widget là hiểu cách Flutter suy nghĩ.
Bài này là bài thứ ba trong series 4 bài về Flutter. Hai bài trước đã trả lời "có nên học Flutter" và dạy nền tảng Dart; bài này đi vào giai đoạn 2 của lộ trình: widget và giao diện.
Sau bài này bạn sẽ: hiểu widget thực chất là gì, phân biệt chính xác StatelessWidget và StatefulWidget, nắm vòng đời của State, biết dùng setState đúng cách, và có tiêu chí rõ ràng để chọn loại widget nào cho từng tình huống.
Widget là gì? Và vì sao "mọi thứ đều là widget"
Định nghĩa ngắn: widget là một bản mô tả giao diện bất biến (immutable). Nó không phải là "thành phần giao diện đang chạy", mà là bản thiết kế để Flutter biết cần vẽ gì.
Trong Flutter, đoạn code này:
[object Object]Có 5 widget: Padding, Column, Text, SizedBox, Icon. Kể cả Padding — thứ chỉ để thêm khoảng cách — cũng là một widget, chứ không phải một thuộc tính CSS. Đây là điểm khác biệt lớn nhất khi chuyển từ web sang Flutter: bạn lắp ghép widget thay vì viết HTML + CSS tách rời nhau.
Ba hệ quả thực tế của việc "mọi thứ là widget":
- Không có CSS. Muốn đổi màu chữ? Dùng
Textvớistyle: TextStyle(color: ...). Muốn bo góc? DùngClipRRecthoặcBoxDecoration. Muốn canh giữa? DùngCenter. - Tất cả đều được tạo bằng constructor của Dart. Vì vậy mọi khái niệm Dart ở bài 2 (class, named parameter,
required,const) xuất hiện ở đây. - Giao diện là một cây. Widget cha chứa widget con, tạo thành widget tree. Muốn hiểu bố cục, bạn phải hình dung được cái cây đó.
Widget, Element và RenderObject — hiểu đúng để không vô tình làm app chậm
Ba khái niệm này hay bị gộp làm một, nhưng phân biệt được sẽ giúp bạn hiểu vì sao const lại quan trọng.
| Thành phần | Bản chất | Đặc điểm |
|---|---|---|
| Widget | Bản mô tả (immutable) | Nhẹ, tạo mới liên tục, không giữ trạng thái |
| Element | "Chỗ neo" của widget trong cây | Ít thay đổi, giữ liên kết widget ↔ render object |
| RenderObject | Thành phần thực hiện tính toán layout + vẽ | Nặng, quản lý kích thước và vị trí |
Khi state thay đổi, Flutter không vẽ lại toàn bộ app: nó tạo lại widget (rẻ), so sánh với cây element (trung bình), và chỉ cập nhật render object khi thật sự cần (đắt). Đây là lý do widget được thiết kế bất biến và nhẹ — bạn yên tâm tạo hàng nghìn widget mà không lo tốn kém.
Cũng chính vì vậy: const widget giúp Flutter biết "widget này không bao giờ đổi" và bỏ qua việc so sánh, tiết kiệm đáng kể khi giao diện lớn.
Ba nhóm widget bạn cần phân biệt
Trong Flutter, widget chia thành ba nhóm theo cách quản lý dữ liệu:
- StatelessWidget — widget không giữ state, chỉ nhận dữ liệu vào và vẽ ra. Ví dụ:
Text,Icon, một thẻ sản phẩm chỉ hiển thị. - StatefulWidget — widget có state nội bộ, có thể thay đổi trong vòng đời của nó. Ví dụ: ô nhập liệu, nút đếm, danh sách đang tải.
- Widget kế thừa/truyền dữ liệu (InheritedWidget, Provider, Riverpod…) — widget dùng để chia sẻ dữ liệu xuống cây con. Nhóm này sẽ học ở giai đoạn 3 của lộ trình; trong bài này chỉ cần biết nó tồn tại.
Bản đồ widget bố cục cần biết trước
Trước khi nói về state, bạn cần biết bộ widget bố cục dùng hằng ngày. Hầu hết giao diện Flutter chỉ được ghép từ khoảng 12 widget dưới đây:
| Widget | Công dụng | Ghi nhớ |
|---|---|---|
Scaffold |
Khung màn hình: app bar, body, FAB, bottom nav | Mỗi màn hình thường có 1 Scaffold |
AppBar |
Thanh tiêu đề trên cùng | Thuộc Scaffold.appBar |
Container |
Khối có padding, margin, kích thước, màu, bo góc | Mạnh nhưng nặng — Padding/SizedBox nhẹ hơn |
Padding |
Thêm khoảng cách bên trong | Nên dùng thay Container khi chỉ cần padding |
SizedBox |
Khoảng trống có kích thước cố định | SizedBox(height: 8) là cách tạo khoảng cách phổ biến nhất |
Center / Align |
Canh giữa / canh theo vị trí | Center = Align(alignment: center) |
Row |
Xếp con theo hàng ngang | Cần Expanded để tránh tràn ngang |
Column |
Xếp con theo cột dọc | Cần Expanded/SingleChildScrollView để tránh tràn dọc |
Expanded / Flexible |
Chia phần không gian còn lại | Expanded(flex: 2) để chiếm gấp đôi |
Stack |
Xếp chồng lớp (ảnh + chữ đè lên) | Dùng Positioned để đặt vị trí |
ListView |
Danh sách cuộn | Dùng .builder cho danh sách dài |
SingleChildScrollView |
Cuộn nội dung tĩnh | Tránh đặt ListView bên trong |
[object Object]Đọc một widget tree thực tế
Khi nhìn một màn hình Flutter, hãy tập đọc từ ngoài vào trong:
[object Object]Kỹ năng đọc cây này quan trọng hơn việc nhớ tên widget: khi giao diện bị tràn (RenderFlex overflowed), lỗi hầu như luôn nằm ở chỗ thiếu Expanded, Flexible hoặc thiếu widget cuộn. Hiểu cây là biết ngay phải sửa ở đâu.
StatelessWidget: widget không có trạng thái
StatelessWidget là widget được mô tả hoàn toàn bằng dữ liệu truyền vào. Nó không thể tự đổi giao diện sau khi được tạo.
[object Object]Những điểm cần nắm:
- Phải override
build(BuildContext context)— đây là nơi bạn trả về cây widget con. super.key— truyền key lên lớp cha, giúp Flutter nhận diện widget trong danh sách.constconstructor khi mọi tham số có thể là hằng — giúp tăng hiệu năng.contextlà "vị trí của widget trong cây", dùng để truy cập theme, kích thước màn hình, dữ liệu kế thừa, điều hướng.
Khi nào dùng StatelessWidget: khi giao diện chỉ phụ thuộc vào dữ liệu đầu vào — thẻ sản phẩm, tiêu đề, icon, badge, footer, một khối tĩnh. Theo kinh nghiệm, khoảng 60–70% widget trong một app tốt là StatelessWidget. Nếu bạn thấy mình dùng StatefulWidget ở mọi nơi, rất có thể bạn đang dùng state nhiều hơn cần thiết.
context là gì và 3 việc bạn sẽ làm với nó
BuildContext là vị trí của widget trong cây widget. Nó không phải "trạng thái toàn cục" mà là con trỏ giúp widget tìm dữ liệu ở các widget cha.
Ba việc bạn gặp mỗi ngày:
[object Object]Hai lưu ý thực tế:
- Không lưu
contextvào biến rồi dùng sau khi hàmasynckết thúc. Trước khi dùng, hãy kiểm traif (!mounted) return;(trongState) hoặcif (!context.mounted) return;. - Không tạo widget cần
contextở ngoàibuild— hãy tạo bên trongbuildhoặc truyền như tham số.
StatefulWidget: widget có trạng thái
StatefulWidget gồm hai class: class widget (bất biến) và class State (giữ dữ liệu có thể thay đổi).
[object Object]Bốn điểm quan trọng:
- State nằm trong class
_BoDemState, không nằm trong widget. Widget có thể bị tạo lại nhiều lần, cònStateđược giữ lại giữa các lần build. - Đọc tham số của widget qua
widget.— ví dụwidget.buoc. - Muốn giao diện đổi, phải gọi
setState. Sửa biến mà không gọisetStatethì giá trị đổi nhưng màn hình không cập nhật. createState()là cầu nối giữa widget và state.
Vòng đời của State — phần quyết định bạn có viết đúng hay không
Đây là kiến thức tách người "copy code chạy được" khỏi người hiểu Flutter.
[object Object]| Hàm | Chạy khi nào | Dùng để làm gì |
|---|---|---|
initState |
1 lần, sau khi state được gắn vào cây | Khởi tạo TextEditingController, AnimationController, gọi API ban đầu |
didChangeDependencies |
Sau initState và mỗi khi dependency đổi |
Đọc dữ liệu từ InheritedWidget, theme, ngôn ngữ |
build |
Nhiều lần | Trả về cây widget — phải thuần khiết, không gọi API, không setState |
didUpdateWidget |
Khi cha truyền tham số mới | So sánh oldWidget với widget, xử lý khi cấu hình đổi |
dispose |
1 lần khi widget bị xoá vĩnh viễn | controller.dispose(), huỷ subscription, đóng stream |
Ví dụ đúng chuẩn — nhớ dispose controller:
[object Object]Quên dispose là lỗi phổ biến nhất của người mới — app vẫn chạy nhưng rò rỉ bộ nhớ, animation giật, và lỗi chỉ lộ ra khi màn hình mở/đóng nhiều lần.
setState đúng cách: 4 lỗi kinh điển
Lỗi 1 — Gọi setState sau khi widget bị xoá
[object Object]Lỗi 2 — Gọi setState bên trong build
Sẽ gây vòng lặp vô hạn: build → setState → build → … Flutter sẽ báo lỗi. Việc dẫn xuất dữ liệu (ví dụ đếm số phần tử) hãy tính trực tiếp trong build, không setState.
Lỗi 3 — Sửa state nhưng không gọi setState
[object Object]Lỗi 4 — setState bọc quá rộng
setState khiến widget hiện tại và toàn bộ cây con build lại. Bọc cả một hàm dài trong setState là nguyên nhân làm app giật. Chỉ đặt thay đổi state bên trong, và tách widget con thành const/StatelessWidget khi có thể.
Khi nào dùng StatelessWidget, khi nào dùng StatefulWidget?
Đây là câu hỏi bị hỏi nhiều nhất. Tiêu chí gọn nhất: giao diện có tự thay đổi trong lúc hiển thị hay không?
| Tình huống | Chọn |
|---|---|
| Hiển thị dữ liệu nhận từ cha | StatelessWidget |
| Thẻ sản phẩm, badge, tiêu đề, icon, footer | StatelessWidget |
| Widget chỉ nhận callback rồi gọi ra ngoài | StatelessWidget |
| Ô nhập liệu, nút bật/tắt, bộ đếm | StatefulWidget |
| Trạng thái tải/lỗi/thành công của một khối | StatefulWidget |
| Animation, timer, gọi API rồi hiển thị kết quả | StatefulWidget |
| Đang phân vân | Bắt đầu bằng StatelessWidget, chỉ nâng lên Stateful khi thật cần |
Một nguyên tắc thực dụng: đẩy state lên nơi cao nhất nhưng không cao hơn mức cần thiết. Nếu hai widget con cần chia sẻ state, hãy để state ở widget cha chung gần nhất; nếu state chỉ dùng trong một khối, giữ nó tại khối đó. Việc này giúp app ít rebuild và dễ debug.
State nên đặt ở đâu: 3 ví dụ sai và cách sửa
Sai 1 — Đặt state ở quá thấp, hai widget con không chia sẻ được
Bạn có màn hình gồm ô nhập và danh sách kết quả; nếu để state trong widget ô nhập thì danh sách không biết dữ liệu. Cách sửa: đưa state lên widget cha chung, truyền dữ liệu xuống và truyền callback lên:
[object Object]ONhapTuKhoa và KetQuaTimKiem đều là StatelessWidget — chỉ có cha giữ state. Đây là mẫu phổ biến nhất và cũng dễ debug nhất.
Sai 2 — Đặt state ở quá cao
Đặt setState ở cả màn hình chỉ để một nút đổi màu sẽ làm toàn bộ màn hình build lại mỗi lần bấm. Cách sửa: tách nút đó thành StatefulWidget riêng.
Sai 3 — Copy state thành hai bản
Nhận dữ liệu từ cha vào initState rồi lưu vào một field mới — khi cha đổi thì bản sao không đổi. Cách sửa: hoặc dùng trực tiếp widget.duLieu, hoặc xử lý chuyển dữ liệu trong didUpdateWidget.
Hiệu năng: 4 thói quen nên tập ngay
- Dùng
constở mọi nơi có thể.const Text('Xin chào')không bao giờ cần build lại. - Tách widget nhỏ thay vì viết một hàm
builddài 300 dòng. Widget nhỏ thì chỉ widget đó build lại. - Ưu tiên StatelessWidget khi không cần state; bọc phần thay đổi trong widget riêng.
- Dùng
keyđúng chỗ. VớiListViewcó dữ liệu thay đổi,ValueKey(id)giúp Flutter nhận diện đúng phần tử thay vì dựng lại sai vị trí:
[object Object]Ví dụ xuyên suốt: nâng cấp một widget từ Stateless lên Stateful
Cách học hiệu quả nhất là xem cùng một widget ở hai phiên bản. Giả sử bạn có thẻ hiển thị số dư ví:
Bước 1 — Bắt đầu bằng StatelessWidget (đúng, vì chưa cần state)
[object Object]Bước 2 — Yêu cầu đổi: cần nút "Âm thầm số dư". Lúc này mới nâng lên StatefulWidget
[object Object]Hãy chú ý hai điều trong ví dụ này:
soDuvẫn là dữ liệu vào bất biến — truy cập quawidget.soDu, không copy sang field củaState. Lỗi phổ biến là copy nó vàoinitStaterồi dữ liệu không cập nhật khi cha truyền giá trị mới._anlà state thật sự — chỉ nó mới nằm trongStatevà mới cầnsetState.
Quy tắc rút ra: trạng thái hiển thị thuộc về widget đó thì để trong State; dữ liệu nghiệp vụ thuộc về cha/nguồn dữ liệu thì truyền xuống như tham số.
Thực hành: 2 bài tập widget nên làm
Bài tập 1 — App đếm có lịch sử. Tạo StatefulWidget hiển thị số đếm, 2 nút tăng/giảm, và danh sách 5 lần thay đổi gần nhất. Luyện: state là List<int>, setState, cập nhật danh sách.
Bài tập 2 — Danh sách có ô tìm kiếm. Một StatefulWidget chứa TextField và ListView; gõ tới đâu lọc danh sách tới đó. Luyện: controller + listener, where() trên danh sách, tách widget con thành StatelessWidget, và dispose controller trong dispose().
Hai bài này nhỏ nhưng chạm đủ mọi khái niệm quan trọng: StatelessWidget, StatefulWidget, vòng đời, setState, danh sách, hiệu năng.
Checklist: bạn đã hiểu widget chưa?
Tự đánh giá bằng 8 câu — nếu trả lời được hết, bạn đã sẵn sàng học quản lý trạng thái:
- Giải thích được vì sao widget phải bất biến, và state nằm ở đâu.
- Phân biệt được widget, element, render object và biết cái nào "đắt".
- Biết khi nào
setStatelà cần thiết và khi nào không. - Kể được 6 hàm trong vòng đời
Statevà việc nên làm ở từng hàm. - Luôn
disposecontroller, animation, subscription. - Biết vì sao phải kiểm tra
mountedtrước khisetStatesauawait. - Biết dùng
const, tách widget nhỏ và dùngValueKeykhi cần. - Đọc được một cây widget và đoán chỗ gây
RenderFlex overflowed.
So sánh với các bài viết liên quan
Wiki nội bộ của site đã có vài bài ngắn về chủ đề này — bạn có thể đọc thêm để so sánh góc nhìn:
- StatelessWidget và StatefulWidget — khi nào dùng loại nào? — bản ngắn, tập trung vào tiêu chí chọn.
- Giải thích Widget trong Flutter: Stateless vs Stateful chi tiết — bản ngắn, giải thích khái niệm.
- Vòng đời Widget (Widget Lifecycle) trong Flutter — đi sâu phần lifecycle.
Bài bạn đang đọc là bản đầy đủ nhất: gộp cả ba chủ đề trên và bổ sung phần hiệu năng + bài tập.
Câu hỏi thường gặp
StatelessWidget có thật sự "không đổi" không?
Widget là bất biến, nhưng giao diện do nó tạo ra vẫn thay đổi được — khi widget cha tạo lại nó với dữ liệu mới. Điểm khác biệt: StatelessWidget không tự đổi, còn StatefulWidget có thể tự đổi qua setState.
Vì sao phải có 2 class cho StatefulWidget, không gộp thành 1?
Vì widget phải bất biến còn state thì phải đổi được. Tách ra giúp Flutter tái sử dụng State (giữ dữ liệu, controller, animation) trong khi tạo lại widget với tham số mới.
Đặt setState ở đâu là đúng?
Đặt ở nơi gần nhất bao trùm mọi widget cần cập nhật, không cao hơn. Nếu chỉ một nút cần đổi màu, hãy bọc nút đó thành StatefulWidget riêng thay vì setState ở cả màn hình.
Có cần dùng StatefulWidget để gọi API không?
Cần một widget có state để lưu kết quả và trạng thái tải. Nhưng khi app lớn hơn, bạn sẽ chuyển sang FutureBuilder hoặc thư viện quản lý trạng thái (Provider, Riverpod, Bloc) — nội dung của giai đoạn 3.
Khi nào nên học quản lý trạng thái (Provider, Riverpod, Bloc)?
Khi bạn thấy state phải truyền qua 3–4 tầng widget, hoặc nhiều màn hình cùng dùng một nguồn dữ liệu. Trước đó, setState vẫn là lựa chọn đúng và đơn giản nhất.
Widget khác gì component của React?
Về ý tưởng: khá giống — đều là mô tả giao diện từ dữ liệu vào. Khác ở chỗ widget trong Flutter bất biến và được tạo lại liên tục, còn state không nằm trong widget mà ở class State riêng. Hiểu điểm này giúp bạn không thắc mắc "vì sao biến trong widget không giữ được giá trị".
rebuild thực ra là gì, có tốn kém không?
Rebuild = gọi lại build() để tạo cây widget mới. Việc tạo widget rẻ; chi phí thật nằm ở layout và vẽ (render object) — Flutter chỉ làm phần đó khi thật sự cần. Vì vậy đừng sợ setState, hãy chỉ tránh setState quá rộng và tận dụng const.
Bước tiếp theo
Bạn vừa nắm phần khó nhất của giao diện Flutter. Ba việc nên làm tiếp:
- Làm 2 bài tập ở trên; đừng bỏ qua phần
dispose— đây là thói quen tách người mới với người đi làm. - Ôn lại nền tảng Dart nếu còn chỗ chưa chắc: Dart cho người mới: 10 khái niệm cần nắm.
- Chốt lộ trình tổng thể nếu bạn chưa đọc bài mở đầu: Có nên học Flutter 2026?.
Bài 4 (bài cuối series) là phần thực chiến được quan tâm nhất: Tích hợp Gemini AI vào app Flutter bằng ví dụ thực tế — chatbot, tóm tắt nội dung và gợi ý thông minh, kèm cách xử lý API key an toàn.
Series 4 bài học Flutter cho người mới
- Có nên học Flutter năm 2026? Lộ trình khóa học lập trình Flutter
- Dart cho người mới: 10 khái niệm cần nắm trước khi viết app Flutter
- Widget trong Flutter là gì? Hiểu StatelessWidget và StatefulWidget (bài này)
- Tích hợp Gemini AI vào app Flutter bằng ví dụ thực tế
Nếu muốn học có người sửa bài và làm dự án thật từ đầu, bạn có thể xem khóa học Flutter thực chiến, lộ trình nâng cao hoặc lịch khai giảng.
Bài viết liên quan
Dart cho người mới: 10 khái niệm cần nắm trước khi viết app Flutter
Học Dart cho người mới: 10 khái niệm cốt lõi kèm ví dụ code — biến, null safety, hàm, collection, class, async/await — để sẵn sàng viết app Flutter.
Có nên học Flutter 2026? Lộ trình khóa học lập trình Flutter
Flutter có đáng học năm 2026 không? Phân tích ưu nhược điểm, ai nên học và lộ trình 5 giai đoạn từ con số 0 đến app thật trên Store.
Flutter có thể áp dụng trong các ngành công nghiệp khác nhau không
Flutter có thể áp dụng trong các ngành công nghiệp khác nhau không
