Stream là gì?

Trong Flutter, Stream là một luồng dữ liệu tuần tự liên tục được truyền dần từng phần (chunk) tới người nhận. Stream thường được sử dụng để xử lý các tác vụ bất đồng bộ và cung cấp cơ chế gửi và nhận dữ liệu liên tục.
Stream được coi là một dạng tiến trình (process) dòng, trong đó dữ liệu được sản sinh và truyền đi theo từng phần. Người nhận (listener) có thể đăng ký (subscribe) để nhận các phần dữ liệu khi chúng được sản sinh. Để dễ hình dung hơn bạn có thể tượng stream như một cái ống dẫn đi vô từ đầu vào và sẽ đi ra tại đầu ra.
Trong flutter stream có thể truyền đi bất cứ kiểu dữ liệu nào bởi vì stream là một loại đối tượng kiểu generic.
Bạn có thể tìm hiểu kiểu dữ liệu generic tại đây: https://vncometech.com/bai-viet/xay-dung-generic-trong-dart
Phân loại các loại Stream trong Flutter
Trong flutter chúng ta ba loại stream chính đó là:

1 Single-Subscription Stream (Stream có một Subscription): Đây là loại Stream mà chỉ một listener (người lắng nghe) có thể đăng ký để nhận dữ liệu từ Stream. Một khi listener đã nhận được tất cả các phần dữ liệu hoặc Stream kết thúc, listener sẽ bị hủy đăng ký và không thể lắng nghe tiếp. Nếu bạn đăng ký lắng nghe ở một nơi khác hoặc muốn lắng nghe lần 2 thì sẽ bị lỗi.
2 Broadcast Stream (Stream truyền phát): Đây là loại Stream mà cho phép nhiều listener đăng ký để nhận dữ liệu từ Stream. Mỗi listener sẽ nhận được tất cả các phần dữ liệu từ Stream, ngay cả khi đã có listener khác đăng ký trước đó. Điều này cho phép truyền dữ liệu đến nhiều nơi cùng một lúc.
3 On-demand Stream (Stream theo yêu cầu) Đây là loại Stream mà chỉ phát dữ liệu khi có người lắng nghe đăng ký. Khi không có listener nào đăng ký, Stream sẽ không phát dữ liệu. Khi có listener đăng ký, Stream sẽ bắt đầu phát dữ liệu từ đầu hoặc từ vị trí hiện tại (tùy thuộc vào cách Stream được cài đặt).
Phần tiếp theo chúng ta đi tìm hiểu cụ thể vào từng stream
Cú pháp khai báo một Stream
Thông thường chúng ta sẽ tạo stream thông qua controller thay vì khai báo trực tiếp stream như thế này.
Stream streamName = Stream.streamMethod;
Trong đó:
- Stream : Là từ khóa khai báo bạn đang tạo một Stream.
- DataType : Là kiểm dữ liệu mà bạn muốn gửi trong Stream.
- streamName : Là tên của stream bạn có thể đặt tùy ý.
- streamMethod bạn có thể dùng các loại method stream cung cấp sẵn như: periodic(), fromIterable(), fromFuture(), take(), ...
Ví dụ: Dựng một stream sử dụng method periodic
Stream myStream = Stream.periodic(Duration(seconds: 1), (count) => count + 1).take(5);
Trong ví dụ này, chúng ta sử dụng Stream.periodic() để tạo một Stream định kỳ phát các sự kiện từ 0 trở đi. Hàm (count) => count + 1 được sử dụng để biến đổi giá trị của sự kiện từ số thứ tự của sự kiện thành giá trị thực tế của sự kiện (từ 1 đến 5). .take(5) được sử dụng để giới hạn số lượng sự kiện chỉ định trong Stream (trong trường hợp này là 5).
Lưu ý rằng Stream tạo ra bởi Stream.periodic() là một Stream vô hạn và nó sẽ không dừng lại bạn không hủy hoặc giới hạn nó, vì vậy bạn có thể sử dụng phương thức take(n) để giới hạn số lượng sự kiện trong Stream nếu cần thiết.
Ví dụ: Sử dụng Stream sử dụng method fromIterable()
List dataList = [1, 2, 3, 4, 5];
Stream myStream = Stream.fromIterable(dataList);
Trong ví dụ này, chúng ta khai báo một danh sách dataList chứa các số từ 1 đến 5. Sau đó, chúng ta sử dụng Stream.fromIterable() để tạo một Stream từ danh sách này và gán cho myStream. Khi lắng nghe (listen) myStream, các sự kiện trong danh sách sẽ được phát ra theo thứ tự tương ứng.
StreamController là gì, vai trò của nó trong Flutter?
Trước khi đi sâu vào tìm hiểu các loại stream mà mình liệt kê bên dưới chúng ta sẽ tìm hiểu về controller của những stream này trước. StreamController có vai trò quan trọng trong việc tạo và điều khiển Stream. StreamController cho phép bạn tạo một Stream và thực hiện các thao tác liên quan đến Stream như phát dữ liệu vào Stream, đóng Stream và quản lý các lắng nghe (listener) của Stream. Vai trò của nó có thể được liệt kê như sau:
1 Tạo Stream
StreamController cho phép bạn tạo một Stream mới thông qua thuộc tính stream. Bằng cách sử dụng StreamController, bạn có thể tạo một Single Subscription Stream hoặc Broadcast Stream để phát các sự kiện.
2 Thêm dữ liệu vào Stream
Bằng cách sử dụng phương thức add(), bạn có thể phát dữ liệu vào Stream thông qua StreamController. Mỗi lần gọi add(), dữ liệu được gửi đi và các lắng nghe (listener) của Stream sẽ nhận được thông báo về sự kiện mới.
3 Đóng stream
Bằng cách gọi phương thức close(), bạn có thể đóng Stream thông qua StreamController. Khi Stream được đóng, không thể phát thêm dữ liệu vào Stream và các lắng nghe (listener) cũng sẽ kết thúc.
4 Quản lý đăng ký lắng nghe các sự kiện từ Stream
StreamController giúp bạn quản lý các lắng nghe đăng ký để lắng nghe sự kiện từ Stream. Khi bạn gọi phương thức listen() trên Stream, StreamController trả về một đối tượng StreamSubscription, cho phép bạn hủy đăng ký lắng nghe bằng cách gọi phương thức cancel().
Các phương thức của stream controller sẽ được gọi trong phần tiếp theo của bài viết.
Cách sử dụng Single subscription stream trong Flutter
Để có thể sử dụng Single Subscription Stream các bạn thực hiện như sau:
1 Chúng ta khai báo StreamController đồng thời qua đây chúng ta cũng đã chỉ định đây là Single subscription stream
StreamController controller = StreamController();
T là kiểu dữ liệu mà bạn muốn truyền vào trong Stream bạn nhé: int, String, Student, ...
2 Tạo ra một stream từ controller của nó
Stream myStream = controller.stream;
3 Đăng ký lắng nghe sự kiện từ stream
StreamSubscription subscription = myStream.listen((data) {
// Xử lý dữ liệu nhận được từ Stream
});
4 Hủy lắng nghe các sự kiện từ stream
Khi không cần thiết lắng nghe nữa, hủy đăng ký lắng nghe bằng cách gọi phương thức cancel() trên đối tượng StreamSubscription. Bạn lưu ý phương thức này chúng ta chỉ hủy lắng nghe thôi chứ không phải hủy Stream
subscription.cancel();
5 Cách mà chúng ta phát dữ liệu trong Stream
Để phát dữ liệu vào Single Subscription Stream, bạn có thể sử dụng phương thức add() trên đối tượng StreamController
controller.add(data);
6 Hủy hoàn toàn Stream
Chúng ta sử dụng phương thức close() để hủy stream, lúc này stream đã bị đóng, chúng ta không thể thêm bất kỳ dữ liệu nào vào đây nữa
controller.close();
Ví dụ
Trong ví dụ này, chúng ta tạo một Single Subscription Stream kiểu int. Sau đó, đăng ký lắng nghe vào Stream và in ra màn hình các sự kiện nhận được. Sau khi phát dữ liệu vào Stream và hoàn thành lắng nghe, chúng ta hủy đăng ký lắng nghe và đóng Stream.
import 'dart:async';
void main() {
StreamController controller = StreamController();
Stream myStream = controller.stream;
StreamSubscription subscription = myStream.listen((data) {
print('Received: $data');
});
controller.add(1);
controller.add(2);
controller.add(3);
subscription.cancel();
controller.close();
}
Kết quả chương trình sẽ in ra màn hình console như sau:
Received: 1
Received: 2
Received: 3
Lưu ý
Khi bạn sử dụng một Single Subscription Stream và đăng ký nhiều lắng nghe (listener), sẽ chỉ có lắng nghe đầu tiên được gọi và nhận được các sự kiện từ Stream. Các lắng nghe sau đó sẽ không nhận được bất kỳ sự kiện nào từ Stream.
Cách sử dụng Broadcast Stream

