Flutter와 Firebase로 Android iOS 둘 다 만들기 13 Stream 사용해보기

3 분 소요

dart의 Stream에 대해 알아봅니다.

개요

대부분의 유명한 모듈들(fire_auth, fire_store등)은 스트림 형태로 데이터를 제공합니다.

대부분의 스트림 강의 처럼 스트림을 파헤치지는 않습니다.

개념 보다는 실무에서 필요한 상황에 필요한 만큼만 사용해봅니다.

참고: https://dart.dev/tutorials/language/streams

왜 써야할까?

데이터를 표시하는 방법은 여러가지입니다.

Stateful 위젯을 사용해서 initState에서 읽고 setState를 사용해서 화면에 반영하는 방법도 있지만 중간에 변경점이 있을 경우를 고려하면 타이머도 돌려야하고 복잡해집니다.

그런 상황에 StreamBuilder를 사용하면 데이터와 화면이 유기적으로 돌아가게 됩니다.

StreamBuilder eg)

return StreamBuilder<QuerySnapshot>(
      stream: Firestore.instance.collection('test').snapshots(),
      builder: (context, snapshot) {
        // snapshot

데이터가 변하는 즉시 snapshot에는 연결 상태(snapshot.connectstate)와 데이터가 들어오기 때문에 간단한 화면 코딩이 가능합니다.

위처럼 파이어스토어는 스트림을 제공하지만 다음 강의에서 진행할 sqflite의 경우 스트림을 제공하지 않습니다.(물론 랩핑한 모듈이 있긴합니다만.. 정확히 원하는 모듈은 아니었습니다.)

그래서 용도는 sqflite의 일정부분 갱신(fetch)입니다.

스트림형태로 만들기 위해 직접 테스트를 해봅니다.

지정시간 마다 화면 변경하기

스트림을 사용하기 이전에 구스타일로 구현해봅니다.

import 'dart:async';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeWidget()
    );
  }
}

class HomeWidget extends StatefulWidget {
  HomeWidget({Key key}) : super(key: key);

  @override
  _HomeWidgetState createState() => _HomeWidgetState();
}

class _HomeWidgetState extends State<HomeWidget> {
  int no = 0;

  @override
  void initState() { 
    super.initState();
    Timer.periodic(Duration(seconds: 1), (t) {
      setState(() => no = t.tick);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('test')),
      body: Text(no.toString())
    );
  }
}

Timer를 이용해서 1초마다 화면 변경을 해보았습니다.

웹환경에서 일반적으로 저렇게 많이들 처리했었죠~

Timer.periodic은 javascript의 setInterval과 같은 것입니다. setInterval보다 간격 조절도 간단하고 여러 측면에서 dart 압승입니다.

실제로 저는 위와 같이 대부분을 코딩하고 스토어 제출까지 벌써 두개나 해버렸습니다~ 뭐 일단 돌아가는 데는 지장이 없으니까요~ 뭐든 만들고 나야 다음 개선점이 보입니다.

기본 스트림 만들기

이번에는 주기적인 데이터를 받을 수 있는 기본적인 스트림으로 구현해봅니다.

Stream stream = Stream.periodic(Duration(seconds: 1), (t) => t).take(10);
int no = 0;

@override
void initState() { 
  super.initState();
  stream.listen((t) {
    setState(() => no = t);
  });

  // or   

  // final subscription = stream.listen(null);
  // subscription.onData((r) => setState(() => no = r));
  // subscription.onDone(() => print('done'));
}
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('test')),
    body: Text(no.toString())
  );
}

위의 타이머 방식에서 스트림 방식으로 변경해봤습니다.

(.take는 10번만 가져오겠다는 것입니다.)

구독 인스턴스를 만들어서 처리할 수도 있습니다.

final subscription = stream.listen(null);
subscription.onData((r) => setState(() => no = r));
subscription.onDone(() => print('done'));

중간에 구독을 끊을 때(subscription.cancel();)나 상태를 파악할 때 사용하면 됩니다.

이제 리슨으로 데이터를 취할 수 있게 되었습니다.

하지만 실무에서 사용할 이유가 없었던 것 같습니다.

우리가 원하는 것은 setState 없이 동작하게 하는 것이죠..

스트림빌더로 처리하기

