1. Stateful in Flutter
- Flutter에서는 상태에 대한 기준을 그림을 Rebuild할 때의 상태 변화로 봄
- 상태가 변할 수 있으면 Stateful : 부모가 자식의 상태를 관리함 (Listening)
- 상태가 불변이면 Stateless
- 상태가 없을 경우도 Stateless이지만, 상태가 존재해도 변하지 않으면 Stateless
- 상태의 변화를 반영하기 위해서는 setter가 필요함
- 화면을 Rebuild해서 상태를 반영할 수 있는 함수인 setState()
- StatefulWidget에서 제공
- setState는 해당 화면 전체를 Rebuild
- 모든 화면을 다시 빌드할 경우 자원 소모량이 많아져 성능이 저하됨
- 특정 화면(context)만을 골라서 Rebuild시켜야 비용이 줄어듬
- 행위와 상태를 따로 만들어서 서로 전달시킬 수는 없음 : 부모 측에서 관리해야 함
- 이를 해결하기 위한 방법
- const 활용 : 메모리에 new한 객체를 그대로 재사용
- 중간 객체 사용 : Rebuild할 객체만을 따로 모으는 StatefulWidget 생성 (가짜 부모)
2. StatefulWidget
- 내부 상태가 변하는 (Mutable) 위젯
- 상태를 반영할 수 있는 setter인 setState() 제공
- setState를 사용하여 화면을 Rebuild할 수 있음
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomePage());
}
}
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// state
int num = 1;
// method
void increase() {
num++;
print("num : $num");
setState(() {});
}
@override
Widget build(BuildContext context) {
print("rebuild 됨");
return Scaffold(
appBar: AppBar(),
body: Center(child: Text("${num}", style: TextStyle(fontSize: 50))),
floatingActionButton: FloatingActionButton(child: Icon(Icons.add), onPressed: increase),
);
}
}
3. setState & Context
- 화면을 Rebuild하는데, 다시 불러오지 않아도 될 화면도 Rebuild를 하기 때문에 자원 낭비의 가능성
- Header와 Bottom의 Context를 분리하여 따로 지정해야 필요한 곳만의 Rebuild가 가능
- 클래스를 따로 분리해야 함
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("rebuilded");
return Container(
color: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Expanded(
child: Header(),
),
Expanded(
child: Bottom(),
),
],
),
),
);
}
}
class Bottom extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Align(
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {},
child: Text(
"증가",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
),
),
),
),
);
}
}
class Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Align(
child: Text(
"1",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
decoration: TextDecoration.none,
),
),
),
);
}
}
4. Widget Tree
- context를 분리한다 하더라도, 각기 다른 클래스에서 context를 주고받을 수 없음
- 부모 클래스에서 변수 및 함수를 대입해줘야 함
- 자식이 다른 자식을 관여할 수 없음 : 이와 같은 구조를 Widget Tree라고 함

- 이와 같은 경우, 결국 부모 클래스를 Rebuild하므로 궁극적인 문제 해결은 불가
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int num = 1;
void increase() {
num++;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(child: HeaderPage(num)),
Expanded(child: MiddlePage(1)),
Expanded(child: BottomPage(increase)),
],
),
),
);
}
}
class HeaderPage extends StatelessWidget {
int num;
HeaderPage(this.num);
@override
Widget build(BuildContext context) {
print("header");
return Container(
color: Colors.red,
child: Align(
child: Text(
"${num}",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
decoration: TextDecoration.none,
),
),
),
);
}
}
class BottomPage extends StatelessWidget {
Function increase;
BottomPage(this.increase);
@override
Widget build(BuildContext context) {
print("bottom");
return Container(
color: Colors.blue,
child: Align(
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
print("버튼 클릭됨");
increase();
},
child: Text(
"증가",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
),
),
),
),
);
}
}
class MiddlePage extends StatelessWidget {
final num;
const MiddlePage(this.num);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
);
}
}
5. const & final
- 부모 클래스를 Rebuild해도 Rebuild를 하지 않게끔 막는 방법 중 하나는 const 클래스 선언
- const : constant class (immutable class) 선언, 클래스 내부 멤버가 바뀌지 않음
- new되어 heap에는 올라오지만, Rebuild될 때 heap의 객체를 그대로 사용
- Rebuild를 해도 새로 new되지 않으므로 메모리 사용량이 줄어듬
- const class는 내부 멤버가 final(상수)로 선언되어야 함 : 변수도 받을 수 없음
- 내부 상태가 다 고정된 값이어야 함

