💡
1. State란?
2. Stateful 과 Stateless 위젯의 차이
3. 위젯 간(부모->자식) State 전달 방식
4. 위젯 간(자식->부모) State 전달 방식
5. GetX를 이용한 상태관리
6. 플러터 위젯의 생명주기(build, didChangeDependencies, mounted와 setState, initState와 dispose, didUpdateWidget 등) -> 매우 중요
공식문서에 의한 state
➡️ 지금 isLoading은 False이네? 혹은 color의 값은 Colors.red이네?와 같이 볼 수 있다.
결국 Flutter에서 State란 데이터라고 생각하면 된다. 상태 관리는 데이터 관리라고 생각하면 된다.
애플리케이션에서 사용자와 상호작용을 하면서 데이터가 변한다. 그러면 애플리케이션은 그에 맞게 화면을 대응해줘야 한다. 뒤로 가기 버튼을 누르면 뒤로 가고, 버튼 클릭하면 특정 데이터가 저장이 되던지 해야 한다.
State는 두 개로 나눌 수 있다.
App state는 플러터 앱 전반에 걸쳐 사용되는 data로 여러 위젯이나 스크린에서 쓰이는 것들을 의미한다.
ex) 로그인 정보, 유저 설정, 장바구니 등등 ...
앱 여기저기 전반적으로 다 필요하고, 한쪽에서 app state를 변경하면 다른 쪽에서도 data 변경을 반영할 필요가 있다.
Widget state는 ephemeral state, local state라고도 불리며, 위젯 내부에서만 사용되는 data를 말한다. 위젯 내부에서 사용되니 딱히 따로 공유하거나 반환할 필요가 없다.
ex) 현재 PageView, 현재 진행중인 애니메이션, BottomNavigationBard의 현재 탭 등등 ...
하지만 이 두 상태를 명확히 구분할 수 없기 때문에 여러 개발 방법론이나 컨벤션, 개발팀의 개발자들끼리의 약속처럼 사전에 약속하는 방법밖에 없다.
➡️ 화면을 사용자의 요구에 맞게 업데이트하는 건 애플리케이션에서 필수적이다. 하지만 간혹 사용자가 터치해도 변하지 않아도 되는 화면이 존재하고 변해야하는 화면이 존재한다. 이 두 가지 경우를 Flutter에서는 나누어서 생각한다.
State는 데이터다. 사용자의 이벤트가 발생하면 데이터는 변한다. 그렇다면 사용자의 이벤트에도 변하지 않아도 되는 것과 사용자의 이벤트가 변하는 두 가지의 경우, 그리고 Widget까지.
setState라는 함수를 사용한다.
사진 속 두 위젯의 공통적인 부분 중 하나인 Input Data는 무엇일까?
Text("Hello")에서 Hello라는 String이 Text위젯의 input data가 된다.
즉, 클래스가 받는 매개변수가 된다.
➡️ Stateless는 State가 없는 Widget일까?
답은 아니다! 이다.
Stateless는 내부적으로 상태를 가지고는 있으나, 값을 변경할 수 없기에, 즉 Data의 상태가 변하지 않기에 Less가 붙었다.
내부적으로 Stateless도 상태를 가지고 있기에, Input Data가 바뀌면 StatelessWidget도 재빌드 된다.
import 'package:flutter/material.dart';
class MyStatelessWidget extends StatelessWidget {
final String title;
MyStatelessWidget({required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text('Hello, $title!'),
),
);
}
}
void main() => runApp(MaterialApp(home: MyStatelessWidget(title: 'Stateless Widget')));
➡️ Stateless와 반대로 상태가 존재하는 Widget으로, 내부적으로 data(=state)가 변경되면 그에 맞게 화면을 다시 그려 변경된 부분을 위젯에 반영하게 된다.
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stateful Widget'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
void main() => runApp(MaterialApp(home: MyStatefulWidget()));


