[Flutter] 8. Stateful & Rebuild

문정준's avatar
May 29, 2025
[Flutter] 8. Stateful & Rebuild

1. Stateful in Flutter

  • Flutter에서는 상태에 대한 기준을 그림을 Rebuild할 때의 상태 변화로 봄
    • 상태가 변할 수 있으면 Stateful : 부모가 자식의 상태를 관리함 (Listening)
    • 상태가 불변이면 Stateless
      • 상태가 없을 경우도 Stateless이지만, 상태가 존재해도 변하지 않으면 Stateless
    • 상태의 변화를 반영하기 위해서는 setter가 필요함
      • 화면을 Rebuild해서 상태를 반영할 수 있는 함수인 setState()
        • StatefulWidget에서 제공
    • setState는 해당 화면 전체를 Rebuild
      • 모든 화면을 다시 빌드할 경우 자원 소모량이 많아져 성능이 저하됨
      • 특정 화면(context)만을 골라서 Rebuild시켜야 비용이 줄어듬
        • 행위와 상태를 따로 만들어서 서로 전달시킬 수는 없음 : 부모 측에서 관리해야 함
      • 이를 해결하기 위한 방법
          1. const 활용 : 메모리에 new한 객체를 그대로 재사용
          1. 중간 객체 사용 : 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라고 함
notion image
  • 이와 같은 경우, 결국 부모 클래스를 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(상수)로 선언되어야 함 : 변수도 받을 수 없음
    • 내부 상태가 다 고정된 값이어야 함
notion image
 
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가 복잡해질 경우 유용하게 사용 가능
notion image
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
notion image
 
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

sxias