Broadcast Stream cho phép nhiều lắng nghe cùng nhận các sự kiện từ Stream. Khi một sự kiện được phát vào Stream, tất cả các lắng nghe đã đăng ký sẽ nhận được sự kiện đó. Điều này khác với Single Subscription Stream, nơi chỉ có lắng nghe đầu tiên được gọi.
Để xấy dựng một Broadcast stream các bạn thực hiện như sau
1 Tạo một StreamController và thông qua đó chúng ta cũng chỉ định loại Stream là broadcast
StreamController controller = StreamController.broadcast();
T là kiểu dự liệu mà bạn muốn truyền trong stream nhé
2 Tạo Stream từ controller
Stream myStream = controller.stream;
3 Đăng ký lắng nghe các sự kiện từ Stream
myStream.listen((data) {
// Xử lý sự kiện
});
4 Thêm các giá trị vào stream
controller.add(data);
5 Hủy stream
controller.close();
Ví dụ
Trong ví dụ này, chúng ta tạo một Broadcast Stream bằng cách thiết lập thuộc tính broadcast của StreamController là true. Sau đó, chúng ta đăng ký hai lắng nghe (listener) vào Broadcast Stream và cả hai lắng nghe đều nhận được các sự kiện từ Stream khi chúng ta phát các số vào Stream.
import 'dart:async';
void main() {
StreamController controller = StreamController.broadcast();
Stream myStream = controller.stream;
myStream.listen((data) {
print('Listener 1: $data');
});
myStream.listen((data) {
print('Listener 2: $data');
});
controller.add(1);
controller.add(2);
controller.add(3);
controller.close();
}
Kết quả sẽ là
Listener 1: 1
Listener 2: 1
Listener 1: 2
Listener 2: 2
Listener 1: 3
Listener 2: 3
Cách sử dụng On Demand Stream
On-demand Stream là một loại Stream trong Flutter được gọi khi có yêu cầu lắng nghe. Nó không phát ra sự kiện tự động như các loại Stream khác, mà chỉ phát sự kiện khi có lắng nghe đăng ký.
Để tạo một On-demand Stream, bạn cần sử dụng lớp StreamController và đặt thuộc tính sync thành false khi khởi tạo StreamController. Điều này sẽ tạo ra một Stream không đồng bộ (asynchronous) được gọi khi có lắng nghe đăng ký.
Ví dụ cách sử dụng
Trong ví dụ này, chúng ta tạo một StreamController với thuộc tính sync được đặt thành false, tạo ra một On-demand Stream. Khi có lắng nghe đăng ký vào Stream thông qua phương thức listen(), nó sẽ được kích hoạt và nhận các sự kiện từ Stream.
import 'dart:async';
void main() {
StreamController controller = StreamController.broadcast(sync: false);
Stream myStream = controller.stream;
// Đăng ký lắng nghe vào Stream khi có yêu cầu
myStream.listen((data) {
print('Received: $data');
});
// Phát sự kiện vào Stream
controller.add(1);
controller.add(2);
controller.add(3);
controller.close();
}
Kết quả sẽ là:
Received: 1
Received: 2
Received: 3
Quản lý việc lắng nghe và hủy lắng nghe với StreamSubcription
StreamSubscription trong Flutter được sử dụng để đăng ký và hủy đăng ký lắng nghe sự kiện từ một Stream. Khi bạn đăng ký lắng nghe một Stream, phương thức listen() trả về một StreamSubscription mà bạn có thể sử dụng để quản lý việc lắng nghe và hủy đăng ký khi cần thiết.
Cách chúng đăng ký
StreamSubscription subscription = myStream.listen((data) {
// Xử lý sự kiện
});
Để hủy lắng nghe sự kiện từ Stream
Để hủy đăng ký lắng nghe, gọi phương thức cancel() trên StreamSubscription
subscription.cancel();
Khi gọi cancel(), lắng nghe sẽ dừng và không nhận được thêm sự kiện từ Stream.
Ví dụ
Trong ví dụ này, chúng ta tạo một Stream từ hàm countStream(), tạo ra các số từ 1 đến 10 mỗi giây. Chúng ta đăng ký lắng nghe vào Stream và in ra giá trị nhận được. Sau 5 giây, chúng ta hủy đăng ký lắng nghe bằng cách gọi cancel() trên StreamSubscription.
import 'dart:async';
void main() {
Stream myStream = countStream();
StreamSubscription subscription = myStream.listen((data) {
print('Received: $data');
});
// Hủy đăng ký sau 5 giây
Timer(Duration(seconds: 5), () {
subscription.cancel();
print('Cancelled subscription');
});
}
Stream countStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
Kết quả sẽ in ra các số từ 1 đến 5 và sau đó thông báo "Cancelled subscription", chỉ ra rằng việc lắng nghe đã được hủy đăng ký.
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Cancelled subscription

