[Flutter] Flutter 애플리케이션의 메인 화면 구성하기: MainScreen 클래스

StudipU·2024년 2월 28일
0

Flutter는 Google에서 개발한 UI 툴킷으로, 단일 코드베이스에서 iOS, Android, 웹 및 데스크톱용 애플리케이션을 개발할 수 있는 기능을 제공합니다. 이번 글에서는 Flutter로 개발한 진중문고 애플리케이션의 메인 화면 구성을 소개드리겠습니다.

MainScreen 클래스 소개 ✨

MainScreen 클래스는 Flutter 애플리케이션에서 메인 화면을 담당하는 위젯입니다. 이 클래스는 StatefulWidget을 상속하여 상태를 가지며, 화면에 표시되는 내용이나 데이터를 업데이트할 수 있습니다. 이 클래스는 애플리케이션의 상단 바, 네비게이션 메뉴, 그리고 쿼터별 도서 목록과 개발자가 선택한 도서 목록 등을 표시합니다.

주요 기능 및 코드 분석 🎭

1. 적응형 어플리케이션

  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( // ... );
  }

적응형 화면을 만들기 위해, BuildContext context로부터 현재 device의 너비와 높이정보를 가져왔습니다. Figma로 작업한 화면의 가로와 세로를 기준(가로:375, 세로:812)으로 위젯들의 높이, 너비, 간격들을 설정했습니다. 따라서 위젯들의 크기 비율을 Figma 화면과 동일하게 만들기 위해 모든 위젯의 너비와 높이에 각각 widthRatioheightRatio를 곱한 값을 사용했습니다.

2. CustomScrollView와 Sliver 위젯 활용

메인 화면은 CustomScrollView와 Sliver 위젯을 사용하여 구성됩니다. 아래는 CustomScrollView 내부의 SliverList와 SliverToBoxAdapter를 사용하여 섹션을 구분하고 구분선을 표시하는 부분입니다.

CustomScrollView(
  slivers: [
    // 네비게이션바
    SliverToBoxAdapter(
      child: nav_widget(context),
    ),
    // 구분선
    SliverList(
      delegate: SliverChildListDelegate([
        // 구분선
        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),
              ),
            ),
          ),
        ),
      ]),
    ),
    // ...
  ],
),

다른 ScrollView 대신 CustomScrollView와 Sliver를 사용한 이유

: Dev's Pick 책 목록은 좌우로 스크롤이 되도록 하면서, 전체 화면이 위아래로 스크롤이 되도록 구현하고 싶었습니다. 전체 화면을 ScrollChildScrollView(child: Column(...))로 수직 스크롤을 구현한 후, 그 자식으로 Dev's Pick 목록을 SingleChildScrollView( scrollDirection: Axis.horizontal, child: ...)를 갖도록 시도한 결과 지속적으로 오류가 발생했습니다. ScrollView안에 ScrollVeiw를 넣었을 때, 각 위젯들이 스크롤을 두고 경쟁하면서 발생하는 오류인 것 같았습니다. 따라서 유연한 스크롤링 구현이 가능한 Sliver 계열의 위젯을 사용하였습니다.

3. Appbar / Drewer / nav_widget (여러 화면에서 사용)

AppBar

AppBar를 커스터마이징하여 메뉴버튼, 로고, 검색 아이콘, 마이페이지 버튼을 추가하였습니다. 각각의 onPressed 이벤트에서는 해당 페이지로 이동하도록 설정되어 있습니다.

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:military_bookstore/screen/search_bookTitle_screen.dart';

