Search
▪️

Widget & Flutter Internals - Deep Dive

Widget Tree에서 root의 build 메소드를 통해서 스크린의 Widget들이 그려지는 것은 사실이고, 변경 사항으로 다시 그려져야 한다고 하면 build 메소드 재 호출로 다시 그릴 것 같지만 사실은 그렇지 않다.
실제로 휴대폰 디스플레이는 60fps를 갖기 때문에 Flutter는 60fps application을 지원한다.
이렇게 많은 rendering을 해야하는데 root부터 다시 그리면 너무 비효율적이다.
따라서 변경 사항이 있는 부분만 다시 rendering 한다.
어떻게 이게 가능한지 알기 위해서는 이제까지 알고 있던 Widget Tree 뿐만 아니라 Element Tree와 Render Tree도 알아야 한다.
Widget Tree
Configuration Setting들을 갖고 있다. (rebuilds frequently)
Element Tree
Widget Tree의 Widget과 Render Tree의 Rendered Object를 포인터로 서로 연결한다. (rarely rebuilds)
Widget Tree에 기반하여 Flutter에 의해 자동으로 생성된다.
Widget Tree의 Widget들이 갖고 있는 Setting 값들을 가리키는 역할만 수행한다. (Stateful의 경우 State Object도 생성하여 Widget Tree와 연결한다.)
이전과 달라진 것이 있는지 감지하는 것이 가능하고 Rendered Object와 연결되어 있으므로 바뀐 것만 다시 rendering하는 것이 가능하다.
Render Tree
스크린에 Rendered Object가 나타난다. (rarely rebuilds)
사용자가 실제로 볼 수 있게 돕는다.
Setting값의 변화를 감지하여 rebuild 하는 것에 대해 살펴보자면, 모든 Element들은 Widget Tree의 Setting 값들을 가리키고 있는데 setState를 통해 현재 Widget Tree의 Setting 값이 달라지게 되면 새로운 setting 값을 가진 Widget을 만들게 된다. 그리고 기존의 Widget을 Discard하고 Element는 새로운 Widget을 연결하게 되는 것이다. 이에 따라 자식 Widget들도 다시 construct하게 되고, 이 역시도 바뀐 부분이 있는지 확인하면서 새로운 Widget을 만들지 말지 결정하게 된다. 새로운 Widget을 만들게 되면 Rendered Object를 새로 만들게 된다. 따라서 build를 호출한다고 해서 모든 Widget을 다시 그리는 것은 아닌 것이다.
즉, Widget Tree의 Widget은 한번 생성되면 immutable한 속성을 갖게 되는 것이다. 단, 바뀌는 것을 감지하여 Widget을 새로 instantiate하여 다시 연결하는 것일 뿐이다. 특히 Stateless의 Widget에서 class를 정의할 때, 생성자 선언부에 const 속성을 부여할 수 있고 이를 통해 complie-time에 instance를 미리 생성할 수 있다. (생성자 호출에는 dynamic time에 결정되는 값을 이용 시 const 할당이 불가능하다. compile-time에 결정되어 있는 경우는 const 할당이 가능하다. 이를 통해 불필요한 rebuild나 rebuild 검사를 막을 수 있다.)
build 메소드를 호출하는 것은 크게 두가지 방법이 있음
1.
setState()
2.
MediaQuery의 상태가 바뀔 때 (App의 규모가 클수록 조심해야 한다.)
따라서 바뀌는 것을 감지하여 buil되기 때문에 const를 쓰는 것도 좋은 방법이다.
Good Code라 함은 두 가지 측면을 볼 수 있다.
1.
Performance
2.
Readability / Understandability
main() 함수에 platform 체크 등으로 코드가 길어지면 builder() 메소드를 만들어 쓴다.
Widget의 Lifecycle이 도는 동안 App의 Lifecycle인 AppLifecycleState을 확인하기 위해서는 클래스에 with WidgetsBindingObserver를 작성하여 observer을 통해서 확인할 수 있다.
모든 Widget은 각자 만의 context를 갖고 있다. 이런 context들은 Flutter가 Widget들의 구조를 이해하는데 사용된다. 이해는 다음을 통해 이뤄지는데, context들을 통해 Widget Tree의 skeleton을 만들게 되고, 각 Widget들이 어떻게 연관 되어 있는지 파악할 수 있다.
모든 Widget 들이 Widget Tree를 구성하면서 데이터를 주고 받을 때, Constructor의 argument들로 주고 받게 되는데 Theme과 MediaQueryData는 Direct Tunnel 역할을 하는 Inherited Widget라는 이용하여 데이터를 바로 주고 받을 수 있다.
Flutter의 모든 Widget은 Key를 가질 수 있다. 하지만 대부분의 Widget들은 Key를 필요로 하지 않는다. (특히 Stateless Widget의 경우는 더 Key를 필요로 하지 않는다.)
그렇다면 Key가 왜 필요할까? → List 중에 하나의 item을 만들면서 생성한 값이 그 item과 묶여야 하는데, 묶이지 않아서 item 삭제 시 문제가 발생할 수 있다.
이유는 무엇인가? → Widget Tree와 Element Tree의 Pointing 연결이 문제인 것이다.
item 삭제 시 Widget은 바로 삭제 되지만 Element는 바로 삭제되지 않는다. Element는 Widget Tree의 동일 레벨에 매칭 되는 Widget이 없는지 확인 후에 Element가 삭제 된다. (즉, Flutter에서 필요가 없는 것은 삭제되는 것이다.) 따라서 중간에 끼여 있는 item을 삭제하면 Element는 동일 레벨에 있는 다른 Widget을 reference하면서 state를 그대로 유지하게 되는 문제가 있는 것이다. (Element Object는 타입 정보를 통해 자기 자신과 매칭되는 동일 레벨의 Widget도 알고 있고, 자기 자신이 갖는 child Element의 정보도 갖고 있다.) 즉, item은 사라졌는데 state는 그대로 유지하게 되는 현상이 발생하면서 생기는 문제인 것이다. 이는 Key를 할당하여 해결할 수 있다.
Key는 class 정의부에서 설정해주는 것이 아니라 class를 호출할 때 인자로 Direct로 넣엇서 설정해준다.
UniqueKey()
rebuild될 때마다 Unique한 Key 값을 생성하므로 원하는 결과를 못 얻을 수도 있다.
ValueKey()
Random Number 이용할 때는 아래와 같이 이용한다.
import 'dart:math';
Random().nextInt(int max);
:는 super constructor을 initializer할 수 있게 호출하고 데이터를 밀어 넣어준다.
ListView.builder ⇒ Key값에 대해 적절히 이용 불가능 (아직 버그 2019.12.30)
ListView(children: list.map()) ⇒ Key값에 대해 적절히 이용 가능