Bạn có một danh sách nhỏ trên màn hình Flutter, dùng ValueNotifier<List<String>> để lưu dữ liệu. Sau khi gọi notifier.value.add(...), log cho thấy list đã có phần tử mới nhưng giao diện chưa thay đổi. Khi một sự kiện khác làm màn hình build lại, phần tử bỗng xuất hiện.
Nguyên nhân thường nằm ở cách phát thông báo thay đổi. Bài này giải thích cơ chế đó và đưa một màn hình nhỏ để bạn tự kiểm tra. API được đối chiếu tài liệu Flutter ngày 23/10/2026. Ví dụ dùng thư viện Flutter tích hợp, không thêm package quản lý state; chưa chạy Flutter SDK hay kiểm thử thiết bị trong lượt biên tập.
Notifier theo dõi giá trị được gán
ValueNotifier phát thông báo khi giá trị mới được gán khác giá trị cũ theo toán tử ==. Nó không tự quan sát mọi thay đổi bên trong một object đang được giữ. Với list thông thường, sửa phần tử tại chỗ không tạo ra một lần gán giá trị khác để notifier xử lý.
Do đó, gọi value.add(...) rồi gán lại chính list ấy vào value cũng dễ không phát thông báo như mong muốn. Một cách rõ ràng cho state nhỏ là tạo list mới sau mỗi thao tác và tránh cho nơi khác sửa trực tiếp list đã công bố.
Phép so sánh này cũng có hệ quả với kiểu dữ liệu tự định nghĩa. Nếu bạn override ==, việc một instance mới có làm notifier phát thông báo hay không còn phụ thuộc hợp đồng equality của kiểu đó. Đừng chỉ nhìn thấy từ khóa new rồi kết luận chắc chắn có cập nhật UI.
Một màn hình có chủ sở hữu state rõ ràng
Bạn có thể đặt ví dụ dưới vào lib/main.dart của dự án Flutter thử đã được tạo. State tạo notifier một lần, gán list mới khi bấm nút và giải phóng tài nguyên khi màn hình bị gỡ khỏi cây widget.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: NotesPage()));
class NotesPage extends StatefulWidget {
const NotesPage({super.key});
@override
State<NotesPage> createState() => _NotesPageState();
}
class _NotesPageState extends State<NotesPage> {
final _notes = ValueNotifier<List<String>>(const []);
void _addNote() {
final next = 'Ghi chu ${_notes.value.length + 1}';
_notes.value = List<String>.unmodifiable([..._notes.value, next]);
}
@override
void dispose() {
_notes.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ValueNotifier demo')),
body: Padding(
padding: const EdgeInsets.all(16),
child: ValueListenableBuilder<List<String>>(
valueListenable: _notes,
child: const Text('Danh sach ghi chu'),
builder: (context, notes, child) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
child!,
Text(notes.isEmpty ? 'Chua co ghi chu' : notes.join(', ')),
FilledButton(onPressed: _addNote, child: const Text('Them')),
],
),
),
),
);
}
}
List.unmodifiable giúp ngăn thay đổi cấu trúc list đã công bố. Trong ví dụ, phần tử là chuỗi nên mô hình khá đơn giản. Nếu phần tử là object có trường mutable, bọc list không khiến các object bên trong trở thành bất biến; bạn vẫn cần quy tắc cập nhật cho từng phần tử.
Builder tự đăng ký listener, ownership vẫn do bạn quyết định
ValueListenableBuilder theo dõi ValueListenable và gọi builder khi có thông báo. Bạn không cần tự gọi setState thêm chỉ để đồng bộ lại cùng dữ liệu này. Tạo notifier ngay trong build sẽ làm vòng đời dữ liệu khó kiểm soát và có thể mất state khi widget cha build lại.
Ở ví dụ, màn hình sở hữu notifier nên màn hình gọi dispose. Nếu notifier được truyền vào từ một tầng khác, tầng tạo và sở hữu nó phải quyết định khi nào giải phóng; widget chỉ mượn không nên tùy tiện dispose một tài nguyên còn được nơi khác dùng.
Tham số child chứa phần giao diện không phụ thuộc danh sách. Việc truyền sẵn subtree có thể tránh tạo lại phần đó trong mỗi lần builder được gọi. Đây là cách tổ chức code theo tài liệu, không phải số đo tối ưu hiệu năng cho mọi màn hình.
Tự kiểm tra theo hành vi
Ghi lại kết quả flutter --version, chạy flutter analyze, rồi thử trên một thiết bị hoặc emulator của dự án. Ban đầu phải có trạng thái rỗng. Mỗi lần bấm nút, danh sách tăng đúng một mục mà không cần thao tác khác khiến màn hình build lại.
Để hiểu lỗi ban đầu, thử thay riêng phần cập nhật bằng mutation tại chỗ trên một list mutable trong dự án thử và quan sát listener. Sau đó khôi phục cách gán list mới. Đừng để phép thử sửa dữ liệu trực tiếp tồn tại trong code production rồi bù bằng một lần rebuild ngẫu nhiên ở màn hình cha.
Khi có widget test, kiểm tra trạng thái trước và sau một lần bấm, nhiều lần bấm và lúc màn hình được mở lại. Mục tiêu là chứng minh hợp đồng dữ liệu, không đếm số lần build rồi suy luận toàn bộ app đã nhanh hơn.
Khi state lớn dần
Nếu nhiều màn hình cùng chỉnh dữ liệu, có quy trình tải/lưu phức tạp hoặc nhiều trường cần cập nhật đồng thời, hãy đưa logic ra một lớp có trách nhiệm rõ hơn. Tài liệu gợi ý ChangeNotifier khi cần quản lý object mutable và phát thông báo chủ động; các kiến trúc state khác cũng cần được chọn theo yêu cầu thực tế.
Một notifier cho state nhỏ có thể rất dễ đọc. Điều cần giữ là quy tắc equality, cách công bố giá trị mới và chủ sở hữu tài nguyên được thể hiện nhất quán trong code.