AppBar appbar_widget(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 AppBar(
    backgroundColor: Color(0xA545B0C5),
    // elevation: 1.0,
    iconTheme: IconThemeData(color: Colors.white),
    leading: Builder(builder: (context) {
      return IconButton(
        icon: SvgPicture.asset(
          "assets/main_screen_icon/menu_btn.svg",
        ),
        onPressed: () {
          // Drawer를 열기 위한 동작
          Scaffold.of(context).openDrawer(); // 이 부분이 Drawer를 열어주는 코드
        },
      );
    }),
    actions: [
      IconButton(
          icon: Image.asset(
            'assets/app_logo.png',
          ),
          onPressed: () async {
            Navigator.pushNamed(context, '/main');
          }),
      SizedBox(width: widthRatio * 3),
      IconButton(
        icon: SvgPicture.asset(
          "assets/main_screen_icon/appbar_search.svg",
          width: widthRatio * 252,
          height: heightRatio * 36,
        ),
        onPressed: () {
          // 검색 아이콘이 눌렸을 때의 동작
          Navigator.push(context,
              MaterialPageRoute(builder: (context) => SearchBookTitleScreen()));
        },
      ),
      IconButton(
        icon: SvgPicture.asset(
          "assets/main_screen_icon/mypage_btn.svg",
          width: widthRatio * 32,
          height: heightRatio * 32,
        ),
        onPressed: () {
          Navigator.pushNamed(context, '/account');
        },
      ),
      SizedBox(width: widthRatio * 5),
    ],
  );
}

Drewer
위 Appbar에서 좌측의 메뉴버튼을 클릭하면 아래와 같은 Drewer가 나타납니다.

로그아웃 상태(위), 로그인 상태(아래)

아래 코드는 위 Drewer Widget의 코드입니다. 아래 코드에서는 Drawer를 생성하고, 사용자 인증 상태에 따라 다른 DrawerHeader를 보여주도록 구현했습니다. 또한 각각의 네비게이션 아이템에 대한 ListTile도 추가되어 있습니다. 각각의 ListTile은 사용자가 해당 항목을 탭했을 때의 동작을 정의합니다. 목적지가 문자열인 경우는 Navigator 경로를 사용하고, 위젯인 경우는 해당 위젯으로 이동하도록 설정했습니다.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:military_bookstore/screen/bookworm_rank_screen.dart';
import 'package:military_bookstore/screen/category_bookList_screen.dart';
import 'package:military_bookstore/screen/quarterly_bookList_screen.dart';
import 'package:military_bookstore/screen/vote_next_book_screen.dart';

Drawer drewer_widget(BuildContext context) {
  final double widthRatio = MediaQuery.of(context).size.width / 375;
  final double heightRatio = MediaQuery.of(context).size.height / 812;

  return Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        Container(
          padding: EdgeInsets.all(0),
          decoration: BoxDecoration(
            color: const Color(0xBB4580C5),
          ),
          width: double.infinity,
          alignment: Alignment.bottomLeft,
          child: FutureBuilder<dynamic>(
            future: getCurrentUser(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const CircularProgressIndicator();
              } else if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else {
                final bool isLogin = snapshot.data != '';
                return isLogin
                    ? loginDrawerHeader(context, snapshot.data)
                    : logoutDrawerHeader(context, widthRatio, heightRatio);
              }
            },
          ),
        ),
        buildListTile(
          context,
          Icons.home,
          '홈',
          '/main',
        ),
        buildListTile(
          context,
          Icons.calendar_today,
          '분기별 보기',
          QuarterlyBookListScreen(),
        ),
        buildListTile(
          context,
          Icons.category,
          '분야별 보기',
          CategoryBookListScreen(),
        ),
        buildListTile(
          context,
          Icons.stars,
          '다독자 랭킹',
          BookwormScreen(), // TODO: 네비게이션 추가
        ),
        buildListTile(
          context,
          Icons.how_to_vote,
          '책 투표하기',
          VoteNextBookScreen(), // TODO: 네비게이션 추가
        ),
      ],
    ),
  );
}

Widget loginDrawerHeader(BuildContext context, DocumentSnapshot snapshot) {
  return UserAccountsDrawerHeader(
    currentAccountPicture: const CircleAvatar(
      backgroundImage: AssetImage('assets/app_logo.png'),
    ),
    accountName: Text(snapshot['username']),
    accountEmail: Container(
      child: Row(
        children: [
          Text(snapshot.id),
          Spacer(),
          Container(
            height: 30,
            child: TextButton(
              onPressed: () {
                FirebaseAuth.instance.signOut();
                Navigator.pushNamed(context, '/main');
                } ,
              child: Text(
                "로그아웃",
                style: const TextStyle(
                  fontSize: 10,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                  decoration: TextDecoration.underline,
                  decorationColor: Colors.white,
                ),
              ),
            ),
          ),
        ],
      ),
    ),
  );
}

