brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Apr 28. 2020

플러터 StatefulWidget 생명주기

StatefulWidget 생명주기(Lifecycle)

안드로이드 개발자라면 액티비티 생명주기를 알고 있을 것이다. 이와 비슷한 개념이 플러터에도 존재한다. 단 StatelessWidget은 생명주기가 없고 State 객체를 생성하는 StatefulWidget만 생명주기를 가진다.


생명주기 관련 함수는 State 클래스에 정의되어 있다. 각 함수들이 어떤 의미이고 언제 어떤 순서로 호출되는지 알아보자.


참고로 해당 생명주기는 SDK 버전 (v1.12.13+hotfix.8)에서 유효하다. 정확히 언제인지는 모르겠지만 아무튼 최신버전에서는 생명주기가 변경되었으니 참고바란다.



우선 주요 함수들의 생명주기는 다음과 같이 도식화할 수 있다.

주요 함수

1) createState()

StatefulWidget 객체를 생성하면 당연히 생성자가 호출된다. 그 후 곧바로 createState()가 호출된다. StatefulWidget에서 필수적으로 오버라이드 해야 하는 함수이다. 이 함수의 역할은 State 객체를 생성하는 일이다.


2) initState()

State 객체가 생성되면 State 객체의 생성자가 호출된다. 이렇게 위젯이 최초 생성되는 상황이면 initState()가 호출된다. 즉 처음 한 번만 호출되고 호출되지 않는다는 뜻이다. 


3) didChangeDependencies()

initState()가 호출된 후에는 didChangeDependencies()가 호출된다. initState()는 최초 한 번만 호출된다고 했다. 그러면 didChangeDependencies()도 한 번만 호출되는 걸까? 아니다. initState() 후에도 호출이 되지만 본질적으로는 해당 위젯이 의존하는 위젯이 변경되면 호출된다. inheritedWidget을 사용하는 경우가 대표적이라고 할 수 있다. 쉽게 말하자면 위젯 A가 위젯 B를 상속받았는데 위젯 B가 업데이트될 때 호출된다. inheritedWidget과 관련된 글에서 다시 살펴볼 것이다.


4) build()

build()를 통해서 위젯이 그려진다(render). State 클래스에서 반드시 오버라이딩 되어야 하는 함수이다. 그렇지 않으면 에러로 알려준다. 위젯을 그려주는 역할을 하기 때문에 최초는 물론이고 변경이 있을 때마다 호출이 된다.


5) setState()

State 객체의 상태가 변경되었다는 것을 프레임워크에 알리는 용도이다. 따라서 State 객체의 상태가 변경될 때마다 setState() 함수를 통해서 알려야 한다. 그래야만 프레임워크가 build() 함수가 호출할 준비를 한다.


6) didUpdateWidget()

부모 위젯이 재 빌드되어 위젯이 갱신될 때 호출된다. didUpdateWidget()이 호출된 후에는 항상 build()를 호출한다. 따라서 만약 didUpdateWidget() 내에서 setState()를 호출하면 build()를 중복 호출하는 것이다.


7) deactivate()

트리에서 State 객체가 제거될 때마다 호출된다. 어떤 경우에는 프레임워크가 제거된 State 객체를 트리의 다른 부분에 다시 삽입하기도 한다. 이 경우에는 State 객체가 트리의 새로운 위치에 적응할 수 있는 기회를 주기 위해 build()를 호출한다.


8) dispose()

트리에서 State 객체가 영구적으로 제거될 때 호출된다. 영구적으로 제거되었기 때문에 build()가 다시 호출되지 않는다. 이미 State 객체가 폐기되었기 때문에 dispose()에서 setState()를 호출하면 안 된다.


9) reassemble()

hot reload를 실행하면 reassemble()이 호출된다. reassemble()이 호출되면 build()도 호출이 된다.


예제 살펴보기

다음 코드는 실제 함수들이 호출되는 것을 확인할 수 있는 예제이다.

예제 코드 보기


예제는 간단한 카운팅을 하는 앱이다. 처음 실행하면 아래 좌측 화면이 나타나고 Counter 버튼을 누르면 우측 화면과 같이 숫자가 증가한다. 3번 눌러서 3이 표시되어있다.

처음 앱을 실행하면 볼 수 있는 로그는 다음과 같다.


I/flutter (31695): TestApp()

I/flutter (31695): build 0

I/flutter (31695): _FirstStatefulWidget()

I/flutter (31695): _FirstStatefulWidgetState() false

I/flutter (31695): initState() 1

I/flutter (31695): didChangeDependencies() 1

I/flutter (31695): build() 1 true


main()에서 처음 실행되는 StatelessWidget인 TestApp 생성자가 만들어지고 해당 위젯의 build() 함수가 호출된다. build()에서 _FirstStatefulWidget()을 MaterialApp의 home 속성으로 사용 중이다. 따라서 _FirstStatefulWidget()의 생성자가 호출된다. 이제부터 StatefulWidget의 생명주기에 관련된 함수의 로그를 확인할 수 있다. 앞서 봤던 생명주기 도식화를 상기하면서 로그를 보면 도움이 된다.