import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int num = 1;
void increase() {
num++;
setState(() {});
}
@override
Widget build(BuildContext context) {
print("노란색");
return Container(
color: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(child: HeaderPage(num)),
// const : rebuild하지 않음 -> heap에 떠있는 객체 그대로 사용
// const 객체에는 변할 수 있는 상태(변수)를 받을 수 없음
Expanded(child: const MiddlePage(1)),
Expanded(child: const MiddlePage(1)),
Expanded(child: BottomPage(increase)),
],
),
),
);
}
}
class HeaderPage extends StatelessWidget {
int num;
HeaderPage(this.num);
@override
Widget build(BuildContext context) {
print("header");
return Container(
color: Colors.red,
child: Align(
child: Text(
"${num}",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
decoration: TextDecoration.none,
),
),
),
);
}
}
class BottomPage extends StatelessWidget {
Function increase;
BottomPage(this.increase);
@override
Widget build(BuildContext context) {
print("bottom");
return Container(
color: Colors.blue,
child: Align(
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
print("버튼 클릭됨");
increase();
},
child: Text(
"증가",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
),
),
),
),
);
}
}
class MiddlePage extends StatelessWidget {
final int num;
const MiddlePage(this.num);
@override
Widget build(BuildContext context) {
print("middle $num");
print("middle ${this.hashCode}");
return Container(
color: Colors.white,
);
}
}
6. Fake Parent Class
- Rebuild할 클래스만 따로 모아서 가짜 부모를 만듦
- StatefulWidget의 역할만 수행하고, 내부의 고유 로직을 포함하지 않음
- 단순히 변수 및 함수의 전달만을 목적으로 사용
- Widget Tree가 복잡해질 경우 유용하게 사용 가능

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("홈페이지 빌드");
return Scaffold(
body: Middle(),
);
}
}
// 가짜 부모 << 혼자만 Stateful 이다
class Middle extends StatefulWidget {
const Middle({
super.key,
});
@override
State<Middle> createState() => _MiddleState();
}
class _MiddleState extends State<Middle> {
int num = 1;
void increase() {
num++;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Top(num),
),
Expanded(
child: Bottom(increase),
),
],
);
}
}
class Bottom extends StatelessWidget {
Function increase;
Bottom(this.increase);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: ElevatedButton(
onPressed: () {
increase();
},
child: Icon(Icons.add),
),
),
);
}
}
class Top extends StatelessWidget {
int num;
Top(this.num);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text("$num", style: TextStyle(fontSize: 50)),
),
);
}
}
7. RiverPod
- Observer 패턴을 활용하여 UI 외부에서 state를 저장하는 storage를 활용한 상태 저장 라이브러리
- Storage는 Provider를 통해 접근 가능하고, Publisher 또는 Subscriber의 역할에 따라 접근하는 객체가 다름
- Publisher : 창고 (Storage or View Model) 에 접근 : Provider.notifier → Storage
- Subscriber : state에 접근 : Provider → Storage → State
- Provider를 통해 만들어진 Storage는 Singleton : 한 번 만들어지면 중복으로 만들어지지 않음
- 하나의 state 및 storage를 공유
- Subscriber는 Provider를 read 또는 watch하여 state를 전달받음
- read : 한 번 받고 나서 연결을 끊음 : const
- watch : 연결을 계속 유지하며 state를 전송받음 : Stateful

import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. 창고 데이터 타입 (int면 필요 없음)
int num = 1;
// 2. 창고
class HomeVM extends Notifier<int> {
@override
// 창고가 만들어질 때 자동 초기화 메서드 (return하는 값을 state로 관리)
int build() {
print("state 초기화 됨");
return num;
}
void increase() {
state++;
// setState가 필요하지 않음
}
}
// 3. 창고 관리자
final homeProvider = NotifierProvider<HomeVM, int>(() {
print("창고 생성됨");
return HomeVM();
});
- Subscriber, Publisher 모두 Provider를 사용하기 위한 ref를 제공하는 ConsumerWidget을 사용
- Provider를 read/watch할 경우 Subscriber / Provider.notifier를 read할 경우 Provider
- ref.read/watch(homeProvider) → state : 필요한 곳에 바로 할당 가능
- ref.read(homeProvider.notifier) → HomeVM (창고) : 창고에서 메서드 호출 가능
import 'package:flutter/material.dart';
import 'package:flutter_prac_state/home_vm.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
print("homepage build");
return Container(
color: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Expanded(child: HeaderPage()),
Expanded(child: BottomPage()),
],
),
),
);
}
}
class HeaderPage extends ConsumerWidget {
HeaderPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
print("header build");
print("창고 생성 직전");
// yield
int model = ref.watch(homeProvider);
print("창고 생성 후");
return Container(
color: Colors.red,
child: Align(
child: Text(
"$model",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 100,
decoration: TextDecoration.none,
),
),
),
);
}
}
class BottomPage extends ConsumerWidget {
BottomPage();
@override
Widget build(BuildContext context, WidgetRef ref) {
print("bottom build");
print("1");
HomeVM vm = ref.read(homeProvider.notifier);
print("2");
return Container(
color: Colors.blue,
child: Align(
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
print("버튼 클릭됨");
vm.increase();
},
child: Text(
"증가",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100),
),
),
),
);
}
}
Share article