프로그래밍/Flutter & Dart

[Flutter] Future Builder

*%$@$#@ 2023. 1. 5.
728x90
반응형

함수를 통해서 어떤 값을 리턴할 때 우리는 기본적으로 즉각적으로 값은 반환하는 상황을 주로 사용하였습니다.

하지만 애플리케이션 외부로부터 데이터를 전달받는 상황이라면 함수가 실행되고 나서 네트워크 문제등에 의해서 값을 바로 출력할 수 없는 경우가 발생합니다. 이 때 Future Builder를 이용해서 값을 받을 수 있습니다. Future Builder를 이용하면서 Future 타입의 데이터를 다루게 되는데 말 그대로 미래의 어느 한 시점(in the future)에 얻게 되는 데이터를 의미합니다.

다음의 소스코드를 통해서 값이 출력되는 과정을 살펴보도록 하겠습니다.

우리가 흔히 사용하는 함수는 함수가 호출되는 즉시 값을 반환하고 반환된 값을 변수에 저장한 뒤 화면에 출력하는 방법으로 사용됩니다. (getNumberNow함수)

하지만 외부로부터 네트워크를 통해 데이터가 전달되는 경우 함수가 호출된 후 값을 얻기까지 시간이 소요될 수 있으며, 이를 모사하기 위해서 getNumber라는 함수를 만들었습니다.

앞선 getNumberNow 함수는 반환자가 int 형식인데 반해 getNumber 함수의 경우 리턴타입이 Future<int> 타입입니다. 같은 int 타입이지만 늦게 받는다는 의미입니다.

두 함수의 차이점은 async와 await를 함수에 사용하였다는 점입니다. 함수명 바로 뒤에 async가 붙었으며 함수가 시작될 때 await가 명시되어 있습니다. 비동기 함수이며, 함수 실행이 완료될 때까지 멈춰 기다린다는 의미입니다.

getNumber 함수는 4초간 기다린 뒤 1이라는 값을 리턴하게 됩니다.

화면에서 출력하는 방법도 서로 다릅니다. getNumberNow()의 경우 Text 위젯을 감싸서 바로 호출하면 되지만, getNumber()함수수의 경우 FutureBuilder라는 별도의 위젯을 사용해야 합니다.

두 개의 파라미터를 설정해줘야 하는데 future는 데이터를 받을 Future 타입의 인스턴스(여기서는 getNumber())를 의미하며, builder는 context와 snapshot을 반환하여 각 상황에 맞게 화면을 구성할 수 있습니다. snpashot에는 future 인스턴스로부터 받은 데이터가 저장되는데, 이 때 저장된 데이터가 있으면 그 값을 출력하고 그렇지 않으면 진행표시아이콘이 출력되도록 하였습니다.

앱을 실행해보면 처음 시작하자마자 getNumberNow 함수를 출력한 결과(7)은 화면에 바로 출력되는데 반해 getNumber함수는 로딩 인디케이터를 보여주다가 4초 후에 결과가 출력됨을 확인할 수 있습니다.

floatingActionButton을 클릭하면 setState를 호출해 앱을 리빌드하도록 하였는데 이 때 getNumber()함수도 다시 호출이 되어 4초간 로딩 후 다시 값이 출력됩니다.

이는 향후 계속적으로 자료 요청을 하게되어 문제 발생 소지가 있습니다. 따라서 getNumber 함수를 바로 future에 지정하는 것이 아니라 이용하여 별도의 인스턴스를 만든 뒤 initState를 이용해서 인스턴스 초기화를 수행하여 리빌드 문제를 방지할 수 있습니다.

앱을 실행하면 세 개의 수가 출력되는데 가장 처음은 일반 함수를 이용한 것이고, 두 번째는 FutureBuilder를 이용하였으나 future 인스턴스로 getNumber 함수를 직접 호출하는 경우 세번째는 FutureBuilder를 이용하고 앞서 초기화된 futureData 인스턴스를 future의 파라메터로 전달한 경우입니다.

floatingActionButton을 클릭하였을 때 두 번째 값은 다시 실행되면서 새로운 값을 출력하지만 아래에 세번째 값은 변화가 없는 것을 확인할 수 있습니다.

이에 대한 최종 코드는 아래와 같습니다.


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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Future Builder'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    Key? key,
    required this.title,
  }) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<int?> futureData;
  
  @override
  void initState(){
    super.initState();
    
    futureData = getNumber();
  }  
  
  int getNumberNow() {
    return 7;
  }
  
  Future<int?> getNumber() async { 
    await Future.delayed(const Duration(seconds: 4));
    Random random = Random();
    
    return random.nextInt(100);
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment : MainAxisAlignment.center,
          children: [
            Text('${getNumberNow()}',
                             style : const TextStyle(fontSize:20),),
            const SizedBox(height : 20),
            FutureBuilder<int?>(
              future: getNumber(), 
              builder : (context, snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting: 
                    return const CircularProgressIndicator(); 
                  case ConnectionState.done:
                  default : 
                    if (snapshot.hasData) {
                      int data = snapshot.data!;  
                      return Text('$data',
                                 style : const TextStyle(fontSize:20),);
                    } else if(snapshot.hasError) {
                      return Text('${snapshot.error}',
                                 style : const TextStyle(fontSize:20),);
                    } else {
                      return const Text('No Data', 
                                       style : TextStyle(fontSize:20),);
                    }
                }
              }
            ),
            const SizedBox(height : 20),
            FutureBuilder<int?>(
              future: futureData, 
              builder : (context, snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting: 
                    return const CircularProgressIndicator(); 
                  case ConnectionState.done:
                  default : 
                    if (snapshot.hasData) {
                      int data = snapshot.data!;  
                      return Text('$data',
                                 style : const TextStyle(fontSize:20),);
                    } else if(snapshot.hasError) {
                      return Text('${snapshot.error}',
                                 style : const TextStyle(fontSize:20),);
                    } else {
                      return const Text('No Data', 
                                       style : TextStyle(fontSize:20),);
                    }
                }
              }
            ),
          ],
        ),   
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState((){}),
        tooltip: 'refresh',
        child: const Icon(Icons.refresh),
      ),
    );
  }
}



728x90
반응형

댓글