Provider를 통해 상태관리 전환

이재영·2024년 8월 24일

Provider라는 상태관리 라이브러리를 사용해보고 싶어, 기존 프로젝트에서 단일 페이지의 상태 변화에 따른 UI 업데이트 로직을 Provider를 사용해서 수정해보았다.

사실, 이번에 적용시킨 상태 관리를 통해서는 성능 향상을 기대할 수는 없지만, 한번 기존 프로젝트에 적용시켜보고 싶은 것도 있었고, 로직을 분리하여 추후에 단위 테스트나 유지보수 측면에서 이점이 있지 않을까 하는 마음에서 해보았다.


2024.10.8 Provider 추가 적용 +

Provider 두번째 적용 - API 호출에 따른 ListView 리빌드


main.dart에 해당 provider를 등록하여 앱 전체에서 접근가능하게 하여, Consumer를 사용하지 않고, context.watch/read 방식을 사용하여 구성해보았다.


기존 코드

import 'package:diet_macro/components/bottom_modal_sheet.dart';
import 'package:diet_macro/components/nutrition_tile.dart';
import 'package:diet_macro/models/isar_data.dart'; // isar_data.dart import
import 'package:diet_macro/models/isar_service.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';

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

  
  State<DietPage> createState() => _DietPageState();
}

class _DietPageState extends State<DietPage> {
  TargetData targetData = TargetData(); // Initialize with default values
  DailyData dailyData = DailyData(); // Initialize with default values
  bool loading = true; // Add a loading state

  double get _caloriesPercent => _calculatePercent(dailyData.calories, targetData.targetCalories);
  double get _carbPercent => _calculatePercent(dailyData.carb, targetData.targetCarb);
  double get _proteinPercent => _calculatePercent(dailyData.protein, targetData.targetProtein);
  double get _fatPercent => _calculatePercent(dailyData.fat, targetData.targetFat);

  
  void initState() {
    super.initState();
    _loadData();
  }

  // Isar 데이터 로드
  Future<void> _loadData() async {
    targetData = await IsarService().getTargetData() ?? TargetData();
    dailyData = await _getDailyDataForToday();

    // D E B U G
    print("DailyData: ${dailyData.calories}칼로리, ${dailyData.carb}탄수화물, ${dailyData.protein}단백질, ${dailyData.fat}지방");

    // 데이터 로드 후 UI 업데이트
    setState(() {
      loading = false;
    });
  }

  // 오늘 날짜의 DailyData 가져오기
  Future<DailyData> _getDailyDataForToday() async {
    final now = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
    final existingData = await IsarService().getDailyDataByDate(now);

    if (existingData != null) {
      // 기존 데이터가 있을 경우, 기존 데이터에 더하기
      existingData.calories += dailyData.calories;
      existingData.carb += dailyData.carb;
      existingData.protein += dailyData.protein;
      existingData.fat += dailyData.fat;

      // 업데이트된 데이터를 저장
      await IsarService().updateDailyData(existingData);
      return existingData;
    }

    return DailyData(); // 기존 데이터가 없으면 새로운 DailyData 반환
  }

  // 진행률 계산 함수
  double _calculatePercent(int current, int target) {
    if (current >= target) {
      return 1;
    } else {
      return 1 - (target - current) / target;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[300],
      floatingActionButton: FloatingActionButton(
          tooltip: 'Add Nutrion',
          backgroundColor: Colors.black,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(50),
          ),
          child: const Icon(Icons.add, color: Colors.white),
          onPressed: () {
            showModalBottomSheet(
              constraints: BoxConstraints(
                maxHeight: MediaQuery.of(context).size.height * 0.65, // 화면 높이의 80%
              ),
              isScrollControlled: true,
              context: context,
              builder: (context) {
                return BottomModalSheet(
                  onAddPressed: (calories, carb, protein, fat) async {
                    // 콜백 함수에서 Isar 데이터 업데이트
                    setState(() {
                      dailyData
                        ..calories += calories
                        ..carb += carb
                        ..protein += protein
                        ..fat += fat;
                      print(
                          "현재 날짜의 DailyData 업데이트된 값: ${dailyData.calories}칼로리, ${dailyData.carb}���수화물, ${dailyData.protein}단백질, ${dailyData.fat}지방");
                      print("날짜: ${dailyData.date}");

                      IsarService().updateDailyData(dailyData);

                      // 팝업 메시지 표시
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('Successfully added your macro!'),
                          duration: const Duration(seconds: 2),
                        ),
                      );
                    });
                  },
                );
              },
            );
          }),
      body: loading
          ? const Center(child: CircularProgressIndicator()) // Show loading indicator while loading
          : Padding(
              padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 15),
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        // Calories
                        _buildCircularPercentIndicator(
                            _caloriesPercent, Colors.red.shade600, "${dailyData.calories}", 'Calories'),
                        const SizedBox(width: 20),
                        // Carb
                        _buildCircularPercentIndicator(
                            _carbPercent, Colors.green.shade600, "${dailyData.carb} g", 'Carb'),
                      ],
                    ),
                    const SizedBox(
                      height: 40,
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        // Protein
                        _buildCircularPercentIndicator(
                            _proteinPercent, Colors.blue.shade800, "${dailyData.protein} g", 'Protein'),
                        const SizedBox(
                          width: 20,
                        ),
                        // Fat
                        _buildCircularPercentIndicator(_fatPercent, Colors.brown.shade600, "${dailyData.fat} g", 'Fat'),
                      ],
                    ),
                    const SizedBox(height: 50),
                    NutritionTile(
                      dividerColor: Colors.red.shade600,
                      nutrition: "Calories",
                      status: '${dailyData.calories} / ${targetData.targetCalories} kcal',
                      percentage: (dailyData.calories / targetData.targetCalories * 100).toInt(),
                    ),
                    const SizedBox(height: 20),
                    NutritionTile(
                      dividerColor: Colors.green.shade600,
                      nutrition: "Carb",
                      status: '${dailyData.carb} / ${targetData.targetCarb} g',
                      percentage: (dailyData.carb / targetData.targetCarb * 100).toInt(),
                    ),
                    const SizedBox(height: 20),
                    NutritionTile(
                      dividerColor: Colors.blue.shade800,
                      nutrition: "Protein",
                      status: '${dailyData.protein} / ${targetData.targetProtein} g',
                      percentage: (dailyData.protein / targetData.targetProtein * 100).toInt(),
                    ),
                    const SizedBox(height: 20),
                    NutritionTile(
                      dividerColor: Colors.brown.shade600,
                      nutrition: "Fat",
                      status: '${dailyData.fat} / ${targetData.targetFat} g',
                      percentage: (dailyData.fat / targetData.targetFat * 100).toInt(),
                    ),
                  ],
                ),
              ),
            ),
    );
  }

  // CircularPercentIndicator를 생성하는 함수
  Widget _buildCircularPercentIndicator(double percent, Color progressColor, String centerText1, String centerText2) {
    return CircularPercentIndicator(
      radius: 52.0,
      lineWidth: 13,
      animation: true,
      animationDuration: 1000,
      backgroundColor: const Color.fromARGB(100, 255, 255, 255),
      percent: percent,
      center: Column(
        mainAxisAlignment: MainAxisAlignment.center, // 텍스트 세로 가운데 정렬
        children: [
          Text(
            centerText1, // 첫 번째 줄 텍스트
            style: GoogleFonts.roboto(
              fontWeight: FontWeight.w500,
              fontSize: 16,
            ),
          ),
          Text(
            centerText2, // 두 번째 줄 텍스트
            style: GoogleFonts.roboto(
              fontWeight: FontWeight.w300,
              fontSize: 12,
            ),
          ),
        ],
      ),
      progressColor: progressColor,
      circularStrokeCap: CircularStrokeCap.round,
    );
  }
}

