StatefulWidget은 상태를 유지하는 위젯입니다. 상태란 화면에서 갱신해야 하는 데이터를 의미합니다. 동적인 화면을 만들려면 StatefulWidget을 상속받은 클래스와 State를 상속받은 클래스가 필요합니다.
- StatefulWidget: 위젯 클래스
- State: StatefulWidget의 상탯값을 유지하고 화면을 구성하는 클래스
StatefulWidget을 상속받은 클래스에는 build() 함수가 없습니다. 대신 createState() 함수를 꼭 재정의해야 합니다. 그리고 이 함수의 반환값은 State를 상속받은 클래스여야 합니다. StatefulWidget에도 변수나 함수를 정의할 수 있지만 이 클래스에서 화면을 구성하지는 않으므로 보통은 단순하게 작성합니다.
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
StatefulWidget을 사용할 때는 createState() 함수에서 반환하는 상태 클래스가 중요합니다. 상태 클래스는 State를 상속받아 작성합니다. 이 상태 클래스에는 build() 함수를 꼭 재정의해야 하며 이 함수에서 반환하는 위젯이 StatefulWidget의 화면을 구성합니다.
// 상태 클래스 구현하기
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
..(생략)..
}
}
상탯값 변경하기
StatefulWidget을 사용하는 이유는 State 클래스에서 상태를 관리할 수 있기 때문입니다. 상태는 State에 선언한 변수를 의미하며 이 변숫값을 변경할 때 화면을 다시 빌드합니다. 그런데 State에 선언한 변숫값을 단순히 변경하는 것만으로 화면을 다시 빌드하지는 않습니다.
다음 코드를 보면 State 클래스에 enabled와 stateText라는 속성을 선언했습니다. 이 값을 changeCheck() 함수에서 변경한다는 가정입니다. 하지만 다음처럼 작성하면 속성값은 변경되지만 변경된 값이 화면에 반영되지는 않습니다.
// 상탯값 변경 예(화면에 반영되지 않음)
class _MyWidgetState extends Stated<MyWidget>{
bool enabled = false;
String stateText = "disable";
void changeCheck() {
if (enabled) {
stateText = "disable";
enabled = false;
} else {
stateText = "enable";
enabled = true;
}
}
.. (생략) ..
}
이처럼 속성값을 변경했는데 화면에 반영되지 않는 이유는 State에 선언한 모든 속성이 화면과 관련된다고 볼 수 없기 때문입니다. 예를 들어 State에 속성을 10개 선언했는데 화면에 출력할 속성은 5개라고 가정해 봅시다. State에 선언한 속성값을 변경하자마자 화면을 다시 빌드한다면 화면과 관련이 없는 속성값을 변경할 때도 화면을 다시 빌드합니다. 매우 비효율적입니다. 화면 빌드는 내부적으로 많은 처리를 수행하므로 공짜가 아닙니다.
따라서 State에서 화면을 다시 빌드하는 순간은 State의 속성값을 변경하는 순간이 아니라 setState() 함수를 호출하는 순간이어야 합니다. setState() 함수는 State 클래스에서 사용할 수 있으며 화면을 다시 빌드하게 해줍니다. 어디선가 setState() 함수를 호출하면 화면을 구성하는 build() 함수가 다시 호출되고 그 결과로 반환된 위젯으로 화면을 갱신합니다. 따라서 속성값이 변경될 때 화면을 다시 빌드해야 한다면 코드를 다음처럼 작성합니다.
// 상탯값 변경하기
class _MyWidgetState extends State<MyWidget>{
bool enabled = false;
String stateText = "disable";
void changeCheck() {
setState(() {
if (enabled) {
stateText = "disable";
enabled = false;
} else {
stateText = "enable";
enabled = true;
}
});
}
.. (생략) ..
}
setState()의 매개변수는 함수입니다. 이 매개변수에 지정한 함수가 끝난 후에 자동으로 build() 함수를 호출합니다. 이 코드에서는 화면에 갱신할 속성값을 setState()의 매개변수에 지정한 함수에서 변경했지만 꼭 그래야 하는 것은 아닙니다. 속성값을 다른 곳에서 변경해도 어차피 setState()로 인해 build()가 호출되므로 화면에 변경 사항이 적용됩니다.
상태 클래스의 역할 알아보기
StatefulWidget으로 화면을 구성할 때 StatelessWidget처럼 StatefulWidget에서 build() 함수를 이용해 화면을 구성하고 상태를 관리하면 되지 않을까 싶은데 이는 성능과 관련된 문제입니다. 이해를 돕기 위해서 위젯 클래스를 다음처럼 작성했다고 가정해 보겠습니다.
// 위젯 클래스 구현 예
class MyParentStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyParentStatefulWidgetState();
}
}
class._MyParentStatefulWidgetState extends State<MyParentStatefulWidget> {
... (생략) ...
@override
Widget build(BuildContext context) {
return Column(
children: [
MySubStatelessWidget(),
MySubStatefulWidget()
],
);
}
}
상태 클래스의 build() 함수에는 MySubStatelessWidget과 MySubStatefulWidget을 포함했습니다. MySubStatelessWidget은 StatelessWidget을 상속받고, MySubStatefulWidget은 StatefulWidget을 상속받는다고 가정하고 위젯 트리를 그리면 다음과 같습니다.
MyParentStatefulWidget
|
Column
|
-------------------------------------
| |
MySubStatelessWidget MySubStatefulWidget
이 위젯 트리에서 MyParentStatefulWidget은 StatefulWidget이므로 build() 함수는 반복해서 호출될 수 있습니다. build() 함수가 다시 호출되면 내부에서 생성한 Column, MySubStatelessWidget, MySubStatefulWidget 클래스의 객체도 다시 생성됩니다. 플러터에서 위젯은 불변이므로 StatelessWidget이든 StatefulWidget이든 화면을 다시 빌드하면 이전 객체를 다시 이용하는 것이 아니라 새로운 객체가 생성됩니다.
그런데 StatelessWidget은 상태를 관리하지 않으므로 보통은 데이터가 많거나 로직이 복잡하지 않습니다. 따라서 상위 위젯으로 인해 화면이 다시 빌드될 때 객체가 새로 생성되더라도 문제가 없습니다.
문제는 StatefulWidget입니다. StatefulWidget은 데이터를 유지하면서 다양한 업무를 처리하고 그 결과로 화면을 갱신하겠다는 의도이므로 보통은 StatelessWidget보다 더 많은 데이터에 복잡한 로직을 가집니다. 즉, StatefulWidget이 StatelessWidget보다 무겁다는 의미입니다. 이런 객체를 화면이 다시 빌드될 때마다 다시 생성한다면 비효율적입니다.
따라서 StatefulWidget은 위젯 트리 구조에 포함해 매번 생성되게 만들고, 실제 데이터와 업무 로직은 State 객체를 따로 두어 화면이 다시 빌드될 때마다 매번 생성되지 않게 합니다.
StatefulWidget이 처음 생성될 때 State 객체도 함께 생성되지만 화면이 다시 빌드되어 StatefulWidget이 다시 생성되더라도 State 객체는 다시 생성되지 않고 이전에 사용하던 State 객체를 그대로 이용합니다.
// 상태 클래스
class MySubStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MySubStatefulWidgetState();
}
}
StatefulWidget 클래스에는 createState() 함수를 꼭 작성해야 하며, 이 함수에서 State 객체를 반환합니다. 하지만 createState() 함수는 StatefulWidget이 처음 생성되는 순간에만 호출됩니다. 즉, 화면이 다시 빌드될 때마다 호출되지 않습니다.
위젯 트리와 똑같은 구조로 엘리먼트 트리가 만들어지며 Element 객체에 StatefulWidget을 위한 State 객체의 정보가 포함됩니다. State 객체는 우선 타입을 기준으로 찾고 타입이 같은 객체가 여러 개이면 위젯의 키값으로 찾습니다. 그래도 없으면 createState() 함수를 호출해 State 객체를 생성합니다.
여기서 중요한 것은 결국 StatefulWidget은 State 객체를 따로 두고 메모리에 유지하면서 재사용하고, 화면이 다시 빌드될 때마다 StatefulWidget 객체만 생성된다는 점입니다. 이런 이유로 StatefulWidget은 State와 함께 사용합니다.
'Flutter' 카테고리의 다른 글
정적인 화면 만들기 (0) | 2024.11.15 |
---|---|
위젯 트리 알아보기 (0) | 2024.11.15 |
화면을 구성하는 위젯 (0) | 2024.11.15 |
멤버를 공유하는 믹스인 (1) | 2024.11.15 |
추상 클래스와 인터페이스 (0) | 2024.11.15 |