📌 Day 13: Provider 상태 관리 적용 테스트 (Counter 앱, 다크모드까지)

2025. 3. 6. 12:29같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지

728x90
반응형

 

📃 개요

 
⚠️ 이전 글(Day 12)에서 다룬 내용처럼 Flutter에서 setState()만으로 상태를 관리하면 코드가 복잡해지고, 여러 위젯에서 상태를 공유하기 어렵습니다.
⚠️ Provider를 사용하면 setState() 없이도 여러 위젯에서 상태를 공유하고 효율적으로 관리할 수 있습니다.
 
이번 글에서는 Provider를 적용하여 setState() 없이 상태를 관리하는 Counter 앱을 자세히 확인해 봅니다.
✅ 기존 하나의 페이지에서 동작했던 Counter 앱을 보너스(TodoList 앱) 처럼 구성해 봅니다.
ChangeNotifier를 활용해 상태를 저장하고, UI 업데이트를 자동으로 수행할 수 있습니다.
Consumer를 활용하여 특정 위젯만 상태가 변경될 때 리빌드할 수 있습니다.
 
 
 
 
 


🔔 주제

🔸 Provider 패턴을 활용하여 Counter 앱 구현
🔸 ChangeNotifier를 사용하여 상태 관리
🔸 Consumer를 활용하여 특정 위젯만 리빌드하기
🔸 context.watch(), context.read()를 활용한 상태 접근
 
 
 
 


1️⃣ 프로젝트 준비 (Provider 패키지 설치)

Provider를 사용하려면 패키지를 설치해야 합니다.
✅ 이미 설치되어 있다면 넘어갑시다.
 
📌 터미널에서 아래 명령어 실행

flutter pub add provider

 
📌 설치 완료 후 pubspec.yaml에 아래 내용이 추가되었는지 확인

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.2  # 2025-03-05 기준 최신

 
📌 프로젝트 구조

📂 counter_app_provider
 ├── main.dart  (앱의 진입점)
 ├── providers/counter_provider.dart  (Provider 상태 관리)
 ├── screens/counter_screen.dart  (카운터 화면)

 
 
 
 
 
 


2️⃣ Provider 등록 (ChangeNotifierProvider)

Provider를 앱에 등록해야 모든 위젯에서 상태를 공유할 수 있습니다.
main.dart에서 ChangeNotifierProvider를 사용하여 상태를 등록합니다.
 
📌 main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/counter_provider.dart';
import 'screens/counter_screen.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
            create: (context) => CounterProvider()), // Provider 등록
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: "Provider Counter App",
      theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
      home: const CounterScreen(),
    );
  }
}

🔔 코드 설명
✅ ChangeNotifierProvider를 사용하여 CounterProvider를 앱에 등록합니다.
✅ 이제 CounterProvider의 상태를 어디서든 사용할 수 있습니다.
 
 
 
 
 


3️⃣상태 클래스 (ChangeNotifier) 생성

상태를 저장하고 관리하는 클래스를 ChangeNotifier를 사용하여 만듭니다.
✅ 앱의 상태 즉, 이 서비스에서 다뤄야 하는 데이터(상태 변수) 및 데이터를 처리하는 로직을 만듭니다.
✅ 여기에서는 카운터에 사용될 변수, 게터, 그리고 증가 및 감소 시킬 함수를 적용합니다.
 
📌 counter_provider.dart

import 'package:flutter/material.dart';

class CounterProvider extends ChangeNotifier {
  int _counter = 0; // 상태 변수

  int get counter => _counter; // 상태 값을 가져오기 (게터)

  void increment() {
    _counter++; // 값 증가
    notifyListeners(); // UI 업데이트 요청
  }

  void decrement() {
    _counter--; // 값 감소
    notifyListeners(); // UI 업데이트 요청
  }
}

🔔 코드 설명
✅ _counter 변수는 현재 카운트 값을 저장합니다.
✅ int get counter => _counter 게터를 통해서 외부에서 접근하도록 해줍니다.
✅ increment()와 decrement() 함수에서 _counter 값을 변경하고 notifyListeners()를 호출하여 UI를 업데이트합니다.
 
 
 
 
 


4️⃣ Counter 화면 구현 (Provider 활용)

Provider의 상태를 사용하여 UI를 업데이트합니다.
context.watch()와 context.read()를 사용하여 상태를 가져옵니다.
 
📌 counter_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/counter_provider.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  CounterScreenState createState() => CounterScreenState();
}