DietProvider 작성

import 'package:diet_macro/models/isar_data.dart';
import 'package:diet_macro/services/isar.service.dart';
import 'package:flutter/material.dart';

class DietProvider with ChangeNotifier {
  TargetData targetData = TargetData(); // Initialize with default values
  DailyData dailyData = DailyData(); // Initialize with default values
  bool loading = true; // Add a loading state

  double get caloriesPercent => _calculatePercent(dailyData.calories, targetData.targetCalories);
  double get carbPercent => _calculatePercent(dailyData.carb, targetData.targetCarb);
  double get proteinPercent => _calculatePercent(dailyData.protein, targetData.targetProtein);
  double get fatPercent => _calculatePercent(dailyData.fat, targetData.targetFat);

  // Isar 데이터 로드
  Future<void> loadData() async {
    targetData = await IsarService().getTargetData() ?? TargetData();
    dailyData = await _getDailyDataForToday();

    // D E B U G
    print("DailyData: ${dailyData.calories}칼로리, ${dailyData.carb}탄수화물, ${dailyData.protein}단백질, ${dailyData.fat}지방");

    loading = false;

    notifyListeners();
  }

  Future<DailyData> _getDailyDataForToday() async {
    final now = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
    final existingData = await IsarService().getDailyDataByDate(now);

    if (existingData != null) {
      // 데이터베이스에 기존 데이터가 있으면 기존 데이터를 반환
      return existingData;
    }

    // 데이터베이스에 기존 데이터가 없으면 새로운 DailyData 객체를 생성
    return DailyData();
  }

  // 진행률 계산 함수
  double _calculatePercent(int current, int target) {
    if (current >= target) {
      return 1;
    } else {
      return 1 - (target - current) / target;
    }
  }

  // DailyData 업데이트
  void updateDailyData(int calories, int carb, int protein, int fat) {
    dailyData
      ..calories += calories
      ..carb += carb
      ..protein += protein
      ..fat += fat;
    IsarService().updateDailyData(dailyData);

    notifyListeners();
  }
}


수정 후 문제점 ?

위젯이 업데이트 될때 기존 데이터가 계속 더해져 중첩되는 에러가 발생하였다...
위 문제의 이유는 DietProviderdailyData 변수를
_getDailyDataForToday() 함수에서 직접 사용했기 때문이다.



데이터 가져오기 메서드

수정 전

Future<DailyData> _getDailyDataForToday() async {
    final now = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
    final existingData = await IsarService().getDailyDataByDate(now);

    if (existingData != null) {
      // 기존 데이터가 있을 경우, 기존 데이터에 더하기
      existingData.calories += dailyData.calories;
      existingData.carb += dailyData.carb;
      existingData.protein += dailyData.protein;
      existingData.fat += dailyData.fat;

      // 업데이트된 데이터를 저장
      await IsarService().updateDailyData(existingData);
      return existingData;
    }

    return DailyData(); // 기존 데이터가 없으면 새로운 DailyData 반환
  }

수정 후

Future<DailyData> _getDailyDataForToday() async {
    final now = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
    final existingData = await IsarService().getDailyDataByDate(now);

    if (existingData != null) {
      // 데이터베이스에 기존 데이터가 있으면 기존 데이터를 반환
      return existingData;
    }

    // 데이터베이스에 기존 데이터가 없으면 새로운 DailyData 객체를 생성
    return DailyData();
  }

profile
how to define. how to solve.

0개의 댓글