➡️ Stateless Widget은 한 번 만들어지면 갱신할 수 없으므로 생명주기가 없다. 즉, 다른 화면으로 넘어가면 모든 로직이 종료된다.
: StatefulWidget 클래스를 상속받은 클래스는 반드시 createState() 함수를 호출해야 한다. 이 함수는 다른 생명주기 함수들이 포함된 State 클래스를 반환한다. 즉, 위젯의 상태를 생성하는 함수로 생각할 수 있다.
class MyHomePage extends statefulWidget{
@override
_MyHomePageState createState() => new_MyHomePageState();
}
Widget 클래스를 나누는 이유?
: createState() 함수가 호출되어 상태가 생성되면 곧바로 mounted 속성이 true로 변경된다.
위젯을 화면에 장착하면?
➡️ mounted == true
mounted 속성이 true라는 것은 위젯을 제어할 수 있는 buildContext 클래스에 접근할 수 있다는 의미이다. 그렇지 않으면 오류가 발생하며 이 buildContext가 활성화 되어야 나중에 나올 setState() 함수를 이용할 수 있다.
즉, 다른 의미로는 setState() 함수를 호출하기 전에 mounted 속성을 점검 코드로 활용하면 안전하게 작성이 가능하다.
if( mounted ){
setState()
}
: 위젯을 초기화하는 기능을 하는 함수.
: 위젯이 생성된 후 State 객체가 생성될 때 호출되는 메소드. 처음 한번만 호출되고 그 이후로는 호출되지 않는다.
@override
initState(){
super.initState();
_getJsonData();
}
: initState() 함수를 호출할때 내부에서 _getJsonData()함수를 호출해 서버에서 받아온 데이터를 화면에 출력하게 만들 수 있다.
: 의존성이 변경되면 호출하는 함수이며, initState가 끝나고 호출된다. 해당 위젯이 의존하는 위젯이 변경되면 재호출된다. (cf. ingeritedWidget)
: context를 사용하여 변수를 초기화하는 명령어가 있다고 할때, initState는 context가 형성되기 전에 호출되어 사용하지 못하므로, didChangeDependecies 메소드 내에서 context를 사용하여 변수를 초기화할 수 있다.
: 이 메소드를 통해 위젯이 그려진다. State 클래스에서 반드시 오버라이딩되어야 하며, 상태가 변경될때마다 호출된다.
@override
Widget build(BuildContext context){
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.amber,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
: State 객체의 상태가 변경되었다는 것을 프레임워크에 알리는 메소드이다. State 객체의 상태가 변경되었을 때마다 호출해줘야한다.
: setState를 할때마다 어떤 명령어를 내리고 싶다면, build 메소드처럼 오버라이딩한 함수를 객체 내에 정의하면 된다.
이때, super.setState(fn)을 하지 않으면 원하지 않는 결과가 나올 수 있다.
class _MyHomePageState extends State<MyHomePage>{
String str = "";
@override
void setState(VoidCallback fn) {
str += "in setState\n";
super.setState(fn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: Colors.red,
child: Text(str),
),
TextButton(
onPressed: () {
str += "in build\n";
setState(() {});
},
child: Text("button"),
),
],
),
),
);
}
}
: 특정 이벤트 같은 경우로 인해 위젯이 변경되면, 이 함수를 호출해 갱신할 수 있다. initState 함수는 위젯을 초기화할 때 한 번만 호출되므로 위젯이 변경되었으 때 호출하는 didUpdateWidget가 필요한 것이다.
: 부모위젯이 재빌드되어 위젯이 갱신될때 호출된다. 이때, 해당 메소드가 호출된 후에는 항상 build() 메소드가 호출되므로, setState를 이 메소드 내에서 호출하면 build가 두 번 호출되는 것이다.
@override
void didUpdateWidget(Widget oldWidget){
if (oldWidget.importantProperty != widget.importantProperty){
_init();
}
}
: 트리에서 State 객체가 제거될 때 호출된다. 프레임워크가 제거된 State 객체를 트리의 다른 부분에 다시 삽입하는 경우가 있는데, 이때는 build 메소드를 한 번 호출한다.
: State 객체가 제거됐다고 해서 해당 메모리까지 지워지는 것은 아니기 때문에, 위에서 말했듯이 build 메소드를 통해 다시 호출이 가능하며, dispose() 함수를 호출하기 전까지는 State 객체를 재사용할 수 있다.
: 트리에서 State 객체가 영구적으로 제거될 때 호출된다. 이 메소드 내에서 setState를 호출하면 안된다.
❌ 위젯을 화면에서 제거하면 mounted == false가 되는데, State 객체가 소멸하면 마지막으로 mounted 속성이 false로 바뀌면서 생명주기가 끝난다. mounted 속성이 false가 되었다는 것은 이 State는 재사용할 수 없다는 의미이다.
: hot reload 실행 시 호출되며, build도 같이 호출된다.
https://velog.io/@tmdgks2222/Flutter-State
https://juntcom.tistory.com/304
https://monocsp.dev/17https://velog.io/@hoha/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%9C%84%EC%A0%AF%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0

