Study Record

[Flutter] FutureBuilder 본문

Flutter/widget

[Flutter] FutureBuilder

초코초코초코 2023. 2. 19. 00:39
728x90

✍ FutureBuilder Widget

네트워크 통신이 필요한 작업 등 비동기로 작업한 이후 UI를 업데이트를 해야 할 경우가 있다면 유용한 위젯이다. 

FutureBuilder<T>({   
  Key? key,   
  Future? future,   
  T? initialData,   
  required Widget Function(BuildContext, AsyncSnapshot<T>) builder, 
})

future에 비동기로 할 작업을 정의하고 builder에 future에 작업이 끝나기 전 보여줄 Widet , 끝난 후 보여줄 Widget을 정의하면 된다. 데이터 타입 T는 비동기 작업 후 반환할 데이터 타입으로 initialData는 future 작업이 끝나기 전 초기 데이터를 정의할 수 있다.

 

예를 들어, 코드가 다음과 같으면,

Future<String> _calculation = Future<String>.delayed(
    const Duration(seconds: 4),
    () => 'Data Loaded',
);

FutureBuilder<String>(
  future: _calculation, // a previously-obtained Future<String> or null
  initialData: "error",
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    print("snapshot state : ${snapshot.connectionState.name}");
    switch (snapshot.connectionState) {
      case ConnectionState.none:
      case ConnectionState.waiting:
        print("connectionState watting ${snapshot.data} , ${snapshot.hasData}");
        return const _LoadingWidget();
      case ConnectionState.active:
        if (snapshot.hasData) {
          return _SuccessWidget(successData: snapshot.data!);
        } else {
          return const _ErrorWidget(errorData: "active data error");         }
      case ConnectionState.done:
        if (snapshot.hasError || snapshot.hasData == false) {
          return const _ErrorWidget(errorData: "done error");
        } else {
          return _SuccessWidget(successData: snapshot.data!);
        }
    }
  },
),

snapshot 은 future 의 작업 상태(snapshot.connectionState)와 future의 반환값(snapshot.data) 정보를 알 수 있는데, connectionState는 4가지 상태를 가질 수 있다.

  • ConnectionState.none : initalData만 있는 상태
  • ConnectionState.waiting : future(비동기 작업)이 시작 단계에 있는 상태로 snapshot.data은 initialData인 상태이다.
  • ConnectionState.active : 비동기 작업의 반환값(snapshot.data)이 null 이 아니지만 언제든 바뀔 수 있는 상태이다. StreamBuilder 에서만 사용된다.
  • ConnectionState.done : 비동기 작업이 완료된 시점이며 반환값(snapshot.data)이 null 이 아니다.

 

또한, 비동기 작업 중 오류가 있다면 snapshot.hasError 값이 true 이며 초기 데이터(initialData)가 null 일 경우 비동기 작업의 반환값이 아직 없으면 snapshot.hasData 는 false을 리턴한다.

 

 

예시) 로딩 위젯에서 4초 뒤 성공 위젯으로 변함

 

import 'package:flutter/material.dart';

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

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final Future<String> _calculation = Future<String>.delayed(
    const Duration(seconds: 4),
    () => 'Data Loaded',
  );

  @override
  Widget build(BuildContext context) {
    print("build start");
    return DefaultTextStyle(
      style: Theme.of(context).textTheme.displayMedium!,
      textAlign: TextAlign.center,
      child: FutureBuilder<String>(
        future: _calculation, // a previously-obtained Future<String> or null
        initialData: "error",
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          print("snapshot state : ${snapshot.connectionState.name}");
          switch (snapshot.connectionState) {
            case ConnectionState.none:
            case ConnectionState.waiting:
              print("connectionState watting ${snapshot.data} , ${snapshot.hasData}");
              return const _LoadingWidget();
            case ConnectionState.active:
              if (snapshot.hasData) {
                return _SuccessWidget(successData: snapshot.data!);
              } else {
                return const _ErrorWidget(errorData: "active data error");
              }
            case ConnectionState.done:
              {
                if (snapshot.hasError || snapshot.hasData == false) {
                  return const _ErrorWidget(errorData: "done error");
                } else {
                  return _SuccessWidget(
                    successData: snapshot.data!,
                  );
                }
              }
          }
        },
      ),
    );
  }
}

