StreamBuilder là widget Flutter tự động xây dựng lại giao diện khi nhận được dữ liệu mới từ luồng bất đồng bộ. Không giống như FutureBuilder, hoạt động với một kết quả duy nhất, StreamBuilder hỗ trợ cập nhật UI liên tục trong suốt vòng đời của Stream. Theo tài liệu chính thức của Flutter (2026), StreamBuilder được sử dụng trong các ứng dụng thời gian thực: chat, bảng tin, giám sát cảm biến và bảng giá tài chính. Đây là công cụ chính của lập trình phản ứng, nơi UI phản ánh trạng thái dữ liệu mà không cần gọi setState thủ công.
Những điểm chính
StreamBuilder là widget từ gói Flutter SDK, đăng ký vào một Stream và xây dựng lại phần tử con của nó sau mỗi sự kiện luồng mới. StreamBuilder chấp nhận một đối tượng Stream và trả về widget dựa trên snapshot mới nhất nhận được từ luồng.
Trong kiến trúc Flutter, StreamBuilder thuộc nhóm widget Builder tách biệt việc xây dựng UI khỏi trạng thái dữ liệu. Không giống như StatefulWidget, nơi thay đổi trạng thái yêu cầu gọi setState rõ ràng, StreamBuilder phản ứng với các sự kiện bất đồng bộ một cách tự động, đơn giản hóa mã và giảm nguy cơ lỗi đồng bộ hóa.
Không giống như FutureBuilder, xử lý một giá trị bất đồng bộ duy nhất, StreamBuilder được thiết kế cho các luồng dữ liệu liên tục. FutureBuilder kết thúc sau khi nhận được kết quả đầu tiên, trong khi StreamBuilder tiếp tục lắng nghe luồng và cập nhật UI sau mỗi sự kiện mới.
StreamBuilder được sử dụng trong mọi tình huống dữ liệu đến liên tục: kết nối WebSocket, callback cảm biến, thông báo Firebase, hàng đợi sự kiện Bluetooth và phát trạng thái ứng dụng qua BLoC. Theo phân tích các dự án Flutter trên GitHub (2025), StreamBuilder nằm trong ba widget Builder được sử dụng nhiều nhất cùng với FutureBuilder và LayoutBuilder.
Kết luận: sử dụng StreamBuilder ở mọi nơi UI cần phản ánh dữ liệu thay đổi liên tục, tránh quản lý trạng thái thủ công qua StatefulWidget.
StreamBuilder đăng ký vào một Stream tại thời điểm xây dựng và hủy đăng ký khi widget bị hủy. Mỗi khi Stream phát ra một sự kiện, StreamBuilder nhận được AsyncSnapshot mới và gọi hàm builder để xây dựng lại UI.
Quy trình gồm ba bước. Đầu tiên: StreamBuilder tạo đăng ký vào Stream được truyền qua phương thức stream.listen. Thứ hai: tại mỗi sự kiện, StreamBuilder cập nhật AsyncSnapshot nội bộ và đánh dấu widget là cần xây dựng lại. Thứ ba: framework gọi hàm builder với snapshot mới và UI hiển thị dữ liệu hiện tại.
Quan trọng: StreamBuilder sử dụng StreamSubscription nội bộ. Nếu Stream được truyền trực tiếp, StreamBuilder đăng ký một lần khi khởi tạo. Nếu Stream thay đổi (ví dụ: khi parent xây dựng lại), StreamBuilder hủy đăng ký khỏi luồng cũ và đăng ký vào luồng mới. Hành vi này được kiểm soát bởi các tham số initialData và buildWhen, cho phép tối ưu hóa số lần xây dựng lại.
Kết luận: hiểu vòng đời đăng ký là nền tảng để sử dụng StreamBuilder đúng cách. Quản lý luồng sai dẫn đến rò rỉ bộ nhớ hoặc dữ liệu cũ trong UI.
Thuộc tính connectionState của đối tượng AsyncSnapshot xác định StreamBuilder đang ở giai đoạn xử lý luồng nào. Có bốn trạng thái: none, waiting, active, done.
None là trạng thái ban đầu khi Stream chưa bắt đầu truyền dữ liệu. Ở trạng thái này, snapshot.connectionState bằng ConnectionState.none và snapshot.data là null. Thông thường, trình giữ chỗ hoặc chỉ báo chờ được hiển thị ở trạng thái này. Nếu Stream không cung cấp dữ liệu ban đầu, StreamBuilder bắt đầu ở trạng thái này.
Waiting là trạng thái chờ dữ liệu từ luồng bất đồng bộ. Stream đang hoạt động nhưng dữ liệu chưa đến. Trạng thái này xảy ra, ví dụ, khi tải dữ liệu từ mạng hoặc mở kết nối dài hạn. Ở trạng thái này, thông thường hiển thị CircularProgressIndicator hoặc bộ tải khung xương.
Active — luồng đang phát dữ liệu và UI hiển thị thông tin hiện tại. Ở trạng thái này, snapshot.hasData là true và snapshot.data chứa giá trị mới nhất từ luồng. Nếu luồng là Broadcast Stream, trạng thái active có thể cùng tồn tại với việc chờ dữ liệu mới.
Done — luồng đã kết thúc, sẽ không có dữ liệu mới. Snapshot.data chứa giá trị cuối cùng được truyền trước khi đóng luồng. Nếu luồng kết thúc thành công, snapshot.hasError là false. Trạng thái này được dùng để hiển thị kết quả cuối cùng: thông báo như “Đã tải xong” hoặc chuyển sang màn hình tiếp theo.
Kết luận: khi xây dựng UI qua StreamBuilder, cần xử lý cả bốn trạng thái để giao diện hiển thị chính xác tải, dữ liệu, lỗi và kết thúc.
StreamController là lớp từ gói dart:async tạo và quản lý Stream. StreamController cho phép thêm dữ liệu, xử lý lỗi và đóng luồng, kiểm soát vòng đời của nó.
StreamController có hai loại: single-subscription (một người đăng ký) và broadcast (nhiều người đăng ký). Bộ điều khiển single-subscription chỉ chấp nhận một người nghe tại một thời điểm — đăng ký lần thứ hai sẽ gây ra ngoại lệ. Bộ điều khiển broadcast cho phép nhiều StreamBuilder cùng lắng nghe một luồng, hữu ích cho BLoC và trạng thái ứng dụng dùng chung.
Khi tạo StreamController qua StreamController<T>.broadcast(), dữ liệu được thêm trước lần đăng ký đầu tiên không được phát lại cho người đăng ký mới. Để lấy giá trị mới nhất khi kết nối, sử dụng BehaviourSubject từ gói rxdart, lưu trữ đệm sự kiện cuối cùng.
Sau khi hoàn thành công việc với bộ điều khiển, cần gọi controller.close(). Không gọi close dẫn đến rò rỉ tài nguyên: luồng vẫn mở, người đăng ký vẫn nằm trong bộ nhớ và GC không giải phóng các đối tượng liên quan.
Kết luận: sử dụng StreamController với quản lý vòng đời rõ ràng. Cho luồng single-subscription, dùng bộ điều khiển tiêu chuẩn; cho trạng thái dùng chung, dùng bộ điều khiển broadcast hoặc BehaviourSubject.
Ví dụ 1 minh họa bộ đếm lùi sử dụng StreamController và StreamBuilder.
import 'dart:async';
class TimerWidget extends StatefulWidget {
const TimerWidget({super.key});
final StreamController<int> controller = StreamController<int>();
void startTimer() {
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
controller.sink.add(count++);
if (count > 10) {
controller.close();
timer.cancel();
}
});
}
}
Trong ví dụ, một bộ điều khiển được tạo để sinh số từ 0 đến 10 với khoảng cách 1 giây. Sau khi đạt 10, close được gọi và luồng kết thúc. StreamBuilder, đăng ký vào luồng của bộ điều khiển này, sẽ hiển thị từng giá trị mới.
Ví dụ 2 — sử dụng StreamBuilder với Broadcast Stream để hiển thị dữ liệu từ nhiều nguồn.
final StreamController<String> broadcastController =
StreamController<String>.broadcast();
StreamBuilder<String>(
stream: broadcastController.stream,
initialData: 'Waiting for data...',
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Text('Lỗi: ${snapshot.error}');
}
return Text('Dữ liệu: ${snapshot.data}');
},
)
Ví dụ thứ hai minh họa xử lý tất cả trạng thái: initialData cho hiển thị ban đầu, waiting cho chỉ báo tải, hasError cho lỗi và data cho kết quả thành công. Mẫu này là tiêu chuẩn cho mã sản xuất với StreamBuilder.
Kết luận: sử dụng initialData để tránh màn hình trống lúc đầu và luôn xử lý hasError để hiển thị lỗi chính xác cho người dùng.
Lỗi 1: tạo Stream mới mỗi lần parent xây dựng lại. Nếu Stream được truyền qua biểu thức tạo đối tượng mới mỗi lần xây dựng, StreamBuilder hủy đăng ký khỏi luồng cũ và đăng ký vào luồng mới, gây vòng lặp xây dựng lại vô hạn. Giải pháp: sử dụng biến remembered hoặc StatefulWidget với Stream cố định.
Lỗi 2: thiếu xử lý lỗi. Stream có thể phát lỗi qua controller.sink.addError, và nếu builder không kiểm tra snapshot.hasError, người dùng thấy màn hình trống hoặc tải vô hạn. Giải pháp: luôn kiểm tra hasError và hiển thị thông báo rõ ràng.
Lỗi 3: rò rỉ bộ nhớ do StreamController không được đóng. Nếu bộ điều khiển không được đóng trong dispose, luồng tiếp tục tồn tại và GC không giải phóng bộ nhớ. Giải pháp: gọi controller.close() trong dispose và lắng nghe sự kiện done cho các hành động kết thúc.
Lỗi 4: sử dụng StreamBuilder với hàm builder chậm. Vì builder được gọi tại mỗi sự kiện luồng, tính toán nặng bên trong nó gây giảm khung hình. Giải pháp: di chuyển tính toán sang isolate riêng hoặc sử dụng Stream.map để biến đổi dữ liệu.
Kết luận: StreamBuilder là công cụ mạnh nhưng đòi hỏi cao. Giám sát vòng đời Stream, xử lý lỗi và tránh thao tác nặng trong builder.
Câu hỏi thường gặp
FutureBuilder được thiết kế cho một kết quả bất đồng bộ duy nhất: nó đăng ký vào Future, nhận một giá trị và kết thúc. StreamBuilder đăng ký vào Stream, có thể phát nhiều giá trị theo thời gian và xây dựng lại UI sau mỗi sự kiện mới.
AsyncSnapshot là đối tượng bất biến chứa trạng thái đăng ký hiện tại (connectionState), giá trị cuối cùng nhận được (data) và đối tượng lỗi (error) nếu luồng phát ra ngoại lệ.
Lỗi được xử lý qua thuộc tính snapshot.hasError và snapshot.error trong hàm builder. Nếu luồng phát lỗi qua sink.addError, AsyncSnapshot nhận lỗi và builder cần hiển thị thông báo phù hợp hoặc UI dự phòng.
Có, nếu Stream là broadcast (được tạo qua StreamController.broadcast). Stream single-subscription chỉ cho phép một người đăng ký. Để chia sẻ một luồng giữa nhiều widget, sử dụng bộ điều khiển broadcast hoặc gói rxdart với BehaviourSubject.
Sử dụng tham số buildWhen để lọc sự kiện nào nên kích hoạt xây dựng lại UI. Cũng áp dụng Stream.transformer hoặc Stream.where để lọc dữ liệu trước khi truyền cho StreamBuilder.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm