[Flutter] Flutter 앱의 분야별 책 목록 화면 구현하기: CategoryBookListScreen 클래스

StudipU·2024년 3월 2일
0

이번 글에서는 진중문고 분야별 책 목록 화면을 구현한 방법에 대해 알아보겠습니다. 코드를 하나씩 살펴보면서 각 부분의 역할과 기능을 자세히 설명하겠습니다.

CategoryBookListScreen 클래스 소개 ✨

CategoryBookListScreenFirestore에 저장된 진중문고 도서 리스트를 분야별로 구분하여 각각의 Category로 묶은 후, 드롭다운 버튼을 통해 선택한 Category의 도서 리스트를 화면에 출력하는 클래스입니다.

주요 기능 및 코드 분석 🎭

1. selectedValue 변수

아래 코드는 CategoryBookListScreen 위젯과 해당 위젯의 상태 클래스 _CategoryBookListScreenState를 정의합니다. selectedValue 변수는 사용자가 선택한 카테고리를 저장합니다. initState() 메서드에서 selectedValue의 초기 값을 '경제경영'으로 설정합니다.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:military_bookstore/database/get_aladin_database.dart';
import 'package:military_bookstore/database/get_database_Info.dart';
import 'package:military_bookstore/screen/book_detail_screen.dart';
import 'package:military_bookstore/widget/appbar_widget.dart';
import 'package:military_bookstore/widget/drewer_widget.dart';
import 'package:military_bookstore/widget/nav_widget.dart';

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

  
  State<CategoryBookListScreen> createState() =>
      _CategoryBookListScreenState();
}

class _CategoryBookListScreenState extends State<CategoryBookListScreen> {
  String? selectedValue;

  void initState() {
    super.initState();
    selectedValue = "경제경영";
  }

  
  Widget build(BuildContext context) {
    // 화면 구현 코드
  }

  // 나머지 코드 (이하 생략)
}

2. 화면 구성 요소

Scaffold 위젯을 사용하여 앱의 기본적인 레이아웃을 구성합니다. FutureBuilder를 사용하여 비동기적으로 데이터를 가져와서 화면을 구성합니다. 데이터가 로드되면 해당 데이터를 사용하여 화면을 구성하고, 로딩 중에는 로딩 화면을 표시합니다.


Widget build(BuildContext context) {
  return Scaffold(
    appBar: appbar_widget(context),
    drawer: drewer_widget(context),
    body: Center(
      child: Container(
        clipBehavior: Clip.antiAlias,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment(-0.00, -1.00),
            end: Alignment(0, 1),
            colors: [Color(0xA545B0C5), Color(0xFF4580C5), Color(0xFF4580C5)],
          ),
        ),
        child: FutureBuilder(
          future: getCategoryBookList(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              // 데이터가 있을 때의 화면 구성
            } else {
              // 데이터를 기다리는 동안의 로딩 화면
            }
          },
        ),
      ),
    ),
  );
}

3. 분야별 도서 데이터 가져오기

getCategoryBookList() 함수는 Firestorecategory collection의 문서 id(책 분야 종류) 정보를 가져와, Map<Map<String, List>> type의 각 문서 정보를 리스트 요소로 갖는 categoryList를 리턴합니다.

  • ex.[{ '분야1' : {'세부분야1' : [책1, 책2, 책3, ...], } }, { '분야2' : ...}]
Future<Map<String, dynamic>> getCategoryBookList() async {
  Map<String, dynamic> categoryList = {};
  QuerySnapshot<Map<String, dynamic>> querySnapshot =
      await FirebaseFirestore.instance.collection("category").get();

  querySnapshot.docs.forEach((doc) => categoryList[doc.id] = doc.data());

  return categoryList;
}

4. 세부분야에서 대분야로 통합