DrawerHeader logoutDrawerHeader(
    BuildContext context, double widthRatio, double heightRatio) {
  return DrawerHeader(
    child: Container(
      alignment: Alignment.topLeft,
      child: Row(
        children: [
          OutlinedButton(
            onPressed: () => Navigator.pushNamed(context, '/login'),
            style: OutlinedButton.styleFrom(
              foregroundColor: Colors.white,
              side: const BorderSide(color: Colors.white),
            ),
            child: Text(
              "로그인",
              style: const TextStyle(
                fontSize: 15,
                fontFamily: 'GowunBatang',
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          const SizedBox(width: 10),
          OutlinedButton(
            onPressed: () => Navigator.pushNamed(context, '/signup'),
            style: OutlinedButton.styleFrom(
              foregroundColor: Colors.white,
              side: const BorderSide(color: Colors.white),
            ),
            child: const Text(
              "회원가입",
              style: TextStyle(
                fontSize: 15,
                fontFamily: 'GowunBatang',
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
    ),
  );
}

Future<dynamic> getCurrentUser() async {
  final User? user = FirebaseAuth.instance.currentUser;
  if (user != null) {
    final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance
        .collection('users')
        .doc(user.email)
        .get();
    return documentSnapshot;
  } else {
    return '';
  }
}

Widget buildListTile(
    BuildContext context, IconData icon, String title, dynamic destination) {
  return ListTile(
    leading: Icon(icon),
    title: Text(
      title,
      style: const TextStyle(
        fontSize: 15,
        fontFamily: 'GowunBatang',
        fontWeight: FontWeight.bold,
      ),
    ),
    onTap: () {
      if (destination != null) {
        if (destination is String) {
          Navigator.pushNamed(context, destination);
        } else if (destination is Widget) {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => destination),
          );
        }
      }
    },
    trailing: const Icon(Icons.navigate_next),
  );
}

사용자 인증은 아래 코드에서 Firebase Authentication을 사용하여 현재 사용자를 가져오고, 해당 사용자의 정보를 Firestore에서 가져오고, 사용자가 로그인 상태인지에 따라 다른 DrawerHeader를 보여주게 됩니다. Firebase Authentication에 대해서는 다음 글에서 더 자세히 다루도록 하겠습니다.

Future<dynamic> getCurrentUser() async {
  final User? user = FirebaseAuth.instance.currentUser;
  if (user != null) {
    final DocumentSnapshot documentSnapshot = await FirebaseFirestore.instance
        .collection('users')
        .doc(user.email)
        .get();
    return documentSnapshot;
  } else {
    return '';
  }
}

이를 통해 Flutter 앱의 Drawer를 커스터마이징하여 사용자 인증 기능을 추가하고, Navigator를 설정해 보았습니다.

nav_widget
nav_widget 함수는 Flutter 앱에서 사용자가 특정 화면으로 이동할 수 있는 네비게이션 위젯을 구현한 함수입니다. SingleChildScrollView(scrollDirection: Axis.horizontal, ...)를 활용해 수평 스크롤 공간 위젯을 만들었습니다. 또한, 특정 가로 공간 내에서 여러 개의 네비게이션 아이템을 표시했고 각 아이템을 클릭할 때 해당하는 화면으로 이동할 수 있도록 하였습니다.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:military_bookstore/screen/bookworm_rank_screen.dart';
import 'package:military_bookstore/screen/category_bookList_screen.dart';
import 'package:military_bookstore/screen/quarterly_bookList_screen.dart';
import 'package:military_bookstore/screen/vote_next_book_screen.dart';

SingleChildScrollView nav_widget(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 SingleChildScrollView(
    scrollDirection: Axis.horizontal,
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        GestureDetector(
          onTap:() {
            Navigator.pushNamed(context, '/main');
          },
          child: Container(
            width: widthRatio * 60,
            height: heightRatio * 42,
            padding: const EdgeInsets.all(10),
            child: FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                'Home',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xD8444444),
                  fontSize: 15,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  height: 0,
                ),
              ),
            ),
          ),),
        GestureDetector(
          onTap: () {
            Navigator.push(context, MaterialPageRoute(
                builder: (context) => QuarterlyBookListScreen()
            )
            );
          },
          child: Container(
            width: widthRatio * 92,
            height: heightRatio * 42,
            padding: const EdgeInsets.all(10),
            child: FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                '분기별 보기',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xD8444444),
                  fontSize: 15,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  height: 0,
                ),
              ),
            ),
          ),
        ),
        GestureDetector(
          onTap: () {
            Navigator.push(context, MaterialPageRoute(
                builder: (context) => CategoryBookListScreen()
            )
            );
          },
          child: Container(
            width: widthRatio * 93,
            height: heightRatio * 38,
            padding: const EdgeInsets.all(10),
            child: FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                '분야별 보기',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xD8444444),
                  fontSize: 15,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  height: 0,
                ),
              ),
            ),
          ),
        ),
        GestureDetector(
          child: Container(
            width: widthRatio * 91,
            height: heightRatio * 42,
            padding: const EdgeInsets.all(10),
            child: FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                '책벌레 랭킹',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xD8444444),
                  fontSize: 15,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  height: 0,
                ),
              ),
            ),
          ),
          onTap: (){
            Navigator.push(context, MaterialPageRoute(
                builder: (context) => BookwormScreen()
            )
            );
          }
        ),
        GestureDetector(
          child: Container(
            width: widthRatio * 76,
            height: heightRatio * 42,
            padding: const EdgeInsets.all(10),
            child: FittedBox(
              fit: BoxFit.fitWidth,
              child: Text(
                '투표하기',
                textAlign: TextAlign.center,
                style: TextStyle(
                  color: Color(0xD8444444),
                  fontSize: 15,
                  fontFamily: 'GowunBatang',
                  fontWeight: FontWeight.bold,
                  height: 0,
                ),
              ),
            ),
          ),
          onTap:() {
            Navigator.push(context, MaterialPageRoute(
                builder: (context) => VoteNextBookScreen()
            ),
            );
          },
        ),
        Container(
          width: widthRatio * 5,
          height: heightRatio * 42,
        ),
      ],
    ),
  );
}

