Hướng Nghiệp Dữ Liệu

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

Đăng bởi Admin
Widget trong Flutter là gì? Hiểu StatelessWidget và StatefulWidget

Widget trong Flutter — minh hoạ 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 StatelessWidgetStatefulWidget, 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]

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":

  1. Không có CSS. Muốn đổi màu chữ? Dùng Text với style: TextStyle(color: ...). Muốn bo góc? Dùng ClipRRect hoặc BoxDecoration. Muốn canh giữa? Dùng Center.
  2. 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.
  3. 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:

  1. 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ị.
  2. 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.
  3. 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.
  • const constructor khi mọi tham số có thể là hằng — giúp tăng hiệu năng.
  • context là "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ó

BuildContextvị 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 context vào biến rồi dùng sau khi hàm async kết thúc. Trước khi dùng, hãy kiểm tra if (!mounted) return; (trong State) hoặc if (!context.mounted) return;.
  • Không tạo widget cần context ở ngoài build — hãy tạo bên trong build hoặ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:

  1. 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òn State được giữ lại giữa các lần build.
  2. Đọc tham số của widget qua widget. — ví dụ widget.buoc.
  3. Muốn giao diện đổi, phải gọi setState. Sửa biến mà không gọi setState thì giá trị đổi nhưng màn hình không cập nhật.
  4. 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]

ONhapTuKhoaKetQuaTimKiem đề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

  1. Dùng const ở mọi nơi có thể. const Text('Xin chào') không bao giờ cần build lại.
  2. Tách widget nhỏ thay vì viết một hàm build dài 300 dòng. Widget nhỏ thì chỉ widget đó build lại.
  3. Ưu tiên StatelessWidget khi không cần state; bọc phần thay đổi trong widget riêng.
  4. Dùng key đúng chỗ. Với ListView có 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:

  1. soDu vẫn là dữ liệu vào bất biến — truy cập qua widget.soDu, không copy sang field của State. Lỗi phổ biến là copy nó vào initState rồi dữ liệu không cập nhật khi cha truyền giá trị mới.
  2. _anstate thật sự — chỉ nó mới nằm trong State và mới cần setState.

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 TextFieldListView; 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 setState là cần thiết và khi nào không.
  • Kể được 6 hàm trong vòng đời State và việc nên làm ở từng hàm.
  • Luôn dispose controller, animation, subscription.
  • Biết vì sao phải kiểm tra mounted trước khi setState sau await.
  • Biết dùng const, tách widget nhỏ và dùng ValueKey khi 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:

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:

  1. 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.
  2. Ô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.
  3. 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

  1. Có nên học Flutter năm 2026? Lộ trình khóa học lập trình Flutter
  2. Dart cho người mới: 10 khái niệm cần nắm trước khi viết app Flutter
  3. Widget trong Flutter là gì? Hiểu StatelessWidget và StatefulWidget (bài này)
  4. 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.