class _SuccessWidget extends StatelessWidget {
  final String successData;
  const _SuccessWidget({required this.successData, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Icon(
            Icons.check_circle_outline,
            color: Colors.green,
            size: 60,
          ),
          Padding(
            padding: const EdgeInsets.only(top: 16),
            child: Text(
              'Result: $successData',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }
}

class _ErrorWidget extends StatelessWidget {
  final String errorData;
  const _ErrorWidget({required this.errorData, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Icon(
            Icons.error_outline,
            color: Colors.red,
            size: 60,
          ),
          Padding(
            padding: const EdgeInsets.only(top: 16),
            child: Text(
              'Error: $errorData',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }
}

class _LoadingWidget extends StatelessWidget {
  const _LoadingWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: const <Widget>[
          SizedBox(
            width: 60,
            height: 60,
            child: CircularProgressIndicator(),
          ),
          Padding(
            padding: EdgeInsets.only(top: 16),
            child: Text(
              'Awaiting result...',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }
}

 

 

+ setState() 와 FutureBuilder

StatefulWiget 에서 setState() 를 하면 build 함수가 다시 실행되면서 FutureBuilder 도 다시 만들어진다. 이 때, future 을 함수로 설정하면 비동기 작업도 다시 실행된다.

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

void main() {
  runApp(
    MaterialApp(home: HomeScreen()),
  );
}

class HomeScreen extends StatelessWidget {
  HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: _MyStatefulWidget(),
    );
  }
}

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

  @override
  State<_MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<_MyStatefulWidget> {
  TextStyle textStyle = TextStyle(
    color: Colors.black,
    fontSize: 15.0
  );

  Future<int> _calculation() async {
    print("_calculation 실행중");
    Future.delayed(Duration(seconds: 3));
    return Random().nextInt(100);
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTextStyle(
      style: Theme.of(context).textTheme.displayMedium!,
      textAlign: TextAlign.center,
      child: FutureBuilder<int>(
        future: _calculation(),
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text("snapshot state : ${snapshot.connectionState}", style: textStyle,),
              Text("data : ${snapshot.data}", style: textStyle,),
              Text("error : ${snapshot.error}", style: textStyle,),
              ElevatedButton(
                onPressed: () {
                  setState(() {});
                },
                child: Text("setsTate()"),
              )
            ],
          );
        },
      ),
    );
  }
}

 

 

future 에 함수대신 변수로 넣으면 setState() 를 해도 비동기 작업을 반복하지 않는다...

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

class HomeScreen extends StatelessWidget {
  HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: _MyStatefulWidget(),
    );
  }
}

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

  @override
  State<_MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<_MyStatefulWidget> {
  TextStyle textStyle = TextStyle(
    color: Colors.black,
    fontSize: 15.0
  );

  Future<int> _calculateValue = Future.delayed(Duration(seconds: 2), (){
    return Random().nextInt(1000);
  });

  @override
  Widget build(BuildContext context) {
    return DefaultTextStyle(
      style: Theme.of(context).textTheme.displayMedium!,
      textAlign: TextAlign.center,
      child: FutureBuilder<int>(
        future: _calculateValue,
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
          print("snapshot ${snapshot.connectionState}");
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text("snapshot state : ${snapshot.connectionState}", style: textStyle,),
              Text("data : ${snapshot.data}", style: textStyle,),
              Text("error : ${snapshot.error}", style: textStyle,),
              ElevatedButton(
                onPressed: () {
                  setState(() {});
                },
                child: Text("setsTate()"),
              )
            ],
          );
        },
      ),
    );
  }
}

 

 

 

 

FutureBuilder class - widgets library - Dart API

Widget that builds itself based on the latest snapshot of interaction with a Future. The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.bui

api.flutter.dev

 

728x90