문제점
위젯의 lifecycle이 build 중인 상태에서 setState를 호출해서 발생
내 경우엔 builder에서 setState를 호출해서 발생했다.
더 정확한 원인을 알아보기 위해 디버그 모드로 살펴봤다.
setState 내부 코드를 들어가보면 많은 assert문을 지나 markNeedsBuild를 호출한다.
markNeedsBuild 내부 중에선 _debugIsInScope함수에서 false가 반환되어 호출된 if문 아래의 _debugAllowIgnoredCallsToMarkNeedsBuild는 false로 되어있어 ! 연산자로 인해 true가 되어 해당 if문의 내부에서 최종적으로 에러가 발생한다.
이제 에러를 발생시키는 아래의 _debugIsInScope 코드를 보면서 에러가 발생하는 상황에 대해 얘기해야 한다.
current에 초기화 값으로 들어오는 this는 rebuild 시작점인 Element가 들어오는데 에러 발생 상황에선 setState를 호출하려는 State를 가진 StatefulWidget이다.
target엔 rebuild 대상인 Element들이 들어오게 된다.
해결되는 코드를 안쓰면 에러 발생 상황엔 target에 setState를 호출하는 위젯이 들어가는데 이 위젯은 this인 StatefulWidget의 자식이다.
위의 코드를 보면 current는 자식이 아닌 부모(_parent)로 올라가며 target을 찾으므로 target에 this의 자식이 들어오게되면 당연히 찾을 수 없어 문제가 발생한다.
아래 코드는 똑같은 에러를 발생시키는 코드이다.
_debugIsInScope코드와 아래 코드를 통해 더 쉽게 에러가 발생하는 상황에 대해 설명하면 MyWidget이 setState를 실행하는 MyWidgetState를 가진 위젯이므로 _debugIsInScope의 current는 MyWidget(this)로 초기화된다.
이후에 setState를 호출하는 Builder가 target이 되면 Builder가 MyWidget의 자식으로 있어 parent로 올라가는 current는 Builder를 찾을 수 없으므로 false가 반환되어 에러가 발생하게 된다.
해결방법
build 중에 호출해야한다면 아래 코드 사용
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(doSomething);
});
위 코드를 통해 문제가 해결되는 이유는 addPostFrameCallback을 통해 build가 끝나고 한 프레임이 렌더링된 이후에 인자로 넘긴 콜백 함수가 실행되므로 setState가 호출되는 시점이 build가 끝난 후 StatefulWidget에서 실행되는 것과 같아 위 문제점에서 얘기했던 target이 rebuild 시작점보다 깊은 곳에 있는 문제가 발생하지 않는 것이다.
참고