inblog logo
|
jjack1
    Flutter

    [Flutter] flutter 상태 관리

    만들면서 배우는 플러터 앱 프로그래밍
    최재원's avatar
    최재원
    Jul 30, 2025
    [Flutter] flutter 상태 관리
    Contents
    1. 상태(객체의 상태)2. 불변 상태, 변하는 상태3. 상태가 변할 때 그림이 reload 될 때(stateless, stateful)4. context5. const6. 위젯 트리와 비용7. 상태 관리를 외부에서 하는 법

    1. 상태(객체의 상태)

    • 모든 객체는 상태를 가질 수 있다
    class HomePage extends StatelessWidget { int num = 1; @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }
    상태 변경 기본 공부 코드
    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> { // 1. 상태 int num = 1; // 2. 행위 void increase() { 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, ), ); } }

    2. 불변 상태, 변하는 상태

    Stateless

    class HomePage extends StatelessWidget { int num = 1; @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }

    Stateful

    class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; void increase() { num++; setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( body: Placeholder(), ); } }

    3. 상태가 변할 때 그림이 reload 될 때(stateless, stateful)

    • 상태가 있어도 불변이면 상태가 없는 것으로 친다
    • 상태가 변할 때 상태가 있는 것으로 친다
    import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp(); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 1. Page (화면 - Scaffold 가져야 한다) // 2. 클래스 (오브젝트 - 상태가 있다) // 3. StatelessWidget -> InMutableWidget(불변위젯) // -> 자식의 상태를 관리 하지 않는다 class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); // 내부적으로 자동으로 createState를 실행한다 } class _HomePageState extends State<HomePage> { // 1. 상태 int num = 1; // 2. 행위 void increase() { num += 1; print("num : $num"); setState(() {}); // 다시 _HomePageState를 new 해서 build를 새로 한는 것 } @override Widget build(BuildContext context) { print("빌드됨! : $num"); return Scaffold( body: Center(child: Text("$num", style: TextStyle(fontSize: 30))), floatingActionButton: FloatingActionButton( onPressed: increase, child: Icon(Icons.add), ), ); } }
    notion image
    notion image

    4. context

    • 화면 그 자체, 화면 데이터를 말함
    • 도화지
    • 각 위젯이 가지는 것
    notion image
    컨텍스트 분리 x
    • StatefulWidget 으로 상태를 관리함
    • setState() 를 실행해서 다시 build 함
    • 상태 변경을 하면 HomePage 전체가 다시 그려짐
    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 { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int num = 1; @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ Expanded( child: Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ), ), Expanded( child: Container( color: Colors.blue, child: Align( child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { num++; setState(() {}); }, child: Text( "증가", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, ), ), ), ), ), ), ], ), ), ); } }
     
    컨텍스트 분리o 상태 변경x
    • 위젯을 분리 했으나 상태 변경을 할 수 없음
    • 상태를 가지는 위젯과 상태를 변경하는 위젯이 분리되어 사용이 불가능함
    • 그림을 다시 그릴 수 없음
    • 상태를 변경하고 싶으면 상태를 가지는 쪽에 상태를 변경하는 방법이 있어야 함
    • 디자인이 망가짐
    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 StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.yellow, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ Expanded( child: Header(), ), Expanded( child: Bottom(), ), ], ), ), ); } } class Header extends StatefulWidget { @override State<Header> createState() => _HeaderState(); } class _HeaderState extends State<Header> { // 1. 상태 int num = 1; @override Widget build(BuildContext context) { return Container( color: Colors.red, child: Align( child: Text( "${num}", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 100, decoration: TextDecoration.none, ), ), ), ); } } 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, ), ), ), ), ); } }
     
    컨텍스트 분리o 상태 변경o 비용x(부모가 다시 그려짐)
    • 위젯을 분리했으나 상태 변경을 하려면 부모가 상태 관리를 하고 자식들에게 전달하는 방법을 사용해야 함
    • 상태 관리를 하면서 위젯을 분리하는데 성공 함
    • 그러나 결국 부모가 다시 build 하기 때문에 비용이 낭비됨
    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, ); } }
     
    const 를 사용해 부분을 다시 build 하지 않게 한다
    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)), 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, ); } }

    5. const

    코드
    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)), 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, ); } }
    • 불변이다
    • 고정 디자인
    • 다시 build 할 때 const 인 위젯은 다시 그리지 않는다
    • 동일한 위젯이 존재하면 재사용함 → 플러터는 재사용하지 않고 다시 만듦
      • 동일한 위젯을 찾아서 재사용하는 프로그램도 있음
      • 플러터는 찾는 과정이 낭비라고 생각해서 새로 만드는 것 같음

    사용 방법

    @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)), Expanded(child: const MiddlePage(1)), Expanded(child: const MiddlePage(1)), Expanded(child: BottomPage(increase)), ], ), ), ); }
    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, ); } }
    • 변수를 final 로 만들면 처음 초기화 되고 변하지 않는다

    6. 위젯 트리와 비용

    코드
    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)), ), ); } }
    • 페이크 트리를 하나 부모로 만들어서 사용

    a 에 상태를 표시하고 c 에 상태 변경 행위를 넘겨주면(fake ❌)

    • HomePage 가 다시 rebuild 되면서 전체 그림이 다시 그려짐
    • 관계 없는 다른 위젯도 다시 그려짐
    • 자원 낭비
    notion image

    a 에 상태를 표시하고 c 에 상태 변경 행위를 넘겨주면(fake ⭕)

    • fake 부모가 다시 rebuild 되면서 fake, a, c 만 다시 rebuild 됨
    • 필요한 부분만 변경 가능함
    • 자원 낭비가 줄어듦
    notion image

     
    notion image
    • 다음과 같은 트리 구조도 있기 때문에 완벽하게 자원을 사용했다고 할 순 없다
    • 그래도 b 트리는 다시 그려지지 않기 때문에 그나마 낫다

    7. 상태 관리를 외부에서 하는 법

    notion image
    • 화면을 그리는 프로그램은 모두 상태 관리 라이브러리를 사용한다
    • 옵저버 패턴으로 만들어졌다
    • 구독자, 제공자(퍼블리셔)
    • 퍼블리셔는 상태 관리 라이브러리에게 한번만 요청하면 된다 (초록선)
    • 구독자는 상태 관리 라이브러리에게 계속 연결 되어있어야 한다 (파란선)
      • 연결 방법
        • read → 한번 값을 받고 끝
        • watch → 계속 값을 받을 수 있음
     
    riverpod 세팅
    import 'package:flutter_riverpod/flutter_riverpod.dart'; // 1. 창고 데이터 타입(int 면 안 만들어도 됨) // 관리 타입은 1가지만 가능함 여러개면 클래스 타입 안에 넣어서 사용함 // 2. 창고 class HomeVM extends Notifier<int> { // 창고가 만들어질 때 초기화 메서드(return 하는 값을 창고가 state 로 관리함) @override int build() { return 1; } } // 3. 창고 관리자 final homeProvider = NotifierProvider<HomeVM, int>(() { return HomeVM(); });
    출처: 만들면서 배우는 플러터 앱 프로그래밍
     
    Share article
    Contents
    1. 상태(객체의 상태)2. 불변 상태, 변하는 상태3. 상태가 변할 때 그림이 reload 될 때(stateless, stateful)4. context5. const6. 위젯 트리와 비용7. 상태 관리를 외부에서 하는 법

    jjack1

    RSS·Powered by Inblog