플러터는 모든것이 위젯이다
정확히 개발자의 관점에서는 레이아웃 및 유저와 관련된 모든것은 위젯을 통해 수행됩니다.
위젯을 사용하면 개발자가 화면의 일부를 디맨션, 컨탠츠, 레이아웃 및 상호작용 측면에서 정의할수 있기 때문에 훨씬 더 많은 기능이 있습니다.
실제로 위젯이란 정확히 무엇일까요?
불변 설정
플러터 소스코드를 읽으면 위젯 클래스는 다음과 같이 정의되어 있습니다.
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
...
}
@immutable 어노테이션은 매우 중요하며 위젯 클래스의 모든 변수는 FINAL이어야 합니다. 따라서 일단 인스턴스화 되면 위젯은 더이상 내부 변수를 조정할 수 업습니다.
플러터로 개발할때 아래와 같이 위젯을 사용하여 화면의 구조를 정의합니다.
Widget build(`BuildContext` context){
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('My title'),
),
body: Container(
child: Center(
child: Text('Centered Text'),
),
),
),
);
}
이 예제는 7개의 위젯을 사용하여 구성되엇습니다.
SafeArea가 트리의 루트인것처럼 보입니다.
아시다시피 위젯 자체는 다른 위젯의 집합일 수 있습니다. 예를들어 이전 코드를 다음과 같이 작성할 수 있습니다.
Widget build(`BuildContext` context){
return MyOwnWidget();
}
위 코드는 MyOwnWidget 이라는 위젯 자체가 SafeArea, Scaffold를 랜더링한다고 가정합니다.
트리에서 엘리먼트의 개념
제가 왜 이것을 언급했는지 생각해 보셨습니까?
나중에 알게되겟지만, 디바이스에 랜더링할 이미지를 구성하는 픽셀을 생성하기 위해서는 화면을 구성하는 모든 작은 부분을 자세하게 알아야하고, 모든 부분을 결정하려면 모든 위젯을 전개할것을 요청해야합니다.
이것을 설명하기 위해 러시아 인형의 원리를 생각해보면 처음에는 닫힌 인형만 보이지만 뚜껑을 열어보면 다른 인형에는 다른 인형이 포함되어 있는것을 볼 수 있습니다.
플러터가 일부에서 전체로 모든 위젯을 전개했을때 그것은 전체에서 일부인 다른 러시아 인형을 얻는것과 유사할 것이다.
다음 다이어그램은 이전 코드에 해당하는 풀 위젯 계층 구조의 일부를 보여줍니다. 노란색으로 코드에 언급된 위젯을 강조하여 무분 위젯 트리에서 찾을수 있도록 하였습니다.
위젯트리라는 말은 프로그래머들이 위젯을 사용하기 때문에 이해하기 쉽게 하기 위해서 사용하지만 플러터에서 실제로는 위젯트리가 없습니다.
그대신 엘리먼트 트리라고 말해야합니다.
이제 엘리먼트의 개념에 대해 알아볼 시간입니다.
각 위젯마다 하나의 엘리먼트에 해당합니다. 엘리먼트는 서로 연결되어 트리를 형성합니다. 따라서 엘리먼트는 트리에서 무언가를 참조합니다.
엘리먼트를 부모, 잠재적자식이 있는 노드로 생각하십시오. 부모 관계를 통해 서로 연결되어 트리 구조를 얻습니다.
엘리먼트는 하나의 위젯을 가리키고 랜더 오브젝트를 가리킬 수도 있습니다.
참고로 엘리먼트는 엘리먼트를 생성한 위젯을 참조합니다.
정리하면
보시다시피 엘리먼트트리는 위젯과 랜더오브젝트 사이를 연결해 줍니다.
그렇다면 위젯이 엘리먼트를 생성하는 이유는 무엇일까요?
3개의 메인 카테고리 위젯들
플러터에서 위젯은 3가지 메인 카테고리로 나뉩니다. 개인적으로 카테고리를 다음과 같이 부릅니다.
카테고리별로 위젯을 나누는것이 중요한 이유는 엘리먼트의 타입과 관련이 있기 때문입니다.
Element types
Element 는 크게 두가지 타입으로 나눌 수 있습니다.
위젯과 엘리먼트는 어떻게 연동이 될까?
플러터 전체 메커니즘은 엘리먼트 또는 랜더러 오브젝트가 틀렸다고 하는데 있다.
엘리먼트를 틀렸다라고 하는 방법은 다음과 같다.
틀렸음의 결과는 해당 엘리먼트가 더러운 엘리먼트 리스트에서 찾을수 있다.
이러한 틀렸음의 결과는 해당 랜더오브젝트가 다시 작성되거나 다시 그려져야 하는 랜더오브젝트 리스트에서 찾을 수 있다.
틀렸음의 유형에 관계없이 스케쥴러바인딩은 플러터 앤진에 새 프레임을 예약하도록 요청한다.
플러터 앤진이 스케쥴러 바인딩을 깨워 마술같은 일이 발생한다.
onDrowFrame()
이 글의 앞 부분에서 스케쥴러바인딩에서는 2가지 주요 역할이 있으며, 그중 하나는 프레임 재구축과 관련하여 플러터 앤진에서 생성한 요청을 처리할 준비가 되었음을 알려주는것이라고 언급했다.
이 단락에서 좀더 자세히 살펴보겠습니다.
아래의 시퀀스다이어그램은 스케쥴러 바인딩이 플러터앤진으로 부터 onDrawFrame()요청을 수신할때 발생하는 상황을 보여준다.
스텝1. 엘리먼트들
WidgetsBinding이 호출 된 후 먼저 엘리먼트와 관련된 변경사항을 체크합니다.
BuildOwner가 엘리먼트 트리 처리를 담당하므로 위젯 바인딩은 빌드 오너의 빌드 스코프 메소드를 호출합니다. 이 메소드는 틀렸음 된 엘리먼트목록(더러움리스트)를 순회하고 리빌드 요청하도록 한다.
rebuild() 메소드의 주요 역할을 다음과 같습니다.
다음 애니메이션은 위 과정을 좀 더 시각적으로 표현합니다.
위젯 올라옴 참고사항
위젯이 올라옴된 경우 위젯카테고리에 의해 정의된 특정 유형의 새로운 엘리먼트를 생성하도록 요청된다.
그러므로,
이러한 엘리먼트는 카테고리별로 고유한 동작을 수행합니다.
예를들어
스텝2. 랜더오브젝트
더러운 엘리먼트와 관련된 모든 작업이 완료되면 엘리먼트트리는 이제 안정적이며 랜더링 과정을 고려할때입니다.
랜더러바인딩은 랜더링트리 처리를 담당하므로 위젯바인딩은 랜더러바인딩의 드로우프레임 메서드를 호출합니다.
아래 다이어그램은 드로우프레임 요청 중에 수행되는 일련의 동작들을 보여줍니다.
이단계 동안 다음 동작이 수행됩니다.
스텝3. 제스쳐 처리
제스쳐는 제스쳐바인딩에 의해 처리됩니다.
플러터 앤진이 제스쳐 관련 이벤트와 관련된 정보를 window.onPointerDataPacket API를 통해 보내면 제스쳐바인딩이 이를 가로채고 버퍼링을 진행하여 다음 과정을 수행합니다.
1. 플러터 앤진에서 출력된 좌표를 디바이스 픽셀 비율과 일치하도록 변환한 다음
2. 랜더러뷰에 이벤트 좌표를 포함하는 모든 랜더러 목록을 제공하도록 요청합니다.
3. 그런다음 해당 랜더러 오브젝트 목록을 순회하여 관련 이벤트를 각각에 전달합니다.
4. 랜더러 오브젝트가 해당 이벤트를 기다리고 있다면 그것을 처리합니다.
이 설명에서 랜더러 오브젝트가 얼마나 중요한지 확인 할 수 있습니다.
스텝4. 애니메이션
이 글의 마지막 부분은 애니매이션 개념, 특히 티커 개념에 중점을 둡니다.
애니메이션을 시작할때 일반적으로 애니메이션컨트롤러 또는 이와 유사한 위젯또는 구성 엘리먼트를 사용합니다.
플러터에서 애니메이션과 관련된 모든 것은 티커의 개념을 참고합니다.
티커는 활성화 된 경우 단 한가지만 수행합니다.
스케쥴러바인딩이 콜백을 등록하도록 요청하고 다음에 사용가능한 경우 플러터 앤진에 다시 호출하도록 요청합니다. 플러터 앤진이 준비되면 onBeginFrame요청을 통해 스케쥴러 바인딩을 호출합니다.
스케쥴러바인딩은 이 요청을 가로챈 다음 티커 콜백 목록을 순회하고 각각 아이템을 호출합니다.
각 티커 틱은 이 이벤트에 관심이있는 모든 컨트롤러에 인터셉트 되어 처리됩니다. 애니메이션이 완료되면 이커가 비활성화되고 그렇지않으면 티커가 다른 콜백을 예약하도록 스케쥴러바인딩을 요청합니다.
빌드컨텍스트는 앨리먼트에서 구현할 수 있는 일련의 게터와 방법을 조화롭게 정의하는 인터페이스 입니다.
특히 빌드컨텍스트는 stl위젯 및 stf위젯의 build메서드 또는 stf위젯 state객체에서 주로 사용됩니다.
buildContext는 그 자체를 나타내는 Element 자체입니다.
- 빌드중인 위젯
- 컨텍스트 변수를 참조하는 State에 연렬된 stf위젯
이는 대부분의 개발자가 모르는 상태에서도 지속적으로 앨리먼트를 처리하고 있음을 의미합니다.
어떻게 빌드컨텍스트를 사용할 수 있을까?
빌드컨텍스트는 위젯과 관련된 엘리먼트 뿐만 아니라 트리의 위젯 위치에도 해당하므로 빌드컨텍스트는 다음과 같은 경우에 매우 유용합니다.
우리는 빌드컨텍스트가 엘리먼트라는것을 이해했으므로 재미를 위해 그것을 사용하는 방법을 보여주고 싶었습니다.
아래 코드는 빌드컨텍스트를 사용하여 stl위젯이 setstate를 마치 사용한것처럼 자체적으로 업데이트 할수 있게 합니다.
void main(){
runApp(MaterialApp(home: TestPage(),));
}
class TestPage extends StatelessWidget {
// final because a Widget is immutable (remember?)
final bag = {"first": true};
Widget build(`BuildContext` context){
return Scaffold(
appBar: AppBar(title: Text('Stateless ??')),
body: Container(
child: Center(
child: GestureDetector(
child: Container(
width: 50.0,
height: 50.0,
color: bag["first"] ? Colors.red : Colors.blue,
),
onTap:(){
bag["first"] = !bag["first"];
//
// This is the trick
//
(context as Element).markNeedsBuild();
}
),
),
),
);
}
}
setState() 메소드를 호출할때 _element.markNeedBuild()와 같은 일을 합니다.
저는 플러터가 어떻게 구성되었는지 아는것이 흥미로울수 있다고 생각했습니다. 그리고 모든것이 효율적이고 확장 가능하도록 설계되었음을 기억하기 바랍니다. 그리고 위젯 엘리먼트 빌드컨텍스트 랜더오브제트와 같은 핵심 개념들은 항상 이해하기 쉽지는 않습니다.