StreamBuilder and FutureBuilder

Baek Dong Hyun·2022년 12월 22일
1

1. FutureBuilder

0. 설명

Future를 사용하는 이유와 같이 데이터를 모두 다 받기 전에 먼저 데이터가 없이 그릴 수 없는 부분을 먼저 그려주기 위해 사용이 되는 것이다. FutureBuilder가 없다면 데이터가 다 받아지기를 기다린 후 화면을 그리거나 데이터의 변함을 setState()를 통해 바꿔야 줘야 할 것이다.

Future란?

flutter의 경우 비동기 통신을 사용하고 있는데 이는 동기식 통신과 다르게 서버에서 데이터를 모두 받아오기전 화면을 그려줄수 있게 되는 장점을 가질 수가 있다. 

Future를 return해주는 함수같은 경우는 dispose를 해줄 필요없다.

Future를 사용하게 되면 미래의 잠재적인 값을 결정하게 되고 정보를 불러오는 동안 어떤걸 보여줄지 선택할수 있도록 해준다. 만약 서버에서 데이터를 받아올때에 어플리케이션 측에서는 정보를 언제 다 받는지 알수가 없다. 그렇기 때문에 future의 상태를 확실히 확인하는 과정이 필요하다.

1. 사용하는 곳

FutureBuilder는 대부분 앨범에서 이미지 가져오기, 현재 배터리 표시, 파일 가져오기, http 요청 등 일회성 응답에 사용

2. 예시용 코드

import 'dart:math';

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  
  Widget build(BuildContext context) {
    final textStyle = TextStyle(fontSize: 16.0);

    return Scaffold(
      body: FutureBuilder(
        future: getNumber(),
        builder: ((context, snapshot) {
          if (!snapshot.hasData) {
            return Center(
              child: CircularProgressIndicator()
            );
          }

          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'FutureBuilder',
                style: textStyle.copyWith(
                  fontWeight: FontWeight.w700,
                  fontSize: 20.0
                ),
              ),
              Text(
                'ConState: ${snapshot.connectionState}',
                style: textStyle,
              ),
              Row(
                children: [
                  Text(
                    'Data: ${snapshot.data}',
                    style: textStyle,
                  ),
                  // if (snapshot.connectionState == ConnectionState.waiting)
                  //   CircularProgressIndicator()
                ],
              ),
              Text(
                'Error: ${snapshot.error}',
                style: textStyle,
              ),
              ElevatedButton(onPressed: () => setState(() {}), child: Text('setState'))
            ],
          );
        }),
      )
    );
  }

  Future<int> getNumber() async {
    await Future.delayed(Duration(seconds: 3));

    final random = Random();

    return random.nextInt(100);
  }
}

화면은

우선 Future 함수는

Future<int> getNumber() async {
  await Future.delayed(Duration(seconds: 3));

  final random = Random();

  return random.nextInt(100);
}

3초 후에 랜덤한 숫자를 보여준다.

위에서부터 코드를 보자면