먼저 StatefulWidget인 _FirstStatefulWidget()의 생성자 로그가 보인다. 그러면 createState()가 호출될 것이고 State 객체가 만들어지면서 _FirstStatefulWidgetState()의 생성자 로그가 나타난다. 처음 State 객체가 만들어진 상황이기 때문에 initState()가 호출된다. initState()가 호출된 다음에는 didChangeDependecies()가 호출된다. 그 후 State 객체의 build()가 호출된다. 주요 함수에서 살펴보았던 순서와 동일하게 로그가 찍힌 것을 알 수 있다.


Counter 버튼을 한 번 누르면 다음과 같은 로그가 나타난다.


I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_DOWN

I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_UP

I/flutter (31695): onClick() 1

I/flutter (31695): setState() 1

I/flutter (31695): build() 1 true


_counter 변수의 값이 변경된 것을 반영하기 위해서 setState()를 호출한다. 그러면 build() 함수가 호출되어 실제로 변경된 값이 위젯에 반영된다.


Go Next 버튼은 다른 화면으로 넘어가는 동작을 한다. 다음과 같이 Next 대신 Back 버튼이 있고 AppBar의 타이틀이 다른 화면이 나온다. 처음 화면과 동일하게 카운팅만 한다.

Go Next를 눌러서 화면 이동했을 때 로그는 다음과 같다.


I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_DOWN

I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_UP

I/flutter (31695): _SecondStatefulWidget()

I/flutter (31695): _SecondStatefulWidgetState() false

I/flutter (31695): initState() 2

I/flutter (31695): didChangeDependencies() 2

I/flutter (31695): build() 2 true // 여기까지 동일

I/flutter (31695): deactivate() 1

I/flutter (31695): build() 1 true


새로운 StatefulWidget인 _SecondStateWidget()이 생성되면서 State 객체도 만든다. 처음 화면과 동일하게 initState(), didChangeDependencies(), build()까지 호출되었다.

그 후에 deactivate()가 호출되었다. 옆에 숫자가 1이므로 첫 번째 화면의 deactivate()의 로그이다. _FirstStateWidgetState 객체가 트리에서 제거되었다는 의미이다. 그러나 바로 다음에 build() 1이라는 로그가 있다. build()가 호출되었다는 것은 State 객체를 트리의 다른 어딘가에 다시 삽입한다는 것이다. 즉 완전히 제거된 것이 아니다.


이때 Go Back을 눌러보자.


I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_DOWN

I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_UP

I/flutter (31695): deactivate() 1

I/flutter (31695): build() 1 true

I/flutter (31695): deactivate() 2

I/flutter (31695): dispose() 2


두 번째 화면으로 넘어갈 때 deactivate() 된 후 어딘가에 다시 삽입된 _FirstStateWidgetState 객체가 다시 deactivate() 된 후 재 삽입되어 build() 되었다. 이 때는 다음과 같이 현재 보고 있는 화면을 위한 트리에 재 삽입된 경우일 것이다.

위 화면을 보면 알겠지만 State가 완전히 폐기된 것이 아니기 때문에 _counter의 값인 3도 그대로 유지되고 있다. 그러나 _SecondStateWidgetState 객체는 deactivate()된 후 dispose()까지 되었다. 이 경우는 State 객체가 트리에서 영구적으로 제거된 것이기 때문에 다시 Go Next로 이동하면 새롭게 State가 생성된다. 따라서 _counter의 값도 유지되지 않고 다음과 같이 초깃값을 가진다.

이전에 _SecondStateWidget을 생성했을 때와 완전히 동일하게 새로운 객체를 생성하는 것을 로그를 통해서 확인할 수 있다.


I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_DOWN

I/ViewRootImpl(31695): ViewRoot's Touch Event : ACTION_UP

I/flutter (31695): _SecondStatefulWidget()

I/flutter (31695): _SecondStatefulWidgetState() false

I/flutter (31695): initState() 2

I/flutter (31695): didChangeDependencies() 2

I/flutter (31695): build() 2 true

I/flutter (31695): deactivate() 1

I/flutter (31695): build() 1 true



다시 처음 화면으로 돌아와서 hot reload를 실행해보자. 이때 로그는 다음과 같다.


Initializing hot reload...

I/flutter (31695): reassemble() 1

I/flutter (31695): build 0

I/flutter (31695): _FirstStatefulWidget()

I/flutter (31695): didUpdateWidget() 1

I/flutter (31695): build() 1 true


hot reload 시에는 reassemble()이 호출되고 StatelessWidget인 TestApp이 build 된다. 그 후 _FirstStateWidget 생성자가 호출된다. 그러나 _FirstStateWidgetState 객체를 새롭게 만들지는 않는다. 이때는 부모 위젯인 TestApp이 재 빌드된 경우이기 때문에  _FirstStateWidgetState 객체에서는 didUpdateWidget()가 호출된 후 build()가 된다.


참고로 두 번째 화면에서 hot reload를 할 때의 로그는 다음과 같다.


Performing hot reload...

I/flutter (31695): reassemble() 2

I/flutter (31695): reassemble() 1

I/flutter (31695): build 0

I/flutter (31695): _FirstStatefulWidget()

I/flutter (31695): didUpdateWidget() 1

I/flutter (31695): build() 1 true

I/flutter (31695): _SecondStatefulWidget()

I/flutter (31695): didUpdateWidget() 2

I/flutter (31695): build() 2 true



브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari