Flutter의 생명주기를 알아보기로 하자.
생명주기의 사전적 의미는 개념 형성에서부터 사용 정지에 이르기까지의 발전상의 변화의 전 과정을 의미한다. 생명주기를 몰라도 앱 개발이 가능하지만 아는 것이 개발할 때 여러모로 유용하다.
State란 StatefulWidget에 대한 논리 및 내부 상태를 의미한다.
State의 개념은 두가지로 정의 된다.
한번 위젯을 생성하고 위젯의 구성요소와 속성들이 변화하지 않을 시 StatelessWidget이 사용된다. StatelessWidget은 build 될 때 build 메서드를 호출하기에 build 메서드를 신경써줘야한다.
StatefulWidget과 StatelessWidget의 가장 큰 차이는 State(상태)가 변화하냐 안하냐의 차이다. State(상태)가 지속적으로 변하거나 위젯의 구성요소들이 지속적으로 변한다면 StatefulWidget을 사용하여 변화하는 것을 계속 업데이트 되는 화면을 통해 보여줄 수 있다.
StatefulWidget의 생명주기는 총 8단계로 구분 됩니다.
state object(객체)를 생성한다. 이 object는 해당 Widget에 대한 모든 변경 가능한 state가 유지되는 곳이다. 이 method는 StatefulWidget 내부에서 필요하다.
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => _MyHomePageState();
}
State object를 생성하면, 프레임워크는 mounted라는 boolean 속성을 true로 설정해서, State object를 BuildContext와 연결한다. 이 속성은 이 State object가 위젯 트리에 있는지 없는지를 알려준다.
백그라운드에서 진행 되는 작업
위젯이 트리에 속하면 처음으로 호출되는 method다. initState()는 오직 한번만 호출 된다. 또한 반드시 super.initState()를 호출해야한다.
initState에서 실행되면 좋은 것들
- 생성된 위젯 인스턴스의 BuildContext에 의존적인 데이터 초기화
- 동일 위젯트리내에 부모위젯에 의존하는 속성 초기화
- Stream 구독, 알림변경, 또는 위젯의 데이터를 변경할 수 있는 다른 객체 핸들링
- HTTP request 관리
void initState() {
super.initState();
// TODO: implement initState
}
didChangeDependencies() 메서드는 위젯이 최초 생성될 때 iniState 다음에 바로 호출된다.
또한 위젯이 의존하는 데이터의 객체가 호출될 때마다 호출된다.
build 메서드는 항상 didChangeDependencies() 다음에 호출된다.
void didChangeDependencies(){
super.didChangeDependencies();
/*state 변경 시 호출*/
}
UI를 구현하는 부분으로, 이 메서드는 가장 많이 호출된다. 이 곳에 계산이 필요한 로직이 많이 존재하면 앱의 퍼포먼스는 현저히 낮아진다.
build() 메서드의 특징은 다음과 같다.
build(){
return Container(...)
}
Widget
didUpdateWidget() method는 부모 위젯이 구성을 변경하고, 위젯을 다시 build해야하는 경우에 호출된다. 프레임워크는 이전 위젯을 새 위젯과 비교하는데 사용할 수 있는 argument를 준다. Flutter는 didUpdateWidget() 이후에 build() method를 호출한다.
새 위젯과 이전 위젯을 비교할 때 유용함
setState() method는 자주 Flutter 프레임워크 자체와 개발자로부터 호출된다. setState() method는 현재 object 내부 상태가 "dirty"라는 것을 프레임워크에 알려준다. 즉, UI에 영향을 줄 수도 있는 방식으로 변경되었음을 의미한다. 이 알림 후에 프레임워크는 build() method를 호출해서 위젯을 업데이트하고 다시 build한다.
setState(() {
// implement setState
});
deactivate() method는 위젯 트리에서 위젯이 제거될 때 호출되지만, state가 위젯 트리의 한 지점에서 다른 지점으로 이동할 때, 현재 프레임 변경이 완료되기 전에 다시 주입될 수 있다. deactivate() method는 거의 사용되지 않는다.
void deactivate() {
super.deactivate();
// TODO: implement deactivate
}
dispose() method는 위젯 트리에서 state object가 영구적으로 제거될 때 호출된다.
void dispose() {
super.dispose();
// TODO: implement dispose
}
dispose() method 다음에는 State object가 현재 트리에 없으므로 mounted 속성은 이제 false다. state object는 다시 mount할 수 없고, setState()가 호출되면 에러가 발생한다.
아래 코드는 생명주기 예시 코드로, 참고하여 어떤 흐름을 갖고 있는지 확인해보면 될 것 같다.
// main.dart
import 'package:flutter/material.dart';
import './ScreenC.dart';
import './ScreenB.dart';
import './ScreenA.dart';
int addNum(int a, int b) {
return a + b;
}
void argumnet() {
addNum(3, 4);
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => ScreenA(),
'/b': (context) => ScreenB(),
'/c': (context) => ScreenC(),
},
);
}
}
// ScreenA.dart
import 'package:flutter/material.dart';
class ScreenA extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScreenA'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.blue,
),
child: Text('Go to ScreenB'),
onPressed: () {
// pushNamed의 첫 번째 인자 값인, context는 ScreenA 위젯의 context(위치)다, routeName인 '/b'는 route의 key값
Navigator.pushNamed(context, '/b');
},
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.blue,
),
child: Text('Go to ScreenC'),
onPressed: () {
Navigator.pushNamed(context, '/c');
},
)
],
),
),
);
}
}
// ScreenB.dart
import 'dart:developer';
import 'package:flutter/material.dart';
class ScreenB extends StatefulWidget {
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
// StatefulWidget을 생성해서, 이 위젯이 위젯트리에 삽입되자마자 곧바로 initState 메서드가 호출된다
// 그래서 만약 우리가 앱이 실행되자마자, 즉, StatefulWidget이 생성되는 순간에 무언가 기능을 구현하고 싶다면
// initState 메서드 내에서 구현을 해주거나,
// 무언가 필요한 메서드를 호출해주면 된다!
void initState() {
super.initState();
print('initState is called');
}
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies is called');
}
void setState(fn) {
super.setState(fn);
log('setState');
}
void didUpdateWidget(covariant ScreenB oldWidget) {
super.didUpdateWidget(oldWidget);
log('didUpdateWidget');
}
void deactivate() {
super.deactivate();
log('deactivate');
}
// dispose 메서드는 위젯이 위젯 트리에서 완전히 제거될 때 호출된다
void dispose() {
super.dispose();
print('dispose is called');
}
void reassemble() {
super.reassemble();
log('reassemble');
}
Widget build(BuildContext context) {
// StatefulWidget이 초기화되었다면 실제적으로 위젯을 build해야한다
print('build is called');
return Scaffold(
appBar: AppBar(
title: Text('ScreenB'),
),
body: Center(
child: ElevatedButton(
child: Text('Go to ScreenA'),
onPressed: () {
setState(() {
print('setState is called');
});
// pop 메서드에 위해 ScreenB 위젯이 파괴되고
// dispose 메서드가 실행되면서 print 메세지가 출력된다
Navigator.pop(context);
},
)),
);
}
}
// ScreenC.dart
import 'package:flutter/material.dart';
class ScreenC extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScreenC'),
),
body: Center(
child: Text(
'ScreenC',
style: TextStyle(fontSize: 24),
),
),
);
}
}