4. Firebase Firestore와의 통신

코드에서 cloud_firestore 패키지를 사용하여 Firebase Firestore와 통신합니다. 아래는 Firestore에서 쿼터별 도서 목록을 가져오는 부분입니다.

Future<Map<String,dynamic>> getQuarterlyBookList() async{
  /// JinJoongBookList collection을 참조하는 Querysnapshot을 통해 data 가져옴
  Map<String, dynamic> quarterlyList = {};
  QuerySnapshot<Map<String, dynamic>> querySnapshot = 
  await FirebaseFirestore.instance.collection("JinJoongBookList").get();

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

아래는 Firestore의 데이터 구조입니다. JinJoongBookList collection는 각 년도에 해당하는 iddocument들을 갖고 있습니다. 또한 각 document의 data는 1Q 부터 4Q 까지를 키 값으로 갖고 각 분기의 책 목록들을 배열(리스트)로 갖는, Map<String, List<String>> 타입으로 구성했습니다.

FutureBuilder를 통해 getQuarterlyBookList()의 반환값을 snapshot.data로 받아올 수 있습니다. yearWithQuartersnapshot.data를 활용하여 모든 분기들을 '년도 분기' ('2023Y 4Q') 와 같은 형식의 문자열로 구성된 배열로 만든 것입니다. (ex. [2019Y 1Q, 2019Y 2Q, 2019Y 3Q, 2019Y 4Q], [2020Y 1Q, 2020Y 2Q, ---]) yearWithQuarter에서 가장 최신 분기 문자열을 가져와서 년도와 분기를 ' '으로 split하여 snapshot.data![yearAndQuarter[0]][yearAndQuarter[1]]로 해당 분기의 도서 목록을 quarterlyBookList에 가져왔습니다. getQuarterlyBookList() 함수의 실행 결과를 기다린 후, if(snapshot.hasData) 조건문을 활용하여 만약 받아온 데이터가 아직 없는 경우 CircularProgressIndicator()를 반환하도록 하여 로딩 스피너를 표시하였습니다.

// ...
FutureBuilder(
  future: getQuarterlyBookList(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      var yearList = snapshot.data!.keys.toList();
      var recentYear = yearList.last;
      /// yearList에 각 쿼터 붙인 리스트들 만들기
      /// [2019Y 1Q, 2019Y 2Q, 2019Y 3Q, 2019Y 4Q], [2020Y 1Q, 2020Y 2Q, ---],
      List<dynamic> yearWithQuarter = snapshot
                      .data![recentYear].keys
                      .map((key) => '$recentYear $key')
                      .toList();
      var firstElement = yearWithQuarter.removeAt(0);
      yearWithQuarter.add(firstElement);

      var recentQuarter = yearWithQuarter[0];
      List<dynamic> yearAndQuarter = recentQuarter.split(' ');
      List<String> quarterlyBookList = List<String>.from(
                      snapshot.data![yearAndQuarter[0]][yearAndQuarter[1]]);
    } else {
      // 데이터가 없는 경우 로딩 스피너를 표시
      return CircularProgressIndicator();
    }
  },
),

