본문 바로가기
개발일지/GoodWishes

[굿위시 제작기] 9. 굿즈 추가 페이지 제작과정 - 카테고리 설정/관리 페이지

by GiraffePark 2024. 10. 19.


굿즈 분류(카테고리) 기록 state

굿즈 추가 페이지의 state 중에는 category가 있습니다.

굿즈를 유저가 직접 설정한 카테고리로 분류하는 부분입니다. 

 

 

 

 

[굿즈 추가 페이지 소스코드 주소 : https://arnopark.tistory.com/917#%EC%84%A0%ED%83%9D%EC%A7%80-%EC%A0%95%EB%8B%B5-2]

 

 

 

굿즈 추가 페이지에서, 사진의 부분을 담당하고 있습니다.

Tag()라는 커스텀 위젯을 사용하고 있는데, 현재 Category state로 지정된 값을 Text() 위젯으로 출력하며,

Tag()를 누르면 카테고리 관리 페이지로 이동하는 onNavigate 속성을 전달받습니다.

 

 

 

 


태그 버튼을 누르면 카테고리 관리 페이지로

   Future<void> categoryButtonHandler(BuildContext context) async {
      final pickedCategory = await Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) =>
              ChangeNotifierProvider<CategoryListProvider>.value(
            value: categoryList,
            builder: (context, child) {
              return const AddCategoryPage();
            },
          ),
        ),
      );
      if (!mounted) {
        return;
      }
      setState(() {
        category = pickedCategory;
      });
    }

onNavigate로 전달받는 핸들러 함수는 위와 같습니다.

 

 

 

굿즈 추가페이지는 CategoryListProvider를 통해, category 전역 state에 접근할 수 있습니다.

 

 

 

MaterialPageRoute를 이용해서 굿즈 관리 페이지(AddCategoryPage)로 이동할 때, 

Provider 전역 state를 접근할 수 있게 해주는 ChangeNotifierProvider()를 같이 전달해주고, 그때 정적 메소드인 value()에 아까 전달받은 category state를 AddCategoryPage와 함께 연결합니다.

 

이제 태그 버튼을 누르면, category state와 연동된 상태의 AddCategoryPage에 접속할 수 있습니다.

 

 

 

 


굿즈 관리 페이지 : AddCategoryPage

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/category_model.dart';
import 'package:goodwishes/constants/ui_numbers.dart';
import 'package:goodwishes/pages/text_dialog.dart';
import 'package:goodwishes/widgets/category_item.dart';
import 'package:goodwishes/widgets/stack_top_navigation_bar.dart';
import 'package:provider/provider.dart';

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

  @override
  Widget build(BuildContext context) {
    final categoryListProvider = Provider.of<CategoryListProvider>(context);

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(
              horizontal: UIDefault.pageHorizontalPadding),
          child: Column(
            children: [
              const StackTopNavigationBar(),
              CategoryList(categoryListProvider: categoryListProvider),
              const CategoryAddBox(),
            ],
          ),
        ),
      ),
    );
  }
}

class CategoryList extends StatelessWidget {
  const CategoryList({
    super.key,
    required this.categoryListProvider,
  });

  final CategoryListProvider categoryListProvider;

  @override
  Widget build(BuildContext context) {
    void deleteCategoryHandler(String id) {
      bool result = categoryListProvider.removeCategory(id);
      if (!result) {
        showDialog(
          context: context,
          builder: (context) {
            return const TextDialog(
              text: '카테고리에 담긴 굿즈가 있습니다. 카테고리 안의 굿즈를 비운 후 제거하세요.',
            );
          },
        );
      }
    }

    return Expanded(
      child: ListView.separated(
        //ListView 내부에 스크롤 기능이 있다.
        itemCount: categoryListProvider.categoryList.length + 1,
        separatorBuilder: (context, index) {
          if (index == 0) return const SizedBox.shrink();
          return const SizedBox(
            height: 5,
          );
        },
        itemBuilder: (context, index) {
          if (index == 0) {
            return Container(
              alignment: Alignment.centerLeft,
              margin: const EdgeInsets.only(top: 10, bottom: 20),
              child: const Text(
                '굿즈 분류 목록',
                style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.w500,
                ),
              ),
            );
          } else {
            return CategoryItem(
              category: categoryListProvider.categoryList.elementAt(index - 1),
              onDeleteCategory: deleteCategoryHandler,
            );
          }
        },
      ),
    );
  }
}

