Never give up

Flutter - addPostFrameCallback 본문

Flutter

Flutter - addPostFrameCallback

대기만성 개발자 2020. 8. 3. 16:27
반응형

build가 끝나지 않은 시점에서 데이터의 변경이 일어나고

notifyListeners를 사용해서 UI를 변경시키면 레드스크린을 볼 수 있습니다

예제로 initState에서 build가 끝나지 않은 상태에서 다음과 같이 값을 변경하면

@override
void initState() {
  super.initState();
  Provider.of<TestInt>(context, listen: false).setNum(2);
}

 

class TestInt extends ChangeNotifier {
  int _num = 0;

  int get getNum => _num;

  void setNum(int value) {
    _num = value;
    notifyListeners(); //문제가 되는 부분
  }
}

 

다음과 같은 에러가 출력됩니다
════════ Exception caught by foundation library ════════════════════════════════════════════════════
The following assertion was thrown while dispatching notifications for TestInt:
setState() or markNeedsBuild() called during build.

This _InheritedProviderScope<TestInt> widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<TestInt>
  value: Instance of 'TestInt'
  listening to value
The widget which was currently being built when the offending call was made was: Builder

축약하자면 build가 끝나기전에 setState 혹은 markNeedsBuild가 호출돼서 오류 발생

오류 발생이유는 lifecycle을 참조하시면 어느정도 이해가 되지않을까 합니다

(참조 : https://devmemory.tistory.com/8?category=935233)

 

해결방안으로 여러가지가 있습니다만 여기서는 두가지 방법을 알아보겠습니다

 

첫째로 콜을 하되 notifyListeners를 사용하지 않는 방법

 

Consumer가 시작되기 전에 값을 변경하면

 

변경한 상태로 출력되기때문에 notifyListeners가 필요하지 않습니다


그리고 build가 끝나는 시점에 callback함수를 호출하는 방법

메인

class CallBackExample extends StatefulWidget {
  @override
  _CallBackExampleState createState() => _CallBackExampleState();
}

class _CallBackExampleState extends State<CallBackExample> {
  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) => _updateData());
    return Consumer<TestInt>(
      builder: (context, value, child) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Callback'),
          ),
          body: Center(
              child: Text('${value.getNum}', style: TextStyle(fontSize: 30))),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.add),
            onPressed: () => value.setNum(value.getNum + 1),
          ),
        );
      },
    );
  }

  void _updateData() {
    Provider.of<TestInt>(context, listen: false).setNum(1);
  }
}

 

해당앱을 빌드하면 처음에 Text위젯에 value.getNum이 콜이 되고

초기값은 0이 되어야 하지만 _updateData콜백함수에서

build가 끝난 후 초기값을 1로 설정하기 때문에 1이 출력됩니다

 

해당 예제에서는 첫번째 방법이 더 간단하고 성능상 이점도 있지만

 

어떤 화면을 먼저 보여준 후 UI를 변경해야될 때 사용하면 좋을거 같습니다

 

주의할 점은 build에서 콜을 하게되면 rebuild가 될 때 다시 콜되기 때문에

 

bool을 이용해서 한번만 콜 되도록 처리를 해주던가

 

stateful위젯을 사용한다면 initstate에서 콜을 하는것이 좋을거 같습니다

 

-- 추가로 해당 동작이 화면이 바인딩 된 후 한번만 실행되면 좋겠다 싶을 때에는 initState에 넣어주시면 됩니다

  @override
  void initState() {
    super.initState();
    
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _updateData();
    });
  }

 

반응형
Comments