FutureBuilder
FutureBuilder 의 가장 큰 장점 중 하나는 캐싱이다. 최초 데이터를 불러오기 전에는 snapshot 의 data는 null 이지만, 그다음 build 부터는 이전에 가져온 데이터를 캐싱한다.
이 캐싱되는 기능을 잘 활용하면, 실제 데이터 로딩을 하고 있지만 유저에게는 로딩을 안하고 있는 것 처럼 보이게 할 수 있다. ex) 인스타그램 페이스북 등에서 실제 스크롤 다운하여 데이터를 로딩하고 있지만 전체가 로딩되지않고, 데이터가 불러올때까지 이전의 포스트들이 유지되는것
import 'dart:math'; import 'package:flutter/material.dart'; import 'package:test_can_remove/main.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { const textStyle = TextStyle( fontSize: 16.0, ); return Scaffold( /* * connectionState가 변경될때마다 builder 함수가 새로 호출된다. (화면에 State가 찍히는걸 보면 알 수 있음) * 따로 setState()를 호출하지 않아도 build가 다시 실행됨 * */ body: FutureBuilder( future: getNumber(), builder: (context, snapshot) { if (!snapshot.hasData) { /* * FutureBuilder는 최초 시작 이후 build된 이전 값이 캐시되기 때문에 최초 한번만 로딩바가 실행됨 * 예를 들어 페이스북에서 스크롤하여 새로운 값을 불러올때 전체가 재로딩되지 않음. * 기존 값들은 캐시 되어있고, 로딩바만 돌아가서 상대적으로 느려보이지 않게하고 데이터가 오는 순간에 화면에 있는 포스트를 바꿈 * */ return Center( child: CircularProgressIndicator(), ); } // if (snapshot.connectionState == ConnectionState.waiting) { // return Center(child: CircularProgressIndicator());// 굉장히 좋지 않음. 계속해서 indicator를 보여주면서 앱이 굉장히 느려보임 // } 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, ), Text( 'Data : ${snapshot.data}', style: textStyle, ), Text( 'Error : ${snapshot.error}', // Future 에러가 리턴되었을때 노출, 아니면 null style: textStyle, ), ElevatedButton( onPressed: () { setState(() {}); }, child: Text('setState'), ), ], ); }, ), ); } Future<int> getNumber() async { await Future.delayed(Duration(seconds: 1)); final random = Random(); // throw Exception('error occured'); // ConnectionState는 에러가 던져져도 done return random.nextInt(100); } }
일반적으론 FutureBulider에서는 3가지 케이스에 대해 에러 핸들링을 한다. (snapshot.hasData, snapshot.hasError
if(snapshot.hasData){ // 데이터가 있을때 위젯 렌더링 } if(snapshot.hasError){ // 에러가 났을때 위젯 렌더링 }
StreamBuilder
StreamBuilder 도 동일하게 데이터 값이 캐싱이 되어 새로 build 함수를 실행해도 데이터 값이 null로 돌아가지 않고 마지막으로 가져왔던 데이터 값을 넣어준다. 그 다음 새로 들어온 데이터들을 렌더링한다.
StreamBuilder 는 Stream 을 닫는 작업까지 해주기 때문에 더 편하게 사용 가능.
import 'dart:math'; import 'package:flutter/material.dart'; import 'package:test_can_remove/main.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { const textStyle = TextStyle( fontSize: 16.0, ); return Scaffold( body: StreamBuilder<int>( // <> 제너릭에는 실제 snapshot 에 들어갈 수 있는 데이터 타입을 넣어줌(안넣어도 되긴함) stream: streamNumbers(), builder: (context, AsyncSnapshot<int> snapshot) { // 로딩중일때 렌더링 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, ), Text( 'Data : ${snapshot.data}', style: textStyle, ), Text( 'Error : ${snapshot.error}', style: textStyle, ), ElevatedButton( onPressed: () { setState(() {}); }, child: Text('setState'), ), ], ); }, ), ); } Stream<int> streamNumbers() async* { for (int i = 0; i < 10; i++) { if(i == 5){ throw Exception('i=5'); // 에러 던지면 동일함 } await Future.delayed(Duration(seconds: 1)); yield i; // 1초마다 i 값 리턴 (active 상태 >> stream이 끝나지 않은 상태), 마지막 i 리턴되면 done으로 변경됨 } } }