본문 바로가기

Flutter/App Making Learn

플러터 당근마켓 앱 카피코딩1 (appBar, body, floatingActionButton, bottomNavigationBar)

완성화면

스파르타코딩클럽에서 국비지원을 받아 공부하는 게시물입니다.

당근마켓을 카피코딩 해보았습니다.
앱을 만들때는 항상 구도(appBar, body, floatingActionButton, BottomNavigationBar)를 어떻게 사용할지 구상을 한뒤,
위에서부터 차례대로 내려오는 방식으로 구성하면 좋을것 같습니다.

그렇기에 appBar부터 시작.

 

appBar
appBar: AppBar(
        backgroundColor: Colors.white,
        elevation: 1,
        shadowColor: Colors.black,
        leading: Row(
          children: [
            SizedBox(width: 16),
            Text(
              '중앙동',
              style: TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
                fontSize: 20,
              ),
            ),
            Icon(
              Icons.keyboard_arrow_down_rounded,
              color: Colors.black,
            ),
          ],
        ),
        leadingWidth: 100,
        actions: [
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.search, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(Icons.menu_rounded, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.bell, color: Colors.black),
          ),
        ],
      ),

appBar에서는 

backgroundColor: Colors.white,
        elevation: 1,
        shadowColor: Colors.black,

배경화면의 색은 흰색으로 지정하고 (MaterialApp 버전이 바뀌면서 색깔이 바뀌는 경우가 있어서 지정해주는게 좋을 것 같다.
elevation은 주어진 int만큼 appBar를 시각적으로 올라와보이게 하는 효과를 주고,
shadowColor는 elevation이 올라간만큼 연계되어 그림자를 시각화 하는 효과를 준다, 아예 없는 경우와 shadowColor를 준 차이가 심해서 주게 되었습니다.

ShadowColor의 유무 차이

그리고
appBar의 틀을 잡아준뒤 무엇무엇을 입력해야할것인지 생각해봐야했고
왼쪽 아래 "중앙동" 과 오른쪽 icon3개를 추가로 입력해 넣어야한다. 그렇기에 우리는 저 영역이 무슨 영역인지 알아야 코드를 입력해 넣을수 있는데

AppBar 구성요소

이것을 토대로 아래의 flexibleSpace와 bottom은 현재 사용중이지 않으니
중앙동 == leading
actions == 3개의 icon
이란것을 알수 있습니다.

leading: Row(
          children: [
            SizedBox(width: 16),
            Text(
              '중앙동',
              style: TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
                fontSize: 20,
              ),
            ),
            Icon(
              Icons.keyboard_arrow_down_rounded,
              color: Colors.black,
            ),
          ],
        ),

이렇게 "중앙동"을 배치시켰습니다.
위에서 차례대로 읽어보면 

leading영역에 Row형태로 배치할것이며 폭 16짜리의 보이지 않는 sizedBox를 넣어 베젤과 Text사이에 공간을 두었다.
Text는 '중앙동'을 나타내고 있고 색상 black, 굵기 bold, 크기 20,
아이콘은 아래로 향하는 화살표로 만들어 주었다.
(leading영역을 onTap하였을때 아이콘이 위아래 바뀌는 애니메이션이나 또다른 창을 띄우는 것도 찾아보면 만들수 있을것 같다)
이로서 leading의 영역은 완료하였습니다.

하지만 여기서 중요한것은.

leading의 영역과 actions의 영역사이에 반드시 leading의 폭을 정해주는

leadingWidth: 100,

 을 입력해주어야 합니다.

입력해주지 않게 된다면

overflow 발생

위 사진처럼 overflow가 발생되기 때문에 반드시 leading의 폭값을 정해주도록 해야했습니다.

그리고 다음 우측에있는
actions는 최대 3개까지 배치할 수 있으며

actions: [
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.search, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(Icons.menu_rounded, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.bell, color: Colors.black),
          ),
        ],

위와 같이 배치를 하였습니다.
각각 버튼들은 onPressed를 통하여 다양한 기능들을 구사할수 있습니다.

 

 

body

바디를 만들기 위해선

위와 같은 형태로 만들어야 하는데 왼쪽에서 오른쪽으로, 위에서 아래로 구상합니다.

이런식으로

그렇다면
Row(이미지) => Column(텍스트) => Rox(아이콘) 식으로 짜야하는데