스트림빌더로 처리한다는 얘기는 setState가 필요하지 않기 때문에 Stateless 위젯에서도 사용 가능하다는 것입니다.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('test')),
    body: StreamBuilder(
      stream: stream,
      builder: (context, snapshot) {
        print(snapshot);
        return Text(snapshot.data.toString());
      },
    )
  );
}

화면부 코드를 변경했는데 에러가 나게 됩니다.

이유는 데이터 리슨을 두 곳(initState, StreamBuilder)에서 하고 있기 때문입니다.

위에 stream.listen 부분을 지워야 동작합니다.

스트림은 한 군데서만 받을 수 있다는 것을 알 수 있습니다.

스트림 콘트롤러

위와 같은 일반 스트림은 파일 다운로드 같은 끝을 알 수 있는 상황에서 쓰입니다.

그래서 데이터 갱신 같은 역할을 할 때는 콘트롤러를 사용하는 것이 유리합니다.

StreamController ctrl = StreamController();
int no = 0;

@override
void initState() { 
  super.initState();
  Timer.periodic(Duration(seconds: 1), (t) => ctrl.add(t.tick) );
}

결과는 위 기본 스트림 예제와 같지만 프로그래머블하게 사용할 수 있음을 알수 있습니다.

콘트롤러에 이벤트를 추가할 수 있게 되었기 때문입니다.

실사용 예: 회원 10명 데이터를 배열로 가지고 있을 때 7번째 회원의 정보를 변경하면 crtl.add(person); 바로 갱신(fetch)가 되는 형태인 것이죠..

브로트캐스트

broadcast는 결국 방송이란 뜻입니다. tv, radio처럼 불특정 다수한테 마구 쏘는 것입니다.

위의 예제는 리슨이 한 곳만 가능했지만 콘트롤러를 브로트캐스트 형태로 선언하면 여러곳에서 사용이 가능합니다.

StreamController ctrl = StreamController.broadcast();
int no = 0;

@override
void initState() { 
  super.initState();
  Timer.periodic(Duration(seconds: 1), (t) => ctrl.add(t.tick) );
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('test')),
    // body: Text(no.toString())
    body: Column(
      children: <Widget>[
        StreamBuilder(
          stream: ctrl.stream,
          builder: (context, snapshot) {
            print(snapshot);
            return Text('a ' + snapshot.data.toString());
          },
        ),
        StreamBuilder(
          stream: ctrl.stream,
          builder: (context, snapshot) {
            print(snapshot);
            return Text('b ' + snapshot.data.toString());
          },
        ),
      ],
    ),
  );
}

간단히 해결이 됩니다.

상태 표시

listen에서는 단순 데이터만 오지만 StreamBuilder에서는 snapshot이라는 거추장 스러운게 오는 이유는 화면처리를 위한 이유가 큽니다.

상태에 따라 프로그레스나 다른 화면 전환이 필요한 경우입니다.

Timer.periodic(Duration(seconds: 1), (t) {      
  if (t.tick > 5) {
    ctrl.close();
    return;
  }
  ctrl.add(t.tick);
});

이렇게 해두고 화면의 print(snapshot) 을 확인해보면

flutter: AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.active, 1, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.active, 2, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.active, 3, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.active, 4, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.active, 5, null)
flutter: AsyncSnapshot<dynamic>(ConnectionState.done, 5, null)

상태가 확인됩니다.

if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator();
if (snapshot.connectionState == ConnectionState.done) return Text('done');
return Text('a ' + snapshot.data.toString());

이런식으로 사용하면 초기 로딩, 진행, 완료 형태를 시각적으로 표현할 수 있습니다.

!snapshot.hasData 로 로딩을 그려줘도 됩니다. dart는 다양한 메쏘드들이 많아서 좋기도 하지만 때때로는 뭘 써야할 지 몰라서 혼돈이 오기도합니다.

결론

대부분의 모듈들은 이미 스트리밍을 갖추고 있지만, 없는 경우 StreamControler 정도만 사용하면 됩니다.

저 같이 C/JAVA등으로 임베디드 위주로 시작한 올드 개발세대에게는 매우 생소하고 와닿지 않는 개념이지만 이해하고 나면 분명히 쓸모가 있습니다~

영상

댓글남기기