Flutter 의 Rendering 과 라이프사이클에 대한 정리
개인적으로 공부하면서 정리한 내용입니다.
잘못된 내용이 있으면 코멘트 부탁드립니다.
Lifecycle 에 대해 작성하기 앞서 Flutter 의 작동 방식에 대해 알아보자.
Flutter는 60fps 를 목표로 한다.
이는 1초에 최소 60번 화면을 다시 그린다는 의미이다.
일반적으로 화면을 한 번 그릴때마다 복잡한 계산을 해야 한다면 60fps 는 무리일 수 있다.
하지만, 미리 계산된 값이 존재하고 이 값이 변하지 않았다는 가정하에
새로 계산을 하는 대신 이전의 값을 가져다 쓸 수 있으면 속도 향상을 기대할 수 있을 것이다.
이러한 방식을 기반으로 Flutter 는 state 라는 개념을 가지고 있으며, 이를 기반으로 렌더링을 한다.
여기에 추가로 Tree 의 개념이 들어간다.
1. Flutter Tree
Flutter 에는 3가지의 Tree 가 있다.
- Widget Tree
- Element Tree
- Render Tree
Widget Tree
일반적으로 우리가 코드로 작성하는 위젯들이 구성되는 트리이다.
모든 위젯은 build 함수를 가지고 있으며, build 함수가 호출되면 Widget Tree 에 새로운 객체가 생성되고 기존에 있던 객체를 지우고 대체하게 된다.
Element Tree
Widget Tree 는 매우 빠르게 지워졌다 생겼다를 반복하지만, Element Tree 는 Widget 이 처음 생성될 때 만들어진다.
( Flutter 가 자체적으로 생성 )
Element Tree 는 Widget Tree 와 1:1 매칭을 하고 있으며, Widget 의 위치정보와 상태 정보를 가지고 있다.
기존에 연결되어 있던 Widget Tree 의 객체가 사라지고 새로운 객체로 변경되면, Element Tree 에서는 새로 만들어진 Widget 과의 1:1 매칭을 위해 탐색에 들어가고, 알맞은 Element 를 찾아서 Widget 과 연결하는 작업을 한다.
이때, Widget 의 정보가 변경되었으면 해당 Widget 을 담당하는 Render Tree 의 요소에게 해당 부분을 다시 그리라고 요청한다.
- Stateful Widget
- Stateless Widget 의 경우 Stateless Element 객체 하나가 생성되지만, Stateful Widget 은 Stateful Element 와 State 객체가 생성된다.
- 따라서 Widget Tree 에서 해당 Widget 이 새로 build 되면 Widget Tree 에 있는 state 의 정보를 기반으로 생성되게 된다.
Render Tree
실제로 화면에 그려지는 부분이며, Element Tree 에서 잠깐 언급했듯이 Element Tree 와 1:1 로 매칭되지만, Widget Tree 에 대한 정보는 가지고 있지 않는다.
이러한 특성때문에 Widget 이 여러번 build 되어 생성되고 없어지고 하더라도 Render Tree 가 Widget Tree 에 의존적이지 않기 때문에 렌더링에는 큰 영향을 주지 않게 된다.
2. BuildContext
모든 위젯은 고유한 context 를 가지고 있다. 여기서 context 는 해당 Widget 에 대한 Meta 정보와 Widget Tree 에서 어디에 위치하고 있는지에 대한 위치 정보를 가지고 있는 객체이다.
따라서 이 객체를 이용해서 Tree 안에 존재하는 특정 위젯을 찾아 접근할 수 있다.
@override
Widget build(BuildContext context) {
return Container();
}
3. Lifecycle
Stateless Widget 의 Lifecycle 은 constructor() => build() 가 끝이다.
( Immutable 객체이기 때문에 정보가 변경되지 않아 한번 빌드되면 끝이 난다. )
반면 Stateful Widget 의 Lifecycle 은 아래처럼 복잡한 절차를 가진다.
- createState()
- mounted = true
- initState()
- didChangeDependencies()
- build()
- didUpdateWidget()
- setState()
- deactivate()
- dispose()
- mounted = false
1) createState()
@override
State<StateFullTest> createState() => _StateFullTestState();
앞서 Element Tree 에서 잠깐 언급했듯이 Stateful Widget 은 Widget 객체와 State 객체로 구성되어 있다.
이 함수를 통해 State 객체를 만들고 반환한다.
2) mounted = true
모든 Widget 들은 mounted 라는 변수를 가지고 있으며, 구글에서 작성한 주석을 읽어보면 다음과 같다.
createState() 를 통해 State 객체를 생성 한 후 initState() 를 호출하기 전에 Framework 는 BuildContext 와 State 객체를 연결시킨 후 "mounts" 시킨다.
/// Whether this [State] object is currently in a tree.
///
/// After creating a [State] object and before calling [initState], the
/// framework "mounts" the [State] object by associating it with a
/// [BuildContext]. The [State] object remains mounted until the framework
/// calls [dispose], after which time the framework will never ask the [State]
/// object to [build] again.
///
/// It is an error to call [setState] unless [mounted] is true.
bool get mounted => _element != null;
3) initState()
@protected
@mustCallSuper
void initState() {
assert(_debugLifecycleState == _StateLifecycle.created);
}
Widget 이 생성되고 Widget Tree 에 추가될 때 호출되며 반드시 super.initState() 를 호출해야 한다.
Framework 는 각각의 State 객체가 생성될 때 오직 단 한번만 이 함수를 호출한다.
이 단계에서 하면 좋을 작업들
- 이 위젯이 속한 트리의 위치정보를 활용한 작업을 하는 경우 (BuildContext 관련 작업)
- Stream 구독 또는 다른 객체의 정보를 변경할 수 있는 이벤트에 대한 구독
4) didChangeDependencies()
@protected
@mustCallSuper
void didChangeDependencies() { }
- State 객체의 의존성이 변경되면 호출된다.
- 또한, initState() 가 호출된 직후에 호출된다.
- 여기에서 BuildContext.dependOnInheritedWidgetOfExactType 함수를 호출하기에 적절하다.
- InheritedWidget 을 참조하고 있다가 해당 위젯이 변경된 경우 해당 함수를 호출하여 변경사항을 알린다.
Framework 가 Dependency 의 변경이 발생할 때 마다 [build] 함수를 호출하기때문에 이 함수를 override 하여 비용이 큰 작업들을 처리하도록 구현할 수 있다. ( network fetches )
5) build()
@protected
Widget build(BuildContext context);
Framework 는 다양한 상황에서 해당 함수를 호출한다.
- initState() 를 호출 한 다음
- didUpdateWidget() 을 호출 한 다음
- setState() 에 대한 호출을 받은 후
- 현재 State 객체에 대한 의존성이 변경된 후
- deactivate() 가 호출 된 다음 State 객체를 해당 Tree 의 다른 위치에 집어넣는다.
Framework 는 위젯이 업데이트 가능한지의 여부에 따라 해당 위젯의 하위 트리를 build 함수가 리턴하는 위젯으로 교체할지 말지 결정한다.
6) didUpdateWidget()
@mustCallSuper
@protected
void didUpdateWidget(covariant T oldWidget) { }
Widget 의 구성이 변하면 언제든지 호출된다.
Framework 는 이 함수를 호출한 후에 반드시 build() 함수를 호출하기때문에 setState() 후의 모든 호출은 불필요하다.
부모 위젯이 다시 빌드되고 현재 트리에서의 위치를 요청하면 새로운 위젯과 이전의 위젯의 runtimeType 과 Widget.key 값이 같은지 다른지를 비교하여 업데이트한다.
7) setState()
@protected
void setState(VoidCallback fn) {
}
현재 객체의 State 가 변경되었다고 Framework 에게 알린다.
setState 에 전달하는 callback 은 즉시 synchronous 로 처리된다. ( 해당 콜백은 async 형태가 될 수 없다. )
8) deactivate()
@protected
@mustCallSuper
void deactivate() { }
Widget 이 Tree 에서 제거될 때 호출된다.
9) dispose()
@protected
@mustCallSuper
void dispose() {
assert(_debugLifecycleState == _StateLifecycle.ready);
assert(() {
_debugLifecycleState = _StateLifecycle.defunct;
return true;
}());
}
Tree 에서 영구적으로 제거될 때 호출된다.
State 가 영원히 다시 빌드되지 않을 때 호출하며, State 는 unmounted 된 상태로 간주하고 mounted 파라미터를 false 로 설정한다.
이 시점 이후로는 State 가 다시 mount 되는 경우는 없다.
- 이 객체에 할당된 리소스들을 해제하는 코드를 작성한다. ( ex. Animation 종료 )