본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓15(HomeScreen, ItemsPage)

이번시간에는 로그인 이후 보여지는 HomeScreen, ItemsPage 화면을 구현해보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

화면 구성은 아래와 같습니다.

 

 

 

./src/screens/home/home_screen.dart

 

import 'package:apple_market3/src/states/user_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'items_page.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _bottomSelectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    debugPrint(">>> build from HomeScreen");
    return Scaffold(
      appBar: AppBar(
        // centerTitle: true,
        title: Text('밀라노', style: Theme.of(context).appBarTheme.titleTextStyle),
        actions: [
          IconButton(
            onPressed: () {
              // 로그아웃하면 '/auth' 로 이동
              UserController.to.setUserAuth(false);
            },
            icon: const Icon(Icons.logout),
          ),
          IconButton(
            onPressed: () {},
            icon: const Icon(CupertinoIcons.search),
          ),
          IconButton(
            onPressed: () {},
            icon: const Icon(CupertinoIcons.text_justify),
          ),
        ],
      ),
      body: IndexedStack(
        index: _bottomSelectedIndex,
        children: <Widget>[
          const ItemsPage(),
          Container(color: Colors.accents[1]),
          Container(color: Colors.accents[3]),
          Container(color: Colors.accents[5]),
          Container(color: Colors.accents[7]),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        // 아이콘이 선택되지 않아도 label 이 보이게 하는 옵션
        // shifting 으로 설정하면 클릭시에만 label 이 보임,
        type: BottomNavigationBarType.fixed,
        // 아이콘이 클릭되면 onTap 이 실행되고, 이걸 currentIndex 에 전달해야 함
        onTap: (index) {
          setState(() {
            debugPrint('BottomNavigationBar(index): $index');
            _bottomSelectedIndex = index;
          });
        },
        // 클릭된 화면으로 이동하려면 매핑해야함
        currentIndex: _bottomSelectedIndex,
        // free icons : flaticon.com 에서 다운로드
        items: [
          // 아이콘이 클릭되면 onTap 이 실행됨, 
          BottomNavigationBarItem(
            icon: ImageIcon(AssetImage(_bottomSelectedIndex == 0
                ? 'assets/imgs/house_filled.png'
                : 'assets/imgs/house.png')),
            label: 'home',
          ),
          BottomNavigationBarItem(
            icon: ImageIcon(AssetImage(_bottomSelectedIndex == 1
                ? 'assets/imgs/near-me_filled.png'
                : 'assets/imgs/near-me.png')),
            label: 'near',
          ),
          BottomNavigationBarItem(
            icon: ImageIcon(AssetImage(_bottomSelectedIndex == 2
                ? 'assets/imgs/chat_filled.png'
                : 'assets/imgs/chat.png')),
            label: 'chat',
          ),
          BottomNavigationBarItem(
            // backgroundColor: Theme.of(context).bottomNavigationBarTheme.selectedItemColor,
            icon: ImageIcon(AssetImage(_bottomSelectedIndex == 3
                ? 'assets/imgs/user_filled.png'
                : 'assets/imgs/user.png')),
            label: 'me',
          ),
        ],
      ),
    );
  }
}

 

 

./src/screens/home/items_page.dart

 

import 'package:extended_image/extended_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import '../../constants/common_size.dart';

class ItemsPage extends StatefulWidget {
  // final String userKey;
  const ItemsPage({Key? key}) : super(key: key);

  @override
  State<ItemsPage> createState() => _ItemsPageState();
}

class _ItemsPageState extends State<ItemsPage> {
  @override
  Widget build(BuildContext context) {
    // 사진 사이즈를 화면 비율에 맞춰서 비례적으로 주기 위해서 LayoutBuilder 사용함,
    return LayoutBuilder(
      builder: (context, constraints) {
        Size size = MediaQuery.of(context).size;
        final imgSize = size.width / 4;
        return _listView(imgSize);
      },
    );
  }

  Widget _listView(double imgSize) {
    return ListView.separated(
      padding: const EdgeInsets.all(padding_16),
      separatorBuilder: (context, index) {
        return Divider(
          thickness: 1, // 실제 라인 두께
          color: Colors.grey[400],
          height: padding_16 * 2 + 1, // 라인 위/아래의 공간
          indent: padding_16, // 시작 부분 공간
          endIndent: padding_16, // 끝나는 부분 공간
        );
      },
      itemCount: 10,
      itemBuilder: (context, index) {
        return InkWell(
          onTap: () {},
          child: SizedBox(
            height: imgSize,
            child: Row(
              children: <Widget>[
                SizedBox(
                    height: imgSize,
                    width: imgSize,
                    child: ExtendedImage.network(
                      'https://picsum.photos/200',
                      fit: BoxFit.cover,
                      // borderRadius 를 사용하기 위해서는, BoxShape.rectangle 설정도 같이 해야함,
                      shape: BoxShape.rectangle,
                      borderRadius: BorderRadius.circular(15.0),
                    )),
                const SizedBox(
                  width: padding_16,
                ),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('제목', style: Theme.of(context).textTheme.subtitle1),
                      Text(
                        '50일전',
                        style: Theme.of(context).textTheme.subtitle2,
                      ),
                      const Text('300 원'),
                      // 금액과 하트 사이에 공백을 최대한 주기위해서 Expanded 사용함, 
                      Expanded(child: Container()),
                      // Row 가 2번 사용된 이유는 아래와 같다.
                      Row(
                        // 오론쪽 끝으로 정렬하기
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          // 폰트 사이즈를 줄이기 위해서 사이즈박스로 처리
                          SizedBox(
                            height: 16,
                            // 이걸로 다시 FittedBox 로 감싸면, 위젯 밖으로 나가지 못한다.
                            // 하지만 하위에 위치한 Row 정렬이 제 기능을 하지 못한다.
                            // 그래서 SizedBox 를 다시 한번더 Row 로 감싸고 정렬을 추가한다
                            child: FittedBox(
                              fit: BoxFit.fitHeight,
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.end,
                                children: const [
                                  Icon(CupertinoIcons.chat_bubble_2, color: Colors.grey),
                                  Text('23', style: TextStyle(color: Colors.grey)),
                                  Icon(CupertinoIcons.heart, color: Colors.grey),
                                  Text('123', style: TextStyle(color: Colors.grey)),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}