일단 Row를 사용하여 이미지부터 처리해보면

child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // CilpRRect 를 통해 이미지에 곡선 border 생성
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              // 이미지
              child: Image.network(
                'https://cdn2.thecatapi.com/images/6bt.jpg',
                width: 100,
                height: 100,
                fit: BoxFit.cover,
              ),
            ),

Image.network를 통해 url을 통한 이미지를 가져오고 높이 100, 폭 100의 크기에 사진을 맞춰준다
*boxfit에대한 플러터 공식문서(https://api.flutter.dev/flutter/painting/BoxFit.html)

ClipRRect는 이미지의 사각에 얼마나 곡률을 줄것인가에 대한 코드이며
125(최대)의 값을 입력하면 원 형태의 사진이 나온다

좌 - circular(8) / 우 - circulat(125)


그리고 앞서 appBar처럼 이미지와 텍스트가 딱 붙어있으면 안되기 때문에
sizedBox를 appBar와 Text사이에 넣어주고
Row를 지나왔으니 Column을 넣어줍니다.

SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.black,
                    ),
                    softWrap: false,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  SizedBox(height: 2),
                  Text(
                    '봉천동 · 6분 전',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.black45,
                    ),
                  ),
                  SizedBox(height: 4),
                  Text(
                    '100만원',
                    style: TextStyle(
                      fontSize: 14,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Row(
                    children: [
                      Spacer(),
                      GestureDetector(
                        onTap: () {},
                        child: Row(
                          children: [
                            Icon(
                              CupertinoIcons.heart,
                              color: Colors.black54,
                              size: 16,
                            ),
                            Text(
                              '1',
                              style: TextStyle(color: Colors.black54),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),

여기서 중요한건 그냥 Column을 넣고 진행하다보면

이 와같이 overflow가 발생하는데 이는 Column의 폭을 따로 지정해주지 않았기 때문에 발생한다.
overflow: TextOverflow를 사용해도 overflow가 해결되진 않았습니다.

여기서는 그렇다면 왼쪽에있는 이미지와 오른쪽에있는 텍스트를 정렬시켜주는게 필요한데 여기서 필요한것이
crossAxisAlignment: CrossAxisAlignment.~,
mainAxisAlignment: MainsAxisAlignment.~,
인데, 필요한 참고 자료는

위 사진들일 비교해가며 어떻게 하는지 확실히 해두어야합니다.
이것을 토대로
이미지에도 텍스트에도 crossAxisAlignment: CrossAxisAlignment.start 를 주었습니다.

그리고 더하여 Column위젯에 리팩터로 Expanded를 적용시켜주어 overflow를 해결하였습니다.

참고로 Expanded는 상위 위젯의 크기만큼 확장시켜주는 위젯인데 flexible가 비슷하지만 다릅니다.
간단하게 말하면 flexible은 디테일하게 2:8 처럼 구역을 지정해줄수 있는가 하면
Expanded는 하나의 위젯이면 하나를 가득 채워주고 두개의 동일한 위젯이 있다하면 절반을 차지합니다.

  Expanded flexible
child가 부모위젯 보다 클때 부모위젯 최대 크기로 확장 부모위젯 최대 크기로 확장
child가 부모위젯 보다 작을때 부모위젯 최대 크기로 확장 변화 없음

그리고 또 하위로 Row를 적용시켜 Spacer()를 사용하여 Column(텍스트)안에 구석에 하트모양 아이콘을 넣어 마무리 하였습니다.

 

floatingActionButton
floatingActionButton: FloatingActionButton(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(125),
        ),
        onPressed: () {},
        backgroundColor: Color(0xFFFF7E36),
        elevation: 1,
        child: Icon(
          Icons.add_rounded,
          size: 36,
        ),
      ),

floatingActionButton 역시 RoundedRectangleBorder를 통해 사각의 곡률을 정해주었습니다.
MaterialApp의 버전이 바뀌면서 그런것 같은데 곡률없이 일반생성하면 원이 아닌 둥근사각형이 나옵니다.

좌 - RoundedRectangleBorder 없음 / 우 - RoundedRectangleBorder 설정

 

bottomNavigationBar
bottomNavigationBar: BottomNavigationBar(
  fixedColor: Color(0xFFFF7E36), // 활성화된 아이콘 색상
  unselectedItemColor: Colors.black, // 비활성화된 아이콘 색상
  showUnselectedLabels: true, // false == 비활성화된 오브젝트 label제거
  selectedFontSize: 12, // 활성화된 label 폰트 사이즈
  unselectedFontSize: 12, // 비활성화된 label 폰트 사이즈
  iconSize: 28, // 전체 아이콘 사이즈
  type: BottomNavigationBarType.fixed,
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home_filled),
      label: '홈',
      backgroundColor: Colors.white,
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.my_library_books_outlined),
      label: '동네생활',
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.fmd_good_outlined),
      label: '내 근처',
    ),
    BottomNavigationBarItem(
        icon: Icon(CupertinoIcons.chat_bubble_2), label: '채팅'),
    BottomNavigationBarItem(
      icon: Icon(Icons.person_outline),
      label: '나의 당근',
    ),
  ],
  currentIndex: 0, // 활성화된 아이콘(index) 번호
),

bottomNavigationBar에도 여러가지 사용할수 있는것들이 많이 있었습니다.
각자의 역할들은 주석으로도 적어 놓았으지만

fixedColor : 활성화된 아이콘 색상
unSelectedItemColor : 비활성화된 아이콘 색상
showUnselectedLabels : false일때 비활성화된 오브젝트 label(Text) 제거
selectedFontSize : 활성화된 label 폰트 사이즈
unselectedFontSize : 비활성화된 label폰트 사이즈
iconSize : 전체 아이콘 사이즈

currentIndex : 활성화된 아이콘(index) 번호

들이 있다. 이중에는 중간중간 setState를 활용하여 유동적으로 바뀌게 할수 있는 기능을 추가하여 사용할 수 있을것 같습니다.

전체코드
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        elevation: 1,
        shadowColor: Colors.black,
        leading: Row(
          children: [
            SizedBox(width: 16),
            Text(
              '중앙동',
              style: TextStyle(
                color: Colors.black,
                fontWeight: FontWeight.bold,
                fontSize: 20,
              ),
            ),
            Icon(
              Icons.keyboard_arrow_down_rounded,
              color: Colors.black,
            ),
          ],
        ),
        leadingWidth: 100,
        actions: [
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.search, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(Icons.menu_rounded, color: Colors.black),
          ),
          IconButton(
            onPressed: () {},
            icon: Icon(CupertinoIcons.bell, color: Colors.black),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // CilpRRect 를 통해 이미지에 곡선 border 생성
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              // 이미지
              child: Image.network(
                'https://cdn2.thecatapi.com/images/6bt.jpg',
                width: 100,
                height: 100,
                fit: BoxFit.cover,
              ),
            ),

            SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.black,
                    ),
                    softWrap: false,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  SizedBox(height: 2),
                  Text(
                    '봉천동 · 6분 전',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.black45,
                    ),
                  ),
                  SizedBox(height: 4),
                  Text(
                    '100만원',
                    style: TextStyle(
                      fontSize: 14,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  Row(
                    children: [
                      Spacer(),
                      GestureDetector(
                        onTap: () {},
                        child: Row(
                          children: [
                            Icon(
                              CupertinoIcons.heart,
                              color: Colors.black54,
                              size: 16,
                            ),
                            Text(
                              '1',
                              style: TextStyle(color: Colors.black54),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(125),
        ),
        onPressed: () {},
        backgroundColor: Color(0xFFFF7E36),
        elevation: 1,
        child: Icon(
          Icons.add_rounded,
          size: 36,
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        fixedColor: Color(0xFFFF7E36), // 활성화된 아이콘 색상
        unselectedItemColor: Colors.black, // 비활성화된 아이콘 색상
        showUnselectedLabels: true, // false == 비활성화된 오브젝트 label제거
        selectedFontSize: 12, // 활성화된 label 폰트 사이즈
        unselectedFontSize: 12, // 비활성화된 label 폰트 사이즈
        iconSize: 28, // 전체 아이콘 사이즈
        type: BottomNavigationBarType.fixed,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_filled),
            label: '홈',
            backgroundColor: Colors.white,
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.my_library_books_outlined),
            label: '동네생활',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.fmd_good_outlined),
            label: '내 근처',
          ),
          BottomNavigationBarItem(
              icon: Icon(CupertinoIcons.chat_bubble_2), label: '채팅'),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            label: '나의 당근',
          ),
        ],
        currentIndex: 0, // 활성화된 아이콘(index) 번호
      ),
    );
  }
}