Sự khác biệt khi sử dụng add() và sink.add()
Trong Flutter, thuộc tính sink được sử dụng khi bạn muốn gửi dữ liệu vào một Stream thông qua một đối tượng Sink độc lập. Sink cung cấp các phương thức để gửi dữ liệu, thông báo lỗi và kết thúc Stream. Thuộc tính sink có sẵn trên các lớp như StreamController, StreamSink và BehaviorSubject.
1 Sự khác biệt giữa việc add dữ liệu thông qua sink
StreamController controller = StreamController();
StreamSink sink = controller.sink;
// Thêm dữ liệu vào stream
controller.add(1);
sink.add(1);
Sự khác biệt chính giữa hai cách này nằm ở mức độ truy cập và quản lý dữ liệu. Khi sử dụng phương thức add() trực tiếp trên StreamController, bạn có quyền trực tiếp gửi dữ liệu vào Stream mà không cần thông qua trung gian.
Khi sử dụng thuộc tính sink của StreamController, bạn có một đối tượng Sink độc lập để gửi dữ liệu vào Stream. Thuộc tính sink cung cấp các phương thức bổ sung như add(), addError(), và close() để quản lý quá trình gửi dữ liệu và kết thúc Stream.
Một lợi ích của việc sử dụng thuộc tính sink là bạn có thể truyền đối tượng Sink cho các thành phần khác trong ứng dụng mà không cần tiếp cận trực tiếp đến StreamController. Điều này có thể hữu ích trong việc tách biệt phần logic của việc gửi dữ liệu và phần logic của việc đăng ký lắng nghe vào Stream.
Tóm lại, cả hai cách sử dụng đều cho phép gửi dữ liệu vào Stream. Sử dụng trực tiếp phương thức add() trên StreamController là cách đơn giản và trực tiếp, trong khi sử dụng thuộc tính sink của StreamController cung cấp quyền kiểm soát tốt hơn với các phương thức bổ sung để quản lý quá trình gửi dữ liệu vào Stream.
2 Ví dụ gửi dữ liệu qua Sink
import 'dart:async';
void main() {
StreamController controller = StreamController();
Stream myStream = controller.stream;
StreamSink sink = controller.sink;
// Gửi dữ liệu vào Stream bằng phương thức add() trên Sink
sink.add(1);
sink.add(2);
sink.add(3);
// Đăng ký lắng nghe vào Stream để nhận dữ liệu
myStream.listen((data) {
print('Received: $data');
});
controller.close();
}
Kết quả sẽ là
Received: 1
Received: 2
Received: 3
Cách sử dụng Widget StreamBuilder
Trong Flutter, StreamBuilder được sử dụng để xây dựng giao diện người dùng động dựa trên dữ liệu từ một Stream. Nó cho phép bạn lắng nghe và cập nhật giao diện người dùng mỗi khi dữ liệu trong Stream thay đổi.
Chúng ta thực hiện theo cú pháp sau:
StreamBuilder(
stream: myStream,
initialData: initialData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
// Xây dựng giao diện người dùng dựa trên dữ liệu snapshot
// snapshot.data chứa dữ liệu mới nhất từ Stream
// snapshot.connectionState chứa trạng thái hiện tại của Stream
// snapshot.error chứa lỗi (nếu có)
},
)
Trong đó:
- stream : Đây là stream mà bạn muốn lắng nghe.
- initialData : Đây là giá trị ban đầu trước khi trong stream có dữ liệu
- builder : Đây là một hàm callback nhận dữ liệu từ Stream và xây dựng giao diện người dùng dựa trên dữ liệu đó. Hàm này được gọi lại mỗi khi dữ liệu trong Stream thay đổi.
Ví dụ
Dưới đây là một ví dụ sử dụng StreamBuilder để hiển thị danh sách số nguyên từ một Stream:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Stream _numberStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('StreamBuilder Example'),
),
body: Center(
child: StreamBuilder(
stream: _numberStream(),
initialData: 0,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Text('Number: ${snapshot.data}');
},
),
),
),
);
}
}
Trong ví dụ này, chúng ta tạo một StreamBuilder để lắng nghe một Stream _numberStream(). Mỗi giây, Stream sẽ phát ra một số nguyên từ 1 đến 5. Builder function được gọi lại và cập nhật giao diện người dùng để hiển thị số nguyên mới nhất từ Stream. Ban đầu, giá trị khởi tạo là 0. Kết quả là giao diện người dùng sẽ hiển thị số nguyên từ 1 đến 5 theo từng giây.
Ví dụ một chương trình đếm số sử dụng Stream
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
StreamController _countController = StreamController();
void startCounting() {
int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
count++;
_countController.sink.add(count);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Stream Example'),
),
body: Center(
child: StreamBuilder(
stream: _countController.stream,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Text('Count: ${snapshot.data}');
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: startCounting,
),
),
);
}
@override
void dispose() {
_countController.close();
}
}
