Flutter Widgets
StatelessWidget
widget 은 app's UI 의 blueprint, configuration 조각에 불과하다. 이 configuration 은 무엇을 위해 필요할까? element 다. element 는 screen 에 실제로 무엇이 보여질지를 나타낸다.
widget 이 tree 에 마운팅 될 때 createElement() 메소드를 호출하며 일치하는 element 를 생성한다. StatelessWidget 은 StatelessElement 를 생성한다.
그런데 만약 데이터 변화에 따라 인터페이스를 rebuild 하고 업데이트 하려면 어떻게 해야할까? StatelessWidget 은 말그대로 stateless 이기 때문에 불가능하다. 해당 위젯은 data 변화를 트랙할 수 없다.
StatefulWidget
StatefulWidget 은 widget 의 특징인 immutable configuration 을 제공하면서도, 시간에 따라 변화하며 rebuild 를 트리거할 수 있는 state object 를 제공한다.
StatefulWidget 은 크게 2가지 역할을 담당한다. immutable 한 value 를 holding 하며, state object 를 생성한다. state object 는 내부에서 데이터를 변화할 수 있도록 허용하면서, child Widget 을 build 한다.
어떻게 동작하는걸까? 위에서 설명한 동작방식과 유사하지만, 몇가지 동작이 추가된다. 먼저 마찬가지로 widget 에 해당하는 element 가 생성된다. 이 때 생성되는 element 는 StatefulElement 이다.
StatefulElement 는 위젯에게 state object 를 요구하고, 이 때 StatefulWidget 에 포함되는 createState() 메소드를 통해 생성한 State 객체가 반환된다. 위 예시에선 그 결과 StatefulElement 가 ItemCounter State 를 관리하게 되는 것이다.
state object 에선 child 를 build 하는데, 이전과 같은 방식으로 build 를 통해 생성되고, Stateless Element 또한 해당하는 Text 위젯에 맞게 생성된다.
state object 내의 이벤트 핸들러에서 setState() 와 함께 count 값이 변경되면 UI update 가 trigger 된다.
element 가 가지고 있는 state object 의 count 값이 증가하고, state object 는 해당하는 element 를 dirty 상태로 마킹한다. 이는 다음 프레임에 children 을 rebuild 할 필요하다는 것을 의미한다.
그 결과 Text 위젯이 새롭게 생성되는 것을 확인할 수 있다. 여기서 중요한 것은, element 는 그대로 유지된다는 것이다. 이는 퍼포먼스에 중요한 역할을 한다.
심지어 ItemCounter 위젯보다 상위 위젯에서 name 의 값을 변경하였다고 가정 하더라도, 결과적으로는 위젯만 rebuild 되고 element 트리는 그대로 유지된다.
이는 stateful hot reload 와 같은 방식이라고 볼 수 있다.
아이러닉한 점은 flutter 에 익숙해질 수록 직접 StatefulWidget 을 사용하는 일은 줄어들게 된다는 것이다. 한가지 이유는 StreamBuilder 와 같이 상황에 맞게 이미 구현된 다양한 형태의 StatefulWidget 이 존재하기 때문이며, 다른 이유는 여러개의 nested stateful widget 을 생성하여 state 를 관리하는 것은 성가시기 때문이라고 한다.
두번째의 경우, top of the tree 노드의 state 데이터에 쉽게 접근하게 해주는 InheritedWidget 을 통해 해소할 수 있다.
Inherited Widget
이와 같은 상황에서 최상위 위젯에 Data 를 최하위 위젯에 보내려면, 일일이 위젯을 생성할 때 값을 전달해야 할 것이다.
단순하게 하단 위젯이 필요한 위젯에 Data 에 바로 접근하는 방법은 없을까? 이 때 사용할 수 있는 것이 Inherited Widget 이다.
사용방법은 다음과 같다. 다음과 같은 방식으로 InheritedWidget 을 상속하는 위젯을 생성한다.
얼마나 떨어진 자식 위젯인지 상관없이, InheritedNose 의 asset 값을 가져오고 싶다면 다음과 같이 호출하면 된다.
이를 static method 로 정의하면 가독성이 훨씬 좋아진다.
실제 Flutter 프로젝트를 진행한다면 자주 사용하게될 Theme.of(context) 와 같은 메소드가 바로 이와 같이 동작한다고 볼 수 있다. 주의할 점은, InheritedWidget 도 widget 이기 때문에 immutable 이며 내부 값을 변경할 수 없다. field 값을 변경하고 싶으면 해당 위젯 전체를 rebuilding 해야 한다. 따라서, 많은 Inherited Widget 은 전체 lifecycle 동안 변하지 않는다.
하지만, 사실상 final 변수더라도 필요에 따라 인스턴스 내부 필드의 값을 변하게 할 수 있다. 즉, 다음과 같이 사용할 수 있다.
NoseService 인스턴스 내부 필드 값은 얼마든지 변경 가능하다. 하지만 그 내부 값의 변경에 따라 화면이 rebuild 되진 않을 것이다.
키를 사용해야 하는 경우
Key 는 Widget, Elements, SemandticsNode 의 식별자로 사용된다. 이는 주로 build phase 에 element 가 재사용될 수 있을지를 판단하기 위해 runtime type 비교와 함께 사용된다.
사실 대부분의 경우 key 를 쓸 필요가 없다. 즉, element 를 업데이트하거나 생성하기 위해 runtime type 만 사용될 것이다. 하지만 collection 내의 여러 위젯을 관리할 때 key 가 필수적으로 사용되어야 하는 경우가 존재한다.
만약 관리되는 위젯들이 stateless 라면 큰 문제 없이 동작한다. state 가 element 에서 따로 관리되지 않기 때문이다.
하지만, 관리되는 위젯들이 stateful 이라면, element 가 state 를 갖게 된다. 이 경우엔, widget 이 swap 되더라도 runtime type 은 동일하기 때문에 element 는 해당 변화를 처리하지 못해, 상태가 의도한대로 변하지 않게 된다.
이 경우에 key 를 넣어주어야 element 가 runtime type 이 같더라도 widget 의 위치가 변한 것을 알고, element 의 위치 또한 변경하게 된다.
key 는 top of the widegt subtree 에 적용되어야 한다. 이 때 주의할 점은, statefulwidget 이 state 가 없는 위젯에 포함되어 있는 경우에도 가장 상단의 위젯에 key 를 적용해야 한다는 것이다. 그 이유는 flutter 의 element-to-widget matching 알고리즘이 한 레벨의 tree 간의 관계에만 적용되기 때문이다.
flutter documentation 을 보면 여러개의 다른 종류의 key 가 있는 것을 확인할 수 있다. 크게는 GlobalKey 와 LocalKey 로 분류되는데, 각각의 사용 용도가 다르다.
먼저, 위 상황 같이 widget collections 내부의 위치를 변경해야 하는 경우에는 다른 children 과의 key 만 구분되면 된다. 그럴 때에는 상황에 맞게 LocalKey 유형인 ValueKey 나 ObjectKey 를 사용할 수 있다.
만약 collections 의 위젯을 ValueKey 나 ObjectKey 로 구분할 수 없다면, 즉 겹칠 수 있는 객체를 가지고 있거나 필드가 없거나 초기화 되지 않아 아예 이를 구분할 수 없는 경우엔 UniqueKey 를 사용할 수 있다. 이 또한 LocalKey 이다.
하지만 주의해야할 것은 UniqueKey 를 사용하면 새롭게 build 될 경우, 새로운 state object 를 생성되어 기존 상태가 날아갈 수 있다는 것이다. collection 내에서 위젯의 위치를 변경하거나 하는 경우엔 크게 문제될 것 없지만, build 메소드 내에서 사용될 경우 매번 새로운 state 가 생성되니 이를 잘 고려해야 한다.
GlobalKey 는 두 위젯 트리 내의 어디에서든 해당 상태에 접근할 수 있게 해준다. 예시로, Form 위젯과 Submit 버튼 위젯이 트리에 전혀 다른 위치에 존재할 때 제출 버튼에서 form 상태의 state object 를 참조하는 경우를 들 수 있다.
final formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: /* form fields */,
)
// In another part of the widget tree...
ElevatedButton(
onPressed: () {
if (formKey.currentState.validate()) {
// Submit the form
}
},
child: Text('Submit'),
)
하지만 이는 키를 추적하는데 추가 리소스가 필요한 비싼 연산으로, 잘 사용되지 않는다. Provider, Bloc 과 같은 다양한 상태관리 라이브러리를 통해 같은 목표를 퍼포먼스 이슈 없이 달성할 수 있기 때문에, 해당 라이브러리들을 사용하는 것이 더욱 권고된다.
참고 자료
https://www.youtube.com/watch?v=CXedqMlLo7M&list=PLOU2XLYxmsIJyiwUPCou_OVTpRIn_8UMd&index=1
https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
InheritedWidget class - widgets library - Dart API
Base class for widgets that efficiently propagate information down the tree. To obtain the nearest instance of a particular type of inherited widget from a build context, use BuildContext.dependOnInheritedWidgetOfExactType. Inherited widgets, when referenc
api.flutter.dev