class CounterScreenState extends State<CounterScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Provider Counter App")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              "현재 카운트",
              style: TextStyle(fontSize: 20),
            ),
            Text(
              "${context.watch<CounterProvider>().counter}", // 상태 값 가져오기
              style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().decrement(),
                  child: const Text("-1 감소"),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().increment(),
                  child: const Text("+1 증가"),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

🔔 코드 설명
✅ context.watch<CounterProvider>().counter → CounterProvider의 counter 값을 가져와 표시
✅ context.read<CounterProvider>().increment() → 버튼 클릭 시 increment() 실행
✅ context.read<CounterProvider>().decrement() → 버튼 클릭 시 decrement() 실행
 
 
 
 
 


5️⃣ Consumer를 활용한 UI 최적화

✅ Consumer를 사용하면 특정 위젯만 상태가 변경될 때 리빌드할 수 있습니다.
✅ watch()는 전체 위젯을 리빌드하지만, Consumer는 필요한 위젯만 리빌드합니다.
 
📌 counter_screen.dart 수정

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/counter_provider.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  CounterScreenState createState() => CounterScreenState();
}

class CounterScreenState extends State<CounterScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Provider Counter App")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              "현재 카운트",
              style: TextStyle(fontSize: 20),
            ),
            Consumer<CounterProvider>(
              builder: (context, provider, child) {
                return Text(
                  "${provider.counter}", // 상태 값 가져오기
                  style: const TextStyle(
                      fontSize: 40, fontWeight: FontWeight.bold),
                );
              },
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().decrement(),
                  child: const Text("-1 감소"),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().increment(),
                  child: const Text("+1 증가"),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

🔔 코드 설명
✅ Consumer를 사용하면 CounterProvider의 counter 값이 변경될 때만 Text 위젯이 업데이트됨
✅ 불필요한 리빌드를 줄여 앱 성능을 향상
 
 
 
 
 


📌 내용 요약

Provider를 사용하면 setState() 없이 상태를 관리할 수 있다.
ChangeNotifier를 사용하여 상태를 저장하고 notifyListeners()를 호출하면 UI가 업데이트된다.
context.watch<T>()는 상태를 감지하여 UI를 업데이트하고, context.read<T>()는 이벤트를 실행하는 데 사용된다.
Consumer를 사용하면 특정 위젯만 상태 변경 시 리빌드할 수 있어 성능을 최적화할 수 있다.
 
 
 
 
 


📌 보너스

🔸 다크/라이트 모드 변경 Provider 추가

🔻 Provider Counter App (with. Dark Mode) 테스트 결과


 
 
 
 
 


 
📌 프로젝트 구조

📂 counter_app_provider
 ├── main.dart  (앱의 진입점)
 ├── providers/counter_provider.dart  (Counter 상태 관리)
 ├── providers/theme_provider.dart  (Theme 상태 관리)
 ├── screens/counter_screen.dart  (카운터 화면)

 
📌 main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/counter_provider.dart';
import 'providers/theme_provider.dart'; // theme provider import 추가
import 'screens/counter_screen.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CounterProvider()), 
        ChangeNotifierProvider(create: (context) => ThemeProvider()), // Provider 등록 추가
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: "Provider Counter App",
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.light, // 🌞 라이트 모드
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark, // 🌙 다크 모드
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: context.watch<ThemeProvider>().themeMode,
      home: const CounterScreen(),
    );
  }
}

 
📌 theme_provider.dart

import 'package:flutter/material.dart';

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system; // 기본적으로 시스템 설정 따름

  ThemeMode get themeMode => _themeMode;

  void toggleTheme() {
    _themeMode =
        (_themeMode == ThemeMode.light) ? ThemeMode.dark : ThemeMode.light;
    notifyListeners();
  }
}

 
📌 counter_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/counter_provider.dart';
import '../providers/theme_provider.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  CounterScreenState createState() => CounterScreenState();
}

class CounterScreenState extends State<CounterScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Provider Counter App"),
        actions: [
          IconButton(
            icon: Icon(
              context.watch<ThemeProvider>().themeMode == ThemeMode.dark
                  ? Icons.dark_mode
                  : Icons.light_mode,
            ),
            style: ButtonStyle(),
            onPressed: () => context.read<ThemeProvider>().toggleTheme(),
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              "현재 카운트",
              style: TextStyle(fontSize: 20),
            ),
            Consumer<CounterProvider>(
              builder: (context, provider, child) {
                return Text(
                  "${provider.counter}", // 상태 값 가져오기
                  style: const TextStyle(
                      fontSize: 40, fontWeight: FontWeight.bold),
                );
              },
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().decrement(),
                  child: const Text("-1 감소"),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().increment(),
                  child: const Text("+1 증가"),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

 

728x90
반응형