위의 getCategoryBookList()에서 반환받은 리스트에는 대분야 안에 세부분야들이 있고, 해당 세부분야에 대한 도서목록 배열로 구성되어있다. 이 구조에서 대분야 아래에 있는 세부분야들을 없애고 특정 대분야 하의 세부분야에 매핑된 도서배열들을 전부 expend하여 {'대분야1': [도서1, 도서2],} 와 같은 형식으로 만드는 코드이다. 아무래도 드롭다운 버튼의 항목들을 세부분야로까지 나누기에는 효율성이 떨어진다고 판단했기 때문이다. categoryListtotalElement에서 키값들로 만든 리스트이다.

FutureBuilder(
  future: getCategoryBookList(),
  builder: (context, snapshot) {
    if (snapshot.hasData){
      Map<String, dynamic> totalElement = {};
      snapshot.data!.forEach((key, value) {
        List<dynamic> subElement = value.values.toList();
        totalElement[key] = List<String>.from(subElement.expand((list) => list).toList());
      });
      List<String> categoryList = totalElement.keys.toList();
      // 이후 코드는 생략

5. 드롭다운 버튼 구현

4번에서 totalElement를 통해 만든 categoryList는 모든 대분야들을 요소로 갖는 리스트이고, 이 대분야들을 items로 만들어 드롭다운 버튼을 구현했다.

Container(
  width: widthRatio * 110,
  height: heightRatio * 28,
  margin: EdgeInsets.only(top: heightRatio * 3),
  padding: EdgeInsets.only(left: widthRatio * 10),
  decoration: ShapeDecoration(
    color: Color(0xFF46B0C6),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
  ),
  alignment: Alignment.center,
  child: DropdownButton<String>(
    value: selectedValue,
    onChanged: (String? newValue) {
      setState(() {
        selectedValue = newValue;
      });
    },
    items: categoryList.map<DropdownMenuItem<String>>((String value) {
      return DropdownMenuItem<String>(
        value: value,
        child: Container(
          constraints: BoxConstraints(
            maxWidth: 80.0,
          ),
          child: FittedBox(
            fit: BoxFit.fitWidth,
            child: Text(value),
          ),
        ),
      );
    }).toList(),
    style: TextStyle(
      color: Colors.white,
      fontSize: 14,
      fontFamily: 'GowunBatang',
      fontWeight: FontWeight.w700,
      height: 0,
      letterSpacing: -0.35,
    ),
    dropdownColor: Color(0xFF46B0C6),
    underline: Container(),
    icon: Icon(
      Icons.arrow_drop_down, // 드롭다운 아이콘 지정
      color: Colors.white, // 아이콘 색상 설정
    ),
  ),
)

6. 도서 리스트 구현

totalElement는 4번에서 설명한 것과 같이, '대분야'를 key값으로 갖고 해당 대분야에 속하는 도서들의 Listvalue 값으로 갖는 Map입니다. 현재 드롭다운 버튼에서 선택된 대분야인 selectedValue를 이용하여, totalElement[selectedValue]로 현재 선택된 대분야의 도서들 정보를 가져왔습니다.

Container(
  margin: EdgeInsets.only(top: heightRatio * 18),
  width: widthRatio * 350,
  height: heightRatio * 550,
  child: SingleChildScrollView(
    child: Column(
      children: List.generate(
        totalElement[selectedValue]!.length,
        (index) => lstItem(totalElement[selectedValue]!, index, widthRatio, heightRatio)),
    ),
  ),
)

CategoryBookListScreen 클래스 전체 코드 🎍

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:military_bookstore/database/get_aladin_database.dart';
import 'package:military_bookstore/database/get_database_Info.dart';
import 'package:military_bookstore/screen/book_detail_screen.dart';
import 'package:military_bookstore/widget/appbar_widget.dart';
import 'package:military_bookstore/widget/drewer_widget.dart';
import 'package:military_bookstore/widget/nav_widget.dart';


class CategoryBookListScreen extends StatefulWidget {

  const CategoryBookListScreen({super.key});

  
  State<CategoryBookListScreen> createState() =>
      _CategoryBookListScreenState();
}

class _CategoryBookListScreenState extends State<CategoryBookListScreen> {
  String? selectedValue;

  void initState() {
    super.initState();
    selectedValue = "경제경영";
  }

  /// setState 함수가 다시 실행되면 build가 다시 됨
  
  Widget build(BuildContext context) {
    double deviceWidth = MediaQuery.of(context).size.width;
    double deviceHeight = MediaQuery.of(context).size.height;

    double widthRatio = deviceWidth / 375;
    double heightRatio = deviceHeight / 812;

    return Scaffold(
      appBar: appbar_widget(context),
      drawer: drewer_widget(context),
      body: Center(
        child: Container(
          clipBehavior: Clip.antiAlias,
          // 배경색상 설정
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment(-0.00, -1.00),
              end: Alignment(0, 1),
              colors: [Color(0xA545B0C5), Color(0xFF4580C5), Color(0xFF4580C5)],
            ),
          ),
          child: FutureBuilder(
            /// {경제경영 : {경제학-경제일반 : ["스틱",---]} }, {과학: {-:[]} }, }
              future: getCategoryBookList(),
              builder: (context, snapshot) {
                if (snapshot.hasData){
                  Map<String, dynamic> totalElement = {};
                  snapshot.data!.forEach((key, value) {
                    /// 각 분야의 세부 분야가 (경제학-경제일반 : [----]) 형태의 value
                    /// 이 value에서 values.toList()는 세부분야의 도서들
                    /// totalElement의 Key는 대분야, Value는 대분야 안의 속한 모든 도서들
                    List<dynamic> subElement = value.values.toList();
                    // print(subElement.expand((list) => list).toList().runtimeType);
                    totalElement[key] = List<String>.from(subElement.expand((list) => list).toList());
                  });

                  List<String> categoryList = totalElement.keys.toList();
                  return Column(
                    children: [
                      nav_widget(context),
                      Container(
                        margin: EdgeInsets.only(
                          top: heightRatio * 11,
                          left: widthRatio * 10,
                          right: widthRatio * 10,
                        ),
                        width: widthRatio * 355,
                        decoration: ShapeDecoration(
                          shape: RoundedRectangleBorder(
                            side: BorderSide(
                              width: widthRatio * 3,
                              strokeAlign: BorderSide.strokeAlignCenter,
                              color: Colors.white.withOpacity(0.5),
                            ),
                          ),
                        ),
                      ),
                      Container(
                        margin: EdgeInsets.only(
                          top: heightRatio * 20,
                        ),
                        child: Row(
                          children: [
                            Container(
                              margin: EdgeInsets.only(
                                left: widthRatio * 18,
                              ),
                              width: widthRatio * 23,
                              height: heightRatio * 23,
                              child: SvgPicture.asset(
                                  "assets/main_screen_icon/white_calender.svg"),
                            ),
                            Container(
                                margin: EdgeInsets.only(
                                  left: widthRatio * 8,
                                  right: widthRatio * 107,
                                ),
                                width: widthRatio * 91,
                                height: heightRatio * 23,
                                child: FittedBox(
                                  fit: BoxFit.fitWidth,
                                  child: Text(
                                    '분야별 책 목록',
                                    textAlign: TextAlign.center,
                                    style: TextStyle(
                                      color: Color(0xE5001F3F),
                                      fontFamily: 'GowunBatang',
                                      fontWeight: FontWeight.bold,
                                      height: 0,
                                      letterSpacing: -0.40,
                                    ),
                                  ),
                                )),
                            Container(
                              width: widthRatio * 110,
                              height: heightRatio * 28,
                              margin: EdgeInsets.only(top: heightRatio * 3),
                              padding: EdgeInsets.only(left: widthRatio * 10),
                              decoration: ShapeDecoration(
                                color: Color(0xFF46B0C6),
                                shape: RoundedRectangleBorder(
                                    borderRadius: BorderRadius.circular(8)),
                              ),
                              alignment: Alignment.center,
                              child: DropdownButton<String>(
                                value: selectedValue,
                                onChanged: (String? newValue) {
                                  setState(() {
                                    selectedValue = newValue;
                                  });
                                },
                                items: categoryList.map<DropdownMenuItem<String>>((String value) {
                                  return DropdownMenuItem<String>(
                                    value: value,
                                    child: Container(
                                      constraints: BoxConstraints(
                                        maxWidth: 80.0,
                                      ),
                                      child: FittedBox(
                                        fit: BoxFit.fitWidth,
                                        child: Text(value),
                                      ),
                                    ),
                                  );
                                }).toList(),
                                style: TextStyle(
                                  color: Colors.white,
                                  fontSize: 14,
                                  fontFamily: 'GowunBatang',
                                  fontWeight: FontWeight.w700,
                                  height: 0,
                                  letterSpacing: -0.35,
                                ),
                                dropdownColor: Color(0xFF46B0C6),
                                underline: Container(),
                                icon: Icon(
                                  Icons.arrow_drop_down, // 드롭다운 아이콘 지정
                                  color: Colors.white, // 아이콘 색상 설정
                                ),
                              ),
                            ),

                          ],
                        ),
                      ),
                      Container(
                        margin: EdgeInsets.only(top: heightRatio * 18),
                        width: widthRatio * 350,
                        height: heightRatio * 550,
                        child: SingleChildScrollView(
                          child: Column(
                            children: List.generate(
                                totalElement[selectedValue]!.length,
                                    (index) =>
                                    lstItem(totalElement[selectedValue]!, index, widthRatio, heightRatio)),
                          ),
                        ),
                      )
                    ],
                  );

                }else{
                  return Column(
                    children: [
                      nav_widget(context),
                      Container(
                        margin: EdgeInsets.only(
                          top: heightRatio * 11,
                          left: widthRatio * 10,
                          right: widthRatio * 10,
                        ),
                        width: widthRatio * 355,
                        decoration: ShapeDecoration(
                          shape: RoundedRectangleBorder(
                            side: BorderSide(
                              width: widthRatio * 3,
                              strokeAlign: BorderSide.strokeAlignCenter,
                              color: Colors.white.withOpacity(0.5),
                            ),
                          ),
                        ),
                      ),
                      Container(
                        margin: EdgeInsets.only(
                          top: heightRatio * 20,
                        ),
                        child: Row(
                          children: [
                            Container(
                              margin: EdgeInsets.only(
                                left: widthRatio * 18,
                              ),
                              width: widthRatio * 23,
                              height: heightRatio * 23,
                              child: SvgPicture.asset(
                                  "assets/main_screen_icon/white_calender.svg"),
                            ),
                            Container(
                                margin: EdgeInsets.only(
                                  left: widthRatio * 8,
                                  right: widthRatio * 107,
                                ),
                                width: widthRatio * 91,
                                height: heightRatio * 23,
                                child: FittedBox(
                                  fit: BoxFit.fitWidth,
                                  child: Text(
                                    '분야별 책 목록',
                                    textAlign: TextAlign.center,
                                    style: TextStyle(
                                      color: Color(0xE5001F3F),
                                      fontFamily: 'GowunBatang',
                                      fontWeight: FontWeight.bold,
                                      height: 0,
                                      letterSpacing: -0.40,
                                    ),
                                  ),
                                )),
                          ],
                        ),
                      ),
                      CircularProgressIndicator(),
                    ],
                  );
                }

              }
          ),

        ),
      ),
    );
  }

  Widget lstItem(List<String> bookList, int index, widthRatio, heightRatio) {
    return GestureDetector(onTap:() {
      Navigator.push(context, MaterialPageRoute(
          builder: (context) => BookDetailScreen(bookTitle: bookList[index],)
      ),);
    },
      child: Container(
        margin: EdgeInsets.only(bottom: heightRatio * 20),
        padding: EdgeInsets.symmetric(
            vertical: heightRatio * 10, horizontal: widthRatio * 10),
        decoration: ShapeDecoration(
          color: Colors.white.withOpacity(0),
          shape: RoundedRectangleBorder(
            side: BorderSide(width: 2, color: Color(0xBFFFFFFF)),
            borderRadius: BorderRadius.circular(20),
          ),
        ),
        child: Container(
          margin: EdgeInsets.only(left: widthRatio * 5),
          child: Row(
            children: [
              FutureBuilder(
                future: getBookImage(bookList[index]),
                builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return CircularProgressIndicator();
                  } else if (snapshot.hasError) {
                    return Image.asset(
                      'assets/app_logo.png',width: widthRatio * 105,fit: BoxFit.fill
                    );
                  } else {
                    return Container(
                      decoration: BoxDecoration(
                        boxShadow: [
                          BoxShadow(
                            color: Color(0x3F000000),
                            blurRadius: 5,
                            offset: Offset(2, 4),
                            spreadRadius: 3,
                          )
                        ],
                      ),
                      child: Image(
                        image: snapshot.data!.image,
                        width: widthRatio * 105,
                        height: widthRatio * 105 * 1.48,
                        fit: BoxFit.fill,
                      ),
                    );
                  }
                },
              ),
              Container(
                margin: EdgeInsets.only(left: 20),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Container(
                            margin: EdgeInsets.only(bottom: heightRatio * 20),
                            constraints: BoxConstraints(
                              maxWidth: 200.0,
                            ),
                            child: FittedBox(
                              fit: BoxFit.fitWidth,
                              child: Text(
                                bookList[index],
                                textAlign: TextAlign.center,
                                style: TextStyle(
                                  color: Color(0xE5001F3F),
                                  fontSize: 18,
                                  fontFamily: 'GowunBatang',
                                  fontWeight: FontWeight.bold,
                                  height: 0,
                                  letterSpacing: -0.40,
                                ),
                              ),
                            )),
                      ],
                    ),
                    FutureBuilder(
                      future: getAladinDescription(bookList[index]),
                      builder:
                          (BuildContext context, AsyncSnapshot<String> snapshot) {
                        if (snapshot.connectionState == ConnectionState.waiting) {
                          return CircularProgressIndicator();
                        } else if (snapshot.hasError) {
                          return Text('책소개를 불러오는데 실패했습니다.', style: TextStyle(
                            color: Color(0xE5001F3F),
                            fontFamily: 'GowunBatang',
                            fontWeight: FontWeight.bold,
                            height: 0,
                            letterSpacing: -0.40,
                          ),);
                        } else {
                          return Container(
                            width: widthRatio * 190,
                            height: heightRatio * 101,
                            child: Text(
                              snapshot.data!,
                              maxLines: 4,
                              overflow: TextOverflow.ellipsis,
                              textAlign: TextAlign.center,
                              style: TextStyle(
                                color: Color(0xE5001F3F),
                                fontFamily: 'GowunBatang',
                                fontWeight: FontWeight.bold,
                                height: 0,
                                letterSpacing: -0.40,
                              ),
                            ),
                          );
                        }
                      },
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<Map<String,dynamic>> getCategoryBookList() async{
    Map<String, dynamic> categoryList = {};
    QuerySnapshot<Map<String, dynamic>> querySnapshot = await FirebaseFirestore.instance.collection("category").get();

    /// 여기서 doc은 docRef.get()한 것과 같은 상태
    querySnapshot.docs.forEach((doc) => categoryList[doc.id] = doc.data());

    return categoryList;

  }

}
profile
컴공 대학생이 군대에서 작성하는 앱 개발 블로그

0개의 댓글