알쓸전컴(알아두면 쓸모있는 전자 컴퓨터)

[Flutter] FutureBuilder에서 setState 할때 future 초기화 제외 하기 본문

Flutter,Dart

[Flutter] FutureBuilder에서 setState 할때 future 초기화 제외 하기

백곳 2019. 8. 1. 00:23

FuterBuilder는 

initState 에서 초기화 메소드가 있는것이 아니고 Widget build 단계에서 초기화 코드를 사용할수 있는 Builder 입니다. 

 

https://www.youtube.com/watch?v=ek8ZPdWj4Qo

Flutter 공식 Youtube 에 나온 설명이 제일 직관 적이네요!

 

하지만 사용중에 버튼 click 이벤트 함수 에서 화면 갱신이 필요해서 setstate 메소드를 실행 하면 

 

다시 builer 가 돌면서 FuterBuilder 의 future 부분(초기화) 을 실행 합니다. 

 

그리고 다시 Widget 을 builder 의 Widget 을 그리고 이부분은 상당히 골치 거리 입니다. 

 

왜냐하면 별것도 아닌것에 화면 갱신이 필요할때마다 초기화 코드를 실행하니까 말이죠. 

 

하지만 해결법은 있습니다. 

 

https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2(참고자료)

 

Flutter: My FutureBuilder Keeps Firing!

Using AsyncMemoizer to prevent FutureBuilder from refiring

medium.com

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

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

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  bool _switchValue;

  @override
  void initState() {
    super.initState();
    this._switchValue = false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Switch(
            value: this._switchValue,
            onChanged: (newValue) {
              setState(() {
                this._switchValue = newValue;
              });
            },
          ),
          FutureBuilder(
              future: this._fetchData(),
              builder: (context, snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.none:
                  case ConnectionState.waiting:
                    return Center(
                      child: CircularProgressIndicator()
                    );
                  default:
                    return Center(
                      child: Text(snapshot.data)
                    );
                }
              }
          ),
        ],
      ),
    );
  }

  _fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  }
}

먼저 위와 같은 코드가 있습니다. 

 

  _fetchData() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  }

해당 부분에서 2초 뒤에 REMOTE DATA 를 넘겨 주죠 

 

FutureBuilder 는 future 에서 _fetchData() 를 실행 하고 2초뒤에 'REMOTE DATA' 를 들고서는 

 

builder 함수로 가게 됩니다.

 

그리고 나서 스위치를 누르게 되면 아래와 같이 됩니다.  즉

 

다시 ConnectionState가none 으로 변하면서  _fetchData()  초기화 함수가 진행 되는것을 볼수가 있습니다. 

해당 부분을 수정 하는게 필요할때가 많습니다. 

 

원인은 아래와 같습니다. 

StatefulWidgets 에서 rebuilder 할때 아래와 같은 코드가 자동으로 적용 됩니다. 

@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.future != widget.future) {
    if (_activeCallbackIdentity != null) {
      _unsubscribe();
      _snapshot = _snapshot.inState(ConnectionState.none);
    }
    _subscribe();
  }
}

 

위에 보면 oldWidget.future != widget.future  결국 rebuild 할때 .future 를 똑같이 실행 합니다. 

결국 future에 걸린 CallBack 함수는 같다는 의미 입니다. 

 

여기서 에서 우리는 

AsyncMemoizer 클래스에 대해서 알아야 합니다 .

 

해당 클래스를 사용하기 위해서는 

 

pubspec.yaml 파일에

 

dependencies:

   async: ^2.3.0

 

을 추가 해주셔야 합니다.

 

해당 클래스는

https://api.flutter.dev/flutter/package-async_async/AsyncMemoizer-class.html 

 

AsyncMemoizer class - async library - Dart API

AsyncMemoizer class A class for running an asynchronous function exactly once and caching its result. An AsyncMemoizer is used when some function may be run multiple times in order to get its result, but it only actually needs to be run once for its effect

api.flutter.dev

async 를 한번만 실행 하고 그 결과값을 캐쉬에 저장 시켜 다시 async 함수를 실행 시킬때에 이미 실행해 캐쉬에 저장된 값을 리턴해주는 클래스 입니다 .

 

이것을 응용 하면 

 

코드 상단에 

 

final AsyncMemoizer _memoizer = AsyncMemoizer();

 

을 해주고 

 

_fetchData() {
  return this._memoizer.runOnce(() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  });
}

_fetchData() 함수를 다음과 같이 작성 하여 FutureBuilder 의 future 에 넣어 줍니다. 

 

그러면  setstate 를 할때 해당 함수는 캐쉬에 저장되어 있는 값을 바로 리턴 합니다. 결국 async 함수 안에 작성된 코드는 실행 하지 않고 캐쉬에 저장한 결과값만 리턴 합니다. 

 

완성된 코드는 아래와 같습니다. 

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

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

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
	final AsyncMemoizer _memoizer = AsyncMemoizer();
  bool _switchValue;

  @override
  void initState() {
    super.initState();
    this._switchValue = false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Switch(
            value: this._switchValue,
            onChanged: (newValue) {
              setState(() {
                this._switchValue = newValue;
              });
            },
          ),
          FutureBuilder(
              future: this._fetchData(),
              builder: (context, snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.none:
                  case ConnectionState.waiting:
                    return Center(
                      child: CircularProgressIndicator()
                    );
                  default:
                    return Center(
                      child: Text(snapshot.data)
                    );
                }
              }
          ),
        ],
      ),
    );
  }

	_fetchData() {
	  return this._memoizer.runOnce(() async {
	    await Future.delayed(Duration(seconds: 2));
	    return 'REMOTE DATA';
	  });
	}
}

 

 

 

 

Comments