5. 개발자 추천 도서 목록 표시

firebase console 사진과 같이, developerPickedBooks collection을 책 제목을 id로 갖는 문서들로 구성하였습니다.

아래 코드는 개발자가 선택한 도서 목록을 가져와 화면에 표시하는 부분입니다. FutureBuilder를 사용하여 비동기적으로 도서 목록을 가져온 후, 데이터의 상태에 따라 다른 화면을 표시합니다. 데이터를 가져오는 동안에는 로딩 스피너를 표시하고, 데이터를 성공적으로 가져오면 개발자가 선택한 도서 목록을 화면에 표시합니다.

Future<List<String>> getDevPicks() async {
    QuerySnapshot devPicksCollecion = await FirebaseFirestore.instance
        .collection("developerPickedBooks")
        .get();
    List<String> devPickList = [];
    devPicksCollecion.docs.forEach((doc) => devPickList.add(doc.id));
    return devPickList;
  }
//...
// Dev's Pick 책 목록을 표시하는 부분
FutureBuilder(
  future: getDevPicks(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      // 데이터를 가져오는 중인 경우 로딩 스피너 표시
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      // 데이터 가져오기 실패 시 에러 메시지 표시
      return Text('Error: ${snapshot.error}');
    } else {
      // 가져온 데이터를 기반으로 도서 목록 표시
      return Padding(
        padding: EdgeInsets.only(left: 0, right: 15),
        child: Row(
          children: snapshot.data!.map((bookTitle) => Container(
            // 각 도서를 나타내는 컨테이너
            // ...
          )).toList(),
        ),
      );
    }
  },
),

6. 도서 목록 아이템 표시

아래 코드는 화면의 도서 목록에서 각 아이템을 표시하는 부분입니다. GestureDetector를 사용하여 아이템을 누를 경우 해당 도서의 상세 화면(Detail Screen)으로 이동하도록 구현되어 있습니다. 각 아이템은 ListTile과 같은 형태로 구성되어 있으며, 사용자의 클릭에 반응하여 상세 정보를 보여줍니다.

// 도서 목록 아이템을 표시하는 부분
Widget lstItem(List<String> bookList, int index, widthRatio, heightRatio) {
  return GestureDetector(
    onTap: () {
      // 아이템을 누를 때 도서 상세 화면으로 이동
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => BookDetailScreen(
            bookTitle: bookList[index],
          ),
        ),
      );
    },
    child: Container(
      // 아이템 내용 및 스타일링
      // ...
    ),
  );
}