class CategoryAddBox extends StatelessWidget {
  const CategoryAddBox({super.key});
  @override
  Widget build(BuildContext context) {
    final TextEditingController controller = TextEditingController();
    final categoryListProvider = Provider.of<CategoryListProvider>(context);
    String newCategory = '';

    return Container(
      margin: const EdgeInsets.only(top: 10, bottom: 10),
      padding: const EdgeInsets.symmetric(horizontal: 10),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(5),
      ),
      child: Row(
        children: [
          Expanded(
            child: Padding(
              padding: const EdgeInsets.only(left: 10),
              child: TextField(
                controller: controller,
                onChanged: (val) {
                  newCategory = val;
                },
                decoration: const InputDecoration(
                  border: InputBorder.none,
                  hintText: '긋즈 분류 추가',
                  hintStyle: TextStyle(
                    color: Colors.grey,
                    fontSize: 16,
                    height: 2,
                  ),
                ),
              ),
            ),
          ),
          IconButton(
            onPressed: () {
              if (newCategory != '') {
                categoryListProvider.addCategory(
                  Category(
                      id: DateTime.now().toString(),
                      categoryName: newCategory,
                      count: 0),
                );
                newCategory = '';
                controller.clear();
              }
            },
            icon: const Icon(Icons.add_rounded),
            color: Colors.black,
            constraints: const BoxConstraints(),
            style: const ButtonStyle(
                tapTargetSize: MaterialTapTargetSize.shrinkWrap),
          ),
        ],
      ),
    );
  }
}

CategoryAddPage는 CategoryList, CategoryAddBox 위젯을 포함하고 있습니다.

우선 CategoryAddPage부터 차근차근 알아보겠습니다.

 

 

 

아까 ChangeNotifierProvider()로 페이지를 연결해주었기 때문에, 전역 category state를 가져올 수 있습니다.

 

 

 

AddCategoryPage는 사진과 같은 UI로 보여지는데,

 

 

 

위 코드의 위젯들이 순차적으로 화면사진의 빨간 네모를 구성합니다.

StackTopNavigationBar : 뒤로가기 버튼
CategoryList : 카테고리 리스트를 출력합니다.
CategoryAddBox : 페이지 맨 하단의, 굿즈 추가 Input과 버튼을 출력합니다.

 

 

 

 

 


AddCategoryPage - CategoryList

class CategoryList extends StatelessWidget {
  const CategoryList({
    super.key,
    required this.categoryListProvider,
  });

  final CategoryListProvider categoryListProvider;

  @override
  Widget build(BuildContext context) {
    void deleteCategoryHandler(String id) {
      bool result = categoryListProvider.removeCategory(id);
      if (!result) {
        showDialog(
          context: context,
          builder: (context) {
            return const TextDialog(
              text: '카테고리에 담긴 굿즈가 있습니다. 카테고리 안의 굿즈를 비운 후 제거하세요.',
            );
          },
        );
      }
    }

    return Expanded(
      child: ListView.separated(
        //ListView 내부에 스크롤 기능이 있다.
        itemCount: categoryListProvider.categoryList.length + 1,
        separatorBuilder: (context, index) {
          if (index == 0) return const SizedBox.shrink();
          return const SizedBox(
            height: 5,
          );
        },
        itemBuilder: (context, index) {
          if (index == 0) {
            return Container(
              alignment: Alignment.centerLeft,
              margin: const EdgeInsets.only(top: 10, bottom: 20),
              child: const Text(
                '굿즈 분류 목록',
                style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.w500,
                ),
              ),
            );
          } else {
            return CategoryItem(
              category: categoryListProvider.categoryList.elementAt(index - 1),
              onDeleteCategory: deleteCategoryHandler,
            );
          }
        },
      ),
    );
  }
}

CategoryList 위젯은 카테고리 전반을 나타냅니다.

근데 구성이 조금 까다롭습니다.

 

 

리스트뷰로 카테고리 아이템 출력

ListView.separated(
        //ListView 내부에 스크롤 기능이 있다.
        itemCount: categoryListProvider.categoryList.length + 1,
        separatorBuilder: (context, index) {
          if (index == 0) return const SizedBox.shrink();
          return const SizedBox(
            height: 5,
          );
        },
        itemBuilder: (context, index) {
          if (index == 0) {
            return Container(
              alignment: Alignment.centerLeft,
              margin: const EdgeInsets.only(top: 10, bottom: 20),
              child: const Text(
                '굿즈 분류 목록',
                style: TextStyle(
                  fontSize: 30,
                  fontWeight: FontWeight.w500,
                ),
              ),
            );
          } else {
            return CategoryItem(
              category: categoryListProvider.categoryList.elementAt(index - 1),
              onDeleteCategory: deleteCategoryHandler,
            );
          }
        },
      ),

ListView.separated()를 이용해서, categoryList의 갯수만큼 카테고리를 출력합니다.

 

 

 

단, 맨 위에는 '굿즈 분류 목록' 타이틀을 출력하고,

 

 

 

 

그 다음부터는 카테고리들을 CategoryItem이라는 위젯을 이용해서 출력합니다.

 

 

 

 

 

CategoryItem

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/category_model.dart';

class CategoryItem extends StatelessWidget {
  final Category category;
  final Function(String) onDeleteCategory;