부모 Widget에서 자식 Widget으로 State 전달하는 방법은 다음과 같은 3단계로 진행한다.
1. 부모 Widget에서 자식 Widget으로 State 보내기
2. 자식 Widget에서 전달받을 state를 class member 변수로 등록하기
3. 자식 Widget에서는 전달받은 state의 ~class 변수를 사용하기

Flutter Application 생성 시 만든 코드를 예를 들면 다음과 같다. MyApp(=부모 Widget)에서 MyHomePage(=자식 Widget)으로 title 이름의 state에 └'Flutter Demo Home Page'값으로 parameter를 전달한다.
MyHomePage(title:'Flutter Demo Home Page'),
MyHomePage(=자식 Widget)에서는 class member에 title을 등록한다. 일반적으로 부모로 전달받은 state는 자식 Widget에서 값을 변경하지 않기 때문에 final 변수를 사용한다.
class MyHomePage extends StatefulWidget{
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
...
}
MyHomePage의 State를 관리하는 _MyHomePageState에서는 MyHomePage의 class member를 사용하기 위해서 widget.title 값으로 접근해서 사용한다. widget이란 keyword를 통해서 MyHomePage class member을 접근 가능에 대해 주의해줘야한다.
class _MyHomePageState extends State<MyHomePage>{
...
@override
Widget build(BuildContext context){
return Scaffold(
appBar:AppBar(
title: Text(widget.title),
),
body:Center(
....
}
}
자식 Widget에서 부모의 State를 직접 접근은 불가능하며, 대신 부모 Widget에서 State 처리 함수를 자식 Widget으로 전달하고 이를 호출하는 방식을 사용해야 한다.
(1) Parent Widget : State 처리 함수 생성, State 처리 함수는 내부 변수를 직접 접근 가능하면 Rendering을 다시 하기 위해서는 setState() 함수를 반드시 호출해야 한다.
(2) Parent Widget : State 처리 함수를 Child Widget으로 function parameter로 전달
(3) Child Widget : Parent Widget에서 전달한 function parameter를 class member로 등록
(4) Child Widget : Parent Widget에서 전달받은 function을 실행

예시)
Parent Widget에서 _counter 값을 선언하고 setState() 함수를 호출하는 _incrementCounter() 함수를 생성한다.
class _MyHomePageState extends State<MyHomePage>{
int _counter = 0;
void _incrementCounter(){
setState((){
_counter++;
});
}
}
Parent Widget에서 Child Widget인 SampelChildWidget으로 notifyParent 이름으로 _incrementCounter() 함수를 전달한다.
SampelChildWidget(notifyParent: _incrementCounter)
ChildWidget에서는 ParentWidget인 MyHomePage에서 전달한 notifyParent 인자 값을 선언하고 받는다.
class SampleChildWidget extends StatefulWidget{
const SampleChildWidget({Key? key, required this.notifyParent}) : super(key: key);
final Function() notifyParent;
}
ChildWidget에서 전달받은 notifyParent 함수를 onPress Event를 받아서 호출한다.
class _SampleChildWidgetState extends State<SampleChildWidget>{
@override
Widget build(BuildContext context){
return ElevatedButton(
onPressed: widget.notifyParent,
child: const Text('Child Widget'),
);
}
}
https://medium.com/swlh/the-simplest-way-to-pass-and-fetch-data-between-stateful-and-stateless-widgets-pages-full-2021-c5dbce8db1db
https://kibua20.tistory.com/232
: 상태 관리는 UI에서 실시간으로 변하는 여러 데이터들의 상태를 효율적으로 관리하기 위한 개념이다.

예시로 가져온 위의 화면을 보면 글쓴이의 프로필 이미지, 닉네임, 글의 작성 시간, 카테고리, 좋아요, 댓글 갯수 등등 한 화면에도 여러 데이터들이 있는 것을 확인할 수 있다. 이 화면에서 상태관리가 필요한 이유는 크게 두 가지가 있다.
1번의 예시 )
사용자가 댓글을 입력하고 업로드 하는 순간 댓글 목록에 새로운 댓글이 보여져야 한다. 화면의 관점에서 새로운 데이터가 생기고 그에 따라서 새로운 UI를 그려줘야하기 때문에 Flutter는 StatefulWidget으로 해당 화면을 재랜더링 한다.
그 외에도 글이나 댓글의 하트를 누를 경우에도, 댓글을 삭제, 수정할 때도 Flutter는 화면의 일부를 변경하기 위해서 화면 전체를 재랜더링 하게 된다.
✅ 하지만 화면의 일부분의 변경을 적용하기 위해 화면 전체를 재랜더링 하는 방식은 너무 비효율적이다.
2번의 예시 )
사용자가 해당 글에 하트를 눌러서 하트 UI가 노란색 하트로 변경이 되었다. 만약에 해당 글에 하트를 눌렀는지의 여부 데이터를 다른 페이지에서도 참고하고 있다면 해당 페이지에서도 하트가 노란색 하트로 변경이 되어야한다.
✅ Flutter가 default로 제공하는 StatefulWidget을 통해서도 기능 구현은 되지만 애플리케이션이 복잡해질수록 setState()로 전체 화면을 재랜더링 하는 방식은 비효율성이 애플리케이션 규모에 비례해 더 커지게 된다.
➡️ 상태 관리 기술을 사용하게 되면 1번처럼 실시간으로 변화하는 데이터에 대한 처리와 2번처럼 여러 컴포넌트에서 공통적으로 사용하는 데이터의 동기화를 아주 쉽고 효율적으로 해결할 수 있다.
라이브러리 import
$ flutter pub add get
라이브러리 동기화
$ flutter pub get
라이브러리 사용
import 'package:get/get.dart';
라이브러리를 사용하는 dart 파일에 import하기
void main() {
runApp(const GetMaterialApp(home: MyApp()));
}
단순 상태 관리와 반응형 상태관리의 차이는 반응형 상태 관리의 경우 데이터가 변화가 있을 때만 재랜더링을 하게 되는 반면에 단순 상태관리는 기존의 데이터와 변경되는 데이터가 같은지 확인하지 않는다.
import 'package:get/get.dart';
class SimpleController extends GetxController {
int counter = 0;
void increase() {
counter++;
update();
}
}
➡️ 단순 상태 관리를 위한 controller를 만들어준다. 이 controller는 counter라는 변수와 counter의 값을 1씩 증가해주는 increase() 함수를 가지고 있다. increase() .함수 안에 update()는 이 controller을 바라보고있는 모든 코드에 업데이트를 알리는 역할을 한다.
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(SimpleController()); // controller 등록
return Scaffold(
appBar: AppBar(
title: const Text("단순 상태관리"),
),
body: Center(
child: GetBuilder<SimpleController>( // 실시간 렌더링
builder: (controller) {
return ElevatedButton(
child: Text(
'현재 숫자: ${controller.counter}',
),
onPressed: () {
controller.increase();
// Get.find<SimpleController>().increase();
},
);
},
),
),
);
}
}
➡️ 위에서 만들어준 controller를 사용하는 화면 클래스이다. 먼저 controller를 사용하기 위해 Get.put()으로 controller를 등록해준다. GetBuilder()아래의 모든 위젯은 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다.
controller.counter는 controller의 변수를 실시간으로 반영하게 되고 controller.increase()는 controller의 counter 데이터를 실시간으로 증가시키게 된다. 만약 GetBuilder를 사용하지 않을 경우 Get.find<[Controller종류]>().[변수 혹은 함수] 로 컨트롤러의 데이터를 실시간 변경 혹은 반영할 수 있다.
반응형 상태관리는 workers라는 추가 기능이 있다. 아래 예제 코드와 함께 반응형 상태 관리 코드도 살펴보도록 하자.
import 'package:get/get.dart';
class ReactiveController extends GetxController {
RxInt counter = 0.obs;
void increase() {
counter++;
}
}
➡️ 반응형 상태관리를 위한 Controller이다. 이 Controller는 counter라는 변수와 counter의 값을 1씩 증가해주는 increase() 함수를 가지고 있다.
단순 상태관리와 비교하면 변수를 선언하는 방식과 업데이트 함수 부분이 다르다. 변수를 선언하는 방식은 변수의 타입을 RxInt, RxString 등 Rx{타입}의 방식으로 선언하고 변수의 값은 .obs를 붙이게 된다. 업데이트의 경우 update() 함수를 부르지 않아도 된다.
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Get.put(SimpleController()); // 단순 상태 관리 controller 등록
Get.put(ReactiveController()); // 반응형 상태 관리 controller 등록
return Scaffold(
appBar: AppBar(
title: const Text("단순 / 반응형 상태관리"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<SimpleController>( // 단순 상태 관리
builder: (controller) {
return ElevatedButton(
child: Text(
'[단순]현재 숫자: ${controller.counter}',
),
onPressed: () {
controller.increase();
// Get.find<SimpleController>().increase();
},
);
},
),
GetX<ReactiveController>( // 반응형 상태관리 - 1
builder: (controller) {
return ElevatedButton(
child: Text(
'반응형 1 / 현재 숫자: ${controller.counter.value}', // .value 로 접근
),
onPressed: () {
controller.increase();
// Get.find<ReactiveController>().increase();
},
);
},
),
Obx( // 반응형 상태관리 - 2
() {
return ElevatedButton(
child: Text(
'반응형 2 / 현재 숫자: ${Get.find<ReactiveController>().counter.value}', // .value 로 접근
),
onPressed: () {
Get.find<ReactiveController>().increase();
},
);
},
),
],
),
),
);
}
}
➡️ 단순 상태 관리를 테스트했던 화면에 그대로 반응형 상태관리 테스트를 위한 위젯을 추가한 코드이다.
먼저 단순 상태 관리와 동일하게 controller를 사용하기 위해 Get.put()으로 controller를 등록해준다. 반응형 상태 관리에서 데이터를 실시간으로 반영하는 방식에는 두가지가 있다.
GetX() 아래의 모든 위젯은 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다. controller.counter.value (단순 상태 관리와 다르게 .value 를 추가해 주어야 합니다) 는 controller의 변수를 실시간으로 반영하게 되고 controller.increase()는 controller의 counter 데이터를 실시간으로 증가시키게 된다. 만약 GetX를 사용하지 않을 경우 Get.find<[Controller종류]>().[변수 혹은 함수] 로 컨트롤러의 데이터를 실시간 변경 혹은 반영할 수 있다.
Obx() 아래의 모든 위젯은 GetX()와 마찬가지로 controller에서 변경되는 데이터를 실시간으로 반영할 수 있는 상태가 된다. 사용 방식은 거의 동일하지만 차이가 있다면 GetX()와 달리 controller의 이름을 지정할 수가 없어서 Get.find() 방식으로 접근해야 한다.반응형 상태관리에서는 worker라는 추가 기능이 있따. Worker는 controller 안에 onInit() 함수를 override하고 그 안에 추가해서 사용하게 되는데 아래의 4가지 종류가 있다.
import 'package:get/get.dart';
class ReactiveController extends GetxController {
static ReactiveController get to => Get.find();
RxInt counter = 0.obs;
@override
void onInit() {
once(counter, (_) {
print('once : $_이 처음으로 변경되었습니다.');
});
ever(counter, (_) {
print('ever : $_이 변경되었습니다.');
});
debounce(
counter,
(_) {
print('debounce : $_가 마지막으로 변경된 이후, 1초간 변경이 없습니다.');
},
time: Duration(seconds: 1),
);
interval(
counter,
(_) {
print('interval $_가 변경되는 중입니다.(1초마다 호출)');
},
time: Duration(seconds: 1),
);
super.onInit();
}
void increase() {
counter++;
}
}
class SimpleController extends GetxController {
static SimpleController get to => Get.find();
...
}
// 전
Get.find<SimpleController>().increase();
// 후
SimpleController.to.increase();
// 전
class SimpleState extends StatelessWidget{}
// 후
class SimpleState extends GetView<SimpleController>{}
// 전
Get.find<SimpleController>().increase();
// 후
controller.increase();
https://danawalab.github.io/flutter/2022/08/05/Flutter-Getx.html
Flutter에 대한 간단한 개념들을 정리하고 공부해보았다. GetX를 사용하는 방식이라던가 상태 관리에 대해서 개념을 이해하고 Stateless Widget과 Stateful Widget의 차이를 알고 부모 Widget과 자식 Widget사이의 정보 전달 매커니즘을 이해하게 되었다.