dart에서는 Future와 Stream으로 비동기 프로그래밍을 구현할 수 있고, 이번 글에서는 Stream에 대해 알아보겠습니다.
Stream의 사전적인 의미는 흐르다는 뜻이며, 프로그래밍에서는 데이터의 흐름을 의미합니다. Stream을 사용한다면 지속적으로 데이터의 흐름을 관찰할 수 있습니다.
StreamBasic은 Stream과 GetX 라이브러리를 활용하여 숫자를 누적 합산하는 위젯입니다.
실행로직
1. sum 변수를 RxInt타입으로 선언합니다.
2. Stream 타입을 리턴하는 countStream함수를 이용해서 0.3초마다 데이터를 전달해서 sum 변수에 값을 더해줍니다.
3. Obx를 이용해서 sum의 값이 변경되는 것을 캐치하고 화면에 출력합니다.
RxInt와 Obx는 GetX 라이브러리를 활용한 것이고, GetX에 대한 자세한 내용은 아래 링크를 참조해주세요.
https://pub.dev/packages/get
Stream 사용 방법의 형태는 Future와 흡사합니다. Future에서는 Future, async, await을 사용했지만 Stream에서는 Stream, async, yield를 사용하고 있습니다.
함수의 앞부분에서 return 타입을 Stream으로 설정하고, 중괄호를 열기 전에 async를 붙여줍니다. 그리고 yield {value}를 호출함으로써 데이터를 전달할 수 있습니다.
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
await Future.delayed(const Duration(milliseconds: 300));
yield i;
}
}
Stream은 builder패턴으로 take, takeWhile, where, listen 등의 함수들을 호출할 수 있습니다. 사용방법은 코드에 주석으로 추가했습니다.
class StreamBasic extends StatefulWidget {
const StreamBasic({Key? key}) : super(key: key);
_StreamBasicState createState() => _StreamBasicState();
}
class _StreamBasicState extends State<StreamBasic> {
RxInt sum = 0.obs;
bool _pageDispose = false;
late Stream<int> _stream;
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
///초기화
_reset(),
///시작
_start(),
],
),
const SizedBox(height: 10),
Obx(() =>
Text("value : ${sum.value}", style: const TextStyle(fontSize: 20))),
],
);
}
///초기화
Widget _reset() {
return ElevatedButton(
onPressed: () => sum.value = 0,
child: const Text("초기화", style: TextStyle(fontSize: 20)));
}
void dispose() {
_pageDispose = true;
super.dispose();
}
Widget _start() {
return ElevatedButton(
onPressed: () async {
_stream = countStream(10);
///take를 활용하면 take에 입력된 숫자 만큼만 listen함수로 들어온다.
_stream.take(9).takeWhile((element) {
///takeWhile에서 false를 리턴하면 stream을 종료시킨다
if (element == 8) {
return false;
} else if (_pageDispose) {
return false;
} else {
return true;
}
}).where((event) {
///where에서 false를 return하면 stream의 listener는 계속 붙어있지만
///listen 함수로 진입하지는 않는다
return !(event > 6);
}).listen((v) => sum.value += v);
},
child: Container(
alignment: Alignment.center,
child: const Text("Stream basic", style: TextStyle(fontSize: 20))));
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
await Future.delayed(const Duration(milliseconds: 300));
yield i;
}
}
}
-------------print----------------------
I/flutter (21956): sum : 1
I/flutter (21956): sum : 3
I/flutter (21956): sum : 6
I/flutter (21956): sum : 10
I/flutter (21956): sum : 15
I/flutter (21956): sum : 21
StreamController와 StreamBuilder를 사용해서 숫자를 증감시키는 StreamBuilderTest위젯을 살펴보겠습니다. StreamBasic 위젯에서는 GetX를 이용해서 sum 변수의 변화를 관찰했다면, 이번에는 StreamController를 이용해서 count 변수의 변화를 관찰하고 있습니다.
구현 로직은 다음과 같습니다.
1. StreamController 생성
2. StreamController 의 listner 및 비동기 작업을 실행할 수 있고, Widget을 반환하는 StreamBuilder 생성
3. 버튼을 클릭해서 streamController의 sink.add 함수를 호출해서 전역변수로 선언한 count의 값을 변화시킬 것
StreamBuilder의 부모인 StreamBuilderBase는 StatefulWidget을 상속받았기 때문에 widget를 반환할 수 있고, 이때 반환하는 위젯은 변경된 count값을 받을 수 있습니다.
///StreamController와 StreamBuilder를 사용한 count 예제
class StreamBuilderTest extends StatefulWidget {
const StreamBuilderTest({Key? key}) : super(key: key);
_StreamBuilderTestState createState() => _StreamBuilderTestState();
}
class _StreamBuilderTestState extends State<StreamBuilderTest> {
final StreamController<int> _controller = StreamController();
int count = 0;
Widget build(BuildContext context) {
return Column(
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
_plusButton(),
_minusButton(),
]),
_builder(),
],
);
}
Widget _plusButton() {
return ElevatedButton(
onPressed: () {
_controller.sink.add(++count);
},
child: Text("plus"));
}
Widget _minusButton() {
return ElevatedButton(
onPressed: () {
_controller.sink.add(--count);
},
child: Text("minus"));
}
StreamBuilder _builder() {
return StreamBuilder(
stream: _controller.stream,
builder: (context, snapshot) {
return Text("StreamBuilder : $count",
style: const TextStyle(fontSize: 20));
},
);
}
void dispose() {
_controller.close();
super.dispose();
}
}