MainScreen Class 전체 코드 🔔

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 MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  void initState() {
    super.initState();
  }

  
  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: getQuarterlyBookList(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  var yearList = snapshot.data!.keys.toList();
                  var recentYear = yearList.last;
                  /// yearList에 각 쿼터 붙인 리스트들 만들기
                  /// [2019Y 1Q, 2019Y 2Q, 2019Y 3Q, 2019Y 4Q], [2020Y 1Q, 2020Y 2Q, ---],
                  List<dynamic> yearWithQuarter = snapshot
                      .data![recentYear].keys
                      .map((key) => '$recentYear $key')
                      .toList();
                  var firstElement = yearWithQuarter.removeAt(0);
                  yearWithQuarter.add(firstElement);

                  var recentQuarter = yearWithQuarter[0];
                  List<dynamic> yearAndQuarter = recentQuarter.split(' ');
                  List<String> quarterlyBookList = List<String>.from(
                      snapshot.data![yearAndQuarter[0]][yearAndQuarter[1]]);

                  return CustomScrollView(
                    slivers: [
                      // 네비게이션바
                      SliverPadding(
                        padding: EdgeInsets.only(
                          top: heightRatio * 10,
                        ),
                        sliver: SliverToBoxAdapter(
                          child: nav_widget(context),
                        ),
                      ),
                      // 구분선
                      SliverList(
                          delegate: SliverChildListDelegate([
                        // 구분선
                        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),
                              ),
                            ),
                          ),
                        ),
                      ])),

                      /// Dev's Pick !
                      SliverList(
                          delegate: SliverChildListDelegate([
                        /// Dev's Pick !
                        Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: [
                            Container(
                              padding: EdgeInsets.all(0),
                              margin: EdgeInsets.only(
                                left: widthRatio * 12,
                                top: heightRatio * 20,
                              ),
                              width: widthRatio * 23,
                              height: heightRatio * 23,
                              child: SvgPicture.asset(
                                'assets/main_screen_icon/star.svg',
                              ),
                            ),
                            Container(
                              margin: EdgeInsets.only(
                                top: heightRatio * 20,
                              ),
                              width: widthRatio * 85,
                              height: heightRatio * 23,
                              child: FittedBox(
                                fit: BoxFit.fitWidth,
                                child: Text(
                                  "Dev's Pick !",
                                  textAlign: TextAlign.center,
                                  style: TextStyle(
                                    color: Color(0xE5001F3F),
                                    fontFamily: 'GowunBatang',
                                    fontWeight: FontWeight.bold,
                                    height: 0,
                                    letterSpacing: -0.40,
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                      ])),
                      SliverToBoxAdapter(
                        child: SingleChildScrollView(
                          scrollDirection: Axis.horizontal,
                          child: FutureBuilder(
                              future: getDevPicks(),
                              builder: (context, snapshot) {
                                if (snapshot.connectionState ==
                                    ConnectionState.waiting) {
                                  return CircularProgressIndicator();
                                } else if (snapshot.hasError) {
                                  return Text('Error: ${snapshot.error}');
                                } else {
                                  return Padding(
                                    padding:
                                        EdgeInsets.only(left: 0, right: 15),
                                    child: Row(
                                      /// Dev's Pick 책 목록의 요소를 하나씩 꺼냄
                                      children: snapshot.data!
                                          .map((bookTitle) => Container(
                                                margin: EdgeInsets.only(
                                                  left: widthRatio * 15,
                                                  top: heightRatio * 15,
                                                ),
                                                decoration: BoxDecoration(
                                                  boxShadow: [
                                                    BoxShadow(
                                                      color: Colors.black
                                                          .withOpacity(0.5),
                                                      blurRadius: 4,
                                                      offset: Offset(2, 4),
                                                      spreadRadius: 1,
                                                    )
                                                  ],
                                                ),
                                                child: FutureBuilder(
                                                  future:
                                                      getBookImage(bookTitle),
                                                  builder:
                                                      (BuildContext context,
                                                          AsyncSnapshot<Image>
                                                              snapshot) {
                                                    if (snapshot
                                                            .connectionState ==
                                                        ConnectionState
                                                            .waiting) {
                                                      return CircularProgressIndicator();
                                                    } else if (snapshot
                                                        .hasError) {
                                                      return Text(
                                                          'Error: ${snapshot.error}');
                                                    } else {
                                                      return GestureDetector(
                                                        onTap:(){
                                                          Navigator.push(context,MaterialPageRoute(builder: (context) => BookDetailScreen(bookTitle: bookTitle)));
                                                        },
                                                        child: Image(
                                                          image: snapshot
                                                              .data!.image,
                                                          width: widthRatio * 105,
                                                          height: widthRatio *
                                                              105 *
                                                              1.48,
                                                          fit: BoxFit.fill,
                                                        ),
                                                      );
                                                    }
                                                  },
                                                ),
                                              ))
                                          ///각 컨테이너 객체들을 children 리스트로 만듦
                                          .toList(),
                                    ),
                                  );
                                }
                              }),
                        ),
                      ),
                      // 구분선
                      SliverList(
                          delegate: SliverChildListDelegate([
                        // 구분선
                        Container(
                          margin: EdgeInsets.only(
                            top: heightRatio * 20,
                            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),
                              ),
                            ),
                          ),
                        ),
                      ])),
                      SliverList(
                          delegate: SliverChildListDelegate([
                        Row(
                          children: [
                            Container(
                              padding: EdgeInsets.all(0),
                              margin: EdgeInsets.only(
                                left: widthRatio * 13,
                                top: heightRatio * 20,
                              ),
                              width: widthRatio * 23,
                              height: heightRatio * 23,
                              child: SvgPicture.asset(
                                  "assets/main_screen_icon/white_calender.svg"),
                            ),
                            Container(
                                margin: EdgeInsets.only(
                                  left: widthRatio * 5,
                                  top: heightRatio * 20,
                                  right: widthRatio * 240,
                                ),
                                width: widthRatio * 71,
                                height: heightRatio * 23,
                                child: FittedBox(
                                  fit: BoxFit.fitWidth,
                                  child: Text(
                                    recentQuarter,
                                    textAlign: TextAlign.center,
                                    style: TextStyle(
                                      color: Color(0xE5001F3F),
                                      fontFamily: 'GowunBatang',
                                      fontWeight: FontWeight.bold,
                                      height: 0,
                                      letterSpacing: -0.40,
                                    ),
                                  ),
                                )),
                          ],
                        ),
                      ])),
                      SliverPadding(
                        padding: EdgeInsets.only(
                            top: heightRatio * 20,
                            left: widthRatio * 13,
                            right: widthRatio * 13),
                        sliver: SliverList(
                          delegate: SliverChildBuilderDelegate(
                            (context, index) {
                              return lstItem(quarterlyBookList, index,
                                  widthRatio, heightRatio);
                            },
                            childCount: quarterlyBookList.length,
                          ),
                        ),
                      ),
                    ],
                  );
                } else {
                  return CustomScrollView(
                    slivers: [
                      // 네비게이션바
                      SliverPadding(
                        padding: EdgeInsets.only(
                          top: heightRatio * 10,
                        ),
                        sliver: SliverToBoxAdapter(
                          child: nav_widget(context),
                        ),
                      ),
                      // 구분선
                      SliverList(
                          delegate: SliverChildListDelegate([
                        // 구분선
                        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),
                              ),
                            ),
                          ),
                        ),
                      ])),

                      SliverToBoxAdapter(
                        child: 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',
                    );
                  } 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<List<String>> getDevPicks() async {
    QuerySnapshot devPicksCollecion = await FirebaseFirestore.instance
        .collection("developerPickedBooks")
        .get();
    List<String> devPickList = [];
    devPicksCollecion.docs.forEach((doc) => devPickList.add(doc.id));
    return devPickList;
  }
}
profile
컴공 대학생이 군대에서 작성하는 앱 개발 블로그

0개의 댓글