  const CategoryItem({
    super.key,
    required this.category,
    required this.onDeleteCategory,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(5),
        color: Colors.white,
      ),
      child: Row(
        children: [
          Expanded(
            child: TextButton(
              onPressed: () {
                Navigator.pop(context, category.categoryName);
              },
              style: const ButtonStyle(
                  padding: WidgetStatePropertyAll(EdgeInsets.zero)),
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 10),
                alignment: Alignment.centerLeft,
                child: Text(
                  '${category.categoryName}   - ${category.count}',
                  style: const TextStyle(
                    color: Colors.black,
                    fontSize: 16,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ),
            ),
          ),
          if (category.id != 'no_category')
            Container(
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(5),
                color: Colors.cyan[300],
              ),
              child: IconButton(
                icon: const Icon(
                  Icons.delete,
                  color: Colors.white,
                  size: 18,
                ),
                padding: const EdgeInsets.all(10),
                constraints: const BoxConstraints(),
                style: const ButtonStyle(
                  tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                ),
                highlightColor: Colors.transparent,
                onPressed: () {
                  onDeleteCategory(category.id);
                },
              ),
            )
        ],
      ),
    );
  }
}

카테고리 이름과 카테고리 제거 버튼을 출력하는 역할을 담당합니다.

 

 

 

'no_category' id의 카테고리는 카테고리 제거 버튼이 없도록 예외처리를 했습니다.

 

 

 

아무런 카테고리가 없는 굿즈들이 기본으로 담겨질 '카테고리 없음' 분류이기 때문입니다.

 

 

 

 

 

 

CategoryAddBox

class CategoryAddBox extends StatelessWidget {
  const CategoryAddBox({super.key});
  @override
  Widget build(BuildContext context) {
    final TextEditingController controller = TextEditingController();
    final categoryListProvider = Provider.of<CategoryListProvider>(context);
    String newCategory = '';

    return Container(
      margin: const EdgeInsets.only(top: 10, bottom: 10),
      padding: const EdgeInsets.symmetric(horizontal: 10),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(5),
      ),
      child: Row(
        children: [
          Expanded(
            child: Padding(
              padding: const EdgeInsets.only(left: 10),
              child: TextField(
                controller: controller,
                onChanged: (val) {
                  newCategory = val;
                },
                decoration: const InputDecoration(
                  border: InputBorder.none,
                  hintText: '긋즈 분류 추가',
                  hintStyle: TextStyle(
                    color: Colors.grey,
                    fontSize: 16,
                    height: 2,
                  ),
                ),
              ),
            ),
          ),
          IconButton(
            onPressed: () {
              if (newCategory != '') {
                categoryListProvider.addCategory(
                  Category(
                      id: DateTime.now().toString(),
                      categoryName: newCategory,
                      count: 0),
                );
                newCategory = '';
                controller.clear();
              }
            },
            icon: const Icon(Icons.add_rounded),
            color: Colors.black,
            constraints: const BoxConstraints(),
            style: const ButtonStyle(
                tapTargetSize: MaterialTapTargetSize.shrinkWrap),
          ),
        ],
      ),
    );
  }
}

 

 

 

화면 하단에 category 이름을 입력한 후 '+버튼'을 누르면, 

 

 

 

  void addCategory(Category element) {
    _categoryListBox.put(element.id, element);
    notifyListeners(); // 값 변경 후 상태 변경 알림
  }

CategoryListProvider에서 미리 생성해놓은 addCategory디스패치 함수를 이용해서 categoryList 전역 state에 카테고리가 추가됩니다. 

 

 

 

그리고 해당 카테고리의 CategoryItem 위젯이 나타납니다.

 

 

 

 

카테고리 제거 버튼

카테고리 제거 버튼에 핸들러 함수를 제작하기 전, CategoryListProvider에 카테고리를 제거할 수 있는 디스패치 함수를 제작합니다.

단, 제거하려는 카테고리 안에 굿즈가 하나도 안 담겨 있을 경우에만 제거할 수 있게 if문 처리를 합니다.

제거하려는 카테고리 안에 굿즈가 없다면 true, 있다면 false를 return 합니다.

 

 

 

 

 

이제 제거 버튼에 연결할 핸들러함수를 제작합니다.

카테고리 제거 디스패치 함수를 불러오는데, 만약 카테고리 안에 굿즈가 담긴 상태라 제거가 실패됐다면, false값을 받아올 것입니다.

그러면 '제거 실패 알림'을 띄우는 코드를 추가합니다.

 

 

 

 

 

카테고리 제거 버튼을 누르면, 카테고리가 제거되고, 해당 CategoryItem() 위젯이 사라집니다.

 

 

 

 

만약, 굿즈가 있는 상태에서 제거하려고 하면, 위와 같은 경고 문구가 뜹니다.

 

 

 

 

 


지정된 카테고리

이제 완성이 됐습니다.

카테고리 설정 페이지의 리스트에서, 사진의 빨간 네모 영역을 터치하면,

 

 

 

 

다시 굿즈 추가 페이지로 돌아오고, 굿즈 분류 항목의 텍스트가 '아까 선택한 카테고리'로 변경된 것을 확인할 수 있습니다.

 

 

반응형