FutureBuilder(
  future: getNumber(),
  builder: ((context, snapshot) {
    if (!snapshot.hasData) {
      return Center(
        child: CircularProgressIndicator()
      );
    }
	...

FutureBuilder에 future를 통해 비동기 처리 함수를 불러오게 되고, 3초가 지나기 전까지는 data를 받아올 수 없으니 로딩 위젯이 나오게 된다. 3초가 지나면 해당 if문을 빠져나와 아래에 있는 코드들이 실행이 된다.

이후 코드를 보면

...

return Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: [
    Text(
      'FutureBuilder',
      style: textStyle.copyWith(
        fontWeight: FontWeight.w700,
        fontSize: 20.0
      ),
    ),
    Text(
      'ConState: ${snapshot.connectionState}',
      style: textStyle,
    ),
    Row(
      children: [
        Text(
          'Data: ${snapshot.data}',
          style: textStyle,
        ),
        // if (snapshot.connectionState == ConnectionState.waiting)
        //   CircularProgressIndicator()
      ],
    ),
    Text(
      'Error: ${snapshot.error}',
      style: textStyle,
    ),
    ElevatedButton(onPressed: () => setState(() {}), child: Text('setState'))
  ],

...

중간에

Text(
  'ConState: ${snapshot.connectionState}',
  style: textStyle,
),

이부분 출력하면 ConnectionState.waiting이 출력되다가 done 으로 바뀐다. 현재 상태에대해 알 수 있다.

그 밑에

Text(
  'Data: ${snapshot.data}',
  style: textStyle,
)

이 부분에서는 통신한 후의 data를 가져와 출력한다.

ElevatedButton(onPressed: () => setState(() {}), child: Text('setState'))

여기서 setState 함수를 넣고 해당버튼을 눌러 재 실행하면 맨 처음에는 로딩화면이 나왔지만 그 후엔 상태만 waiting으로 뜨고 값도 null이 아닌 이전 값에서 done으로 바뀌면서 바뀐 값으로 다시 출력한다.

connectionState가 바뀔 때 builder 함수가 새로 불린다. 그래서 setState함수를 사용하면 build를 실행하지 않고 화면의 변화를 FutureBuilder가 캐치해준다.
FutureBuilder 는 이전 데이터를 기억하는 캐싱데이터를 가져온다. FutureBuilder 가 실행이 되고, 기존의 값을 기억한 다음 바뀐 값으로 저장을 한다.

에러를 확인하기 위해 throw를 사용한다.

Future<int> getNumber() async {
  await Future.delayed(Duration(seconds: 3));

  final random = Random();

  throw Exception('에러가 발생했습니다.');

  return random.nextInt(100);
}

이렇게 return전에 throw로 에러를 던지고 상단에

if (!snapshot.hasData) {
  return Center(
    child: CircularProgressIndicator()
  );
}

이 부분 로딩 화면을 잠시 주석한다. 출력된 화면을 보면

이미지를 보면 data가 잘 출력되던 안되던 done으로 출력된 것을 볼 수 있다.

data를 잘 받으면 data에 null이 아닌 값이 출력되고 Error에 null이 출력된다.

반대로 error가 난다면 data에 null이 출력되고 Error에는 에러 문구가 출력된다.

데이터 통신할 때 로딩처리 , 에러처리할 때는

if (snapshot.hasData) {...}
if (snapshot.hasError) {...}

2. StreamBuilder

0. 설명

stream 처리를 하기위해 StreamBuilder를 사용한다. StreamBuilder를 사용하면 setState 함수를 사용하지 않고도 UI를 업데이터 할 수 있다. 항상 stream의 최신 값을 가져오니, 최신 값을 확인 할 필요가 없다.

Stream이란?
Stream은 데이터가 들어오고 나가는 통로이다. 데이터가 변하는 걸 보고 있다가 그에 맞춰 적절한 처리, 필터링(where)이나 수정(map), 버퍼링(take)같은 일을 한다.

1. 사용하는 곳

StreamBuilder는 대부분 위치 업데이트 , 음악 재생 , 스톱워치 일부 데이터를 여러번 가져올 때 사용

2. 예시용 코드

import 'dart:math';

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  
  Widget build(BuildContext context) {
    final textStyle = TextStyle(fontSize: 16.0);

    return Scaffold(
      body: StreamBuilder(
        stream: streamNumber(),
        builder: ((context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}')
            );
          }

          if (!snapshot.hasData) {
            return const Center(
              child: CircularProgressIndicator()
            );
          }

          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'StreamBuilder',
                style: textStyle.copyWith(
                  fontWeight: FontWeight.w700,
                  fontSize: 20.0
                ),
              ),
              Text(
                'ConState: ${snapshot.connectionState}',
                style: textStyle,
              ),
              Row(
                children: [
                  Text(
                    'Data: ${snapshot.data}',
                    style: textStyle,
                  ),
                  // if (snapshot.connectionState == ConnectionState.waiting)
                  //   CircularProgressIndicator()
                ],
              ),
              Text(
                'Error: ${snapshot.error}',
                style: textStyle,
              ),
              ElevatedButton(onPressed: () => setState(() {}), child: Text('setState'))
            ],
          );
        }),
      )
    );
  }

  Stream<int> streamNumber() async* {
    const Duration duration = Duration(seconds: 1);
    for (int i = 0; i < 10; i++) {
      await Future.delayed(duration);

      yield i;
    }
  }
}

사실 위에 코드에서 Future를 Stream으로 바꾸고 비동기처리 함수를 다르게 바꿨다.

위에서부터 보자면 FutureBuilder랑 유사하다.

StreamBuilder(
  stream: streamNumber(),
  builder: ((context, snapshot) {

그냥 future위치에 stream이 들어가는거 말고 다른거 없다.

출력해서 확인하면 현재 상태가 조금 다르다.

Future와 마찬가지로 connectionState이걸로 상태를 알아볼 수 있다.

  • watting : Stream 을 기다리는 상태
  • active: Stream에서 계속 값을 받고 있을때!(Stream 이 완전히 끝나기 전 상태)
  • done: Stream이 완전히 끝난 상태

마찬가지로 data로 값을 출력할 수 있다.

StreamBuilder 또한 캐싱된 데이터를 가져온다. setState 한다고 해서 data 값이 null 로 가지 않는다.

Stream은 원래 한번 열게되면 닫아줘야하는데 StreamBuilder같은 경우는 닫는거까지 자동으로 해준다.

마찬가지로 에러를 확인하기 위해 코드를 수정

Stream<int> streamNumber() async* {
  const Duration duration = Duration(seconds: 1);
  for (int i = 0; i < 10; i++) {
    if (i == 5) throw Exception('i == 5');
    
    await Future.delayed(duration);

    yield i;
  }
}

i가 5일 때 에러를 던져본다.

Future와 마찬가지로 data가 null로 변하고 에러코드가 나온다.

정리
인강보면서 중요한 부분같아 정리를 해봤는데 앞으로도 자주 사용한다고도 한다.
현재는 아직 느낌으로밖에 잘 모르지만, 인강 잘 따라서해보고 혼자서 공부할 때 자주찾아보면서 적용하다보면 사용해야될 때 알맞게 적용할 수 있지않을까 생각한다.

profile
안녕하세요.

1개의 댓글

comment-user-thumbnail
2023년 10월 20일

잘봤씀다

답글 달기