티스토리 뷰
들어가며
본 게시글은 노마드코더의 Flutter로 웹툰 만들기를 보고 정리한 글입니다.
목표
해당 챕터에서는 Google 스타일의 MaterialApp 위젯이나 iOS 스타일의 Cupertino 위젯을 사용하는 대신 커스텀 스타일을 만들어보며 Flutter의 위젯 사용에 익숙해지는 것이다. 목표로하는 UI는 다음 이미지에서 보이는 디자인이다.
니코쌤이 자신이 코드 짤때의 순서도 살펴보길 바란다고 하시기에 순서에 집중하여 작성하겠다.
※ 나의 데스크 셋업의 경우 메모리가 8GB로 모바일 에뮬을 돌리기엔 턱없이 모자르므로 집에 굴러다니는 S10 공폰을 에뮬레이터로 사용하였다.
우선 현재의 기본 화면이다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Hello Flutter!'),
),
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
1. appBar 지우기
우선 appBar를 지워야 한다. Scaffold의 appBar는 ?로 필수가 아니기 때문에 단순히 Scaffold 인자에서 appBar를 지워주면 된다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
2. 배경색 설정
이제 앱의 배경색을 지정하려 한다. Scaffold에서는 backgroundColor라는 파라미터가 있기 때문에 단순히 인자만 추가해주면 된다.
우리가 만드려는 앱의 배경색은 검정색이기 때문에 Colors.black을 backgroundColor의 인자로 넘겨준다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Text('Hello World!'),
),
),
);
}
}
3. 요소 쌓기
우리가 만들려는 다음 화면은 row by row의 수직 구조로 요소들이 쌓여있다. 그렇기에 요소를 쌓기 위해 Column이란 위젯을 사용한다.
3.1 헤더 만들기
우리가 우선 만들 부분은 다음의 부분이다.
3.1.1 Column
기존의 사용하던 Center는 지우고 Column을 새로 이용한다. Column은 Center처럼 child 하나만 요구하지 않고 children의 리스트를 요구한다.
3.1.2 Row와 Text
각 children 리스트의 요소는 하나의 Row가 들어가게 된다. Row는 수평으로 쌓기 위한 것이고, 우리가 만들고 있는 제일 위의 Row는 Text 두 개로 이루어진 Column을 갖게 된다.
Text는 각각 'Hey, Selena'와 'Welcome back'이 될 것이다. Text는 style 파라미터로 꾸밀 수 있는데, 여기에 TextStyle 위젯을 넘겨주면 된다. 우리가 원하는 것은 흰 색의 글자이니 Colors.white를 TextStyle에 넘겨주면 된다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: Column(children: [
Row(
children: [
Column(children: [
Text('Hey, Selena',
style: TextStyle(
color:Colors.white,
),
),
Text('Welcome back',
style: TextStyle(
color: Colors.white,
),
),
],)
],
)
],)
),
);
}
}
3.1.3 SizedBox 추가
다만 이렇게만 하면 글자가 왼쪽 위에 바짝 붙어 있게 된다. 우선 윗 공간에 여백을 주기 위해 Column에 height가 80인 SizedBox를 추가한다.
3.1.4 Alignment
그리고 오른쪽으로 가게 하기 위해서 alignment라는걸 해준다. 이와 관련된 파라미터론 Row엔 수평 방향에 관한MainAxisAlignment가 있고 Column은 수직 방향에 관한 CrossAxisAlignment가 있다. 우리는 Row의 요소를 오른쪽으로 가게 하고 싶은 것이니 MainAxisAlighment.end를 Row의 mainAxisAlignment의 인자로 넘겨주면 된다.
Column으로 묶여있는 두 Text도 가운데 정렬이 아닌 오른쪽 정렬이 되길 바라기 때문에 crossAxisAlignment에 CrossAxisAlignment.end를 넘겨주면 된다.
3.1.5 Text 세부 설정
그 다음 Text의 폰트 크기를 살짝 키우고 굵게 하기 위해 아까 위에서 보았던 style의 TextStyle에서 fonSize와 fontWeight를 지정해주면 된다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: Column(children: [
SizedBox(
height: 80,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hey, Selena',
style: TextStyle(
color:Colors.white,
fontSize: 38,
fontWeight: FontWeight.w600,
),
),
Text('Welcome back',
style: TextStyle(
color: Colors.white,
),
),
],)
],
)
],)
),
);
}
}
아래 글짜는 살짝 반투명한데 color가 withOpacity라는 메소드를 지니고 있어 Colors.white.withOpacity(0.8) 이런 식으로 해주면 된다. 외에도 Text들의 세부 인자만 살짝식 바꿔주었다.
그리고 사실 배경색은 완전한 검정이 아닌 #181818인데, 이런 커스텀 색상 코드는 Colors.black 대신 Color(0xFF181818) 이런식으로 입력해주면 된다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Column(children: [
SizedBox(
height: 80,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hey, Selena',
style: TextStyle(
color:Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
),
),
Text('Welcome back',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 18,
),
),
],)
],
)
],)
),
);
}
}
3.1.6 Padding
여기서 요소를 보면 너무 오른쪽에 붙어있는 것을 볼 수 있다. 이는 padding을 주어서 해결이 가능한데, padding은 Padding으로 패딩을 줄 위젯을 감싸면 된다. 이 경우엔 Scaffold의 body 제일 바깥에 있는 Column을 감싸주면 된다. 여기서 파라미터 padding에 값을 넘겨주어 얼만큼 패딩을 줄지 지정할 수 있다. EdgeInsets.all(10) 하면 모든 방향에 10만큼에 패딩을 준다는 것이고, only를 쓰면 어느 방향에 줄지 4방으로 정할 수 있다. symmetric은 수직, 수평으로 정할 수 있다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Column(children: [
SizedBox(
height: 80,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hey, Selena',
style: TextStyle(
color:Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
),
),
Text('Welcome back',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 18,
),
),
],)
],
)
],),),
),
);
}
}
3.2 버튼 부분
그 다음은 버튼 부분을 만든다. 여기서 중점적으로 볼 것은 버튼 두 개의 형태가 매우 유사한 점을 이용해서 어떻게 DX를 높이며 개발할지를 살펴본다.
3.2.1 SizedBox
우선 버튼 섹션은 헤더 부분과 떨어져 있기 때문에 SizedBox를 Column에 추가해 서로 거리를 떨어뜨려준다.
3.2.2 텍스트 설정
'Total Balance'의 Text를 추가하고 TextStyle은 fontSize 22, 흰색에 불투명도 0.8로 지정하고, 가운데에 정렬되어 있는 Text가 왼쪽에 정렬될 수 있도록 제일 바깥 Column의 crossAxisAlignment를 start로 설정해준다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(...
Row(...
SizedBox(
height: 120,
),
Text('Total Balance',
style: TextStyle(
fontSize: 22,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
),
);
}
}
그 아래 '$5 194 482'도 비슷하게 추가하는데, 위아래 SizedBox를 추가했다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 80,
),
Row(...
SizedBox(...
Text(...
SizedBox(
height: 10,
),
Text('\$5 194 482',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(
height: 20,
),
],
),
),
),
);
}
}
3.2.3 버튼 Row
Row로 버튼을 수평으로 배치하는데, 여기서 버튼에는 Container라고 하는 HTML의 div같은 요소를 사용한다. 이 Container는 background color, border radius 등을 조작하며 요소를 꾸미는데 자주 사용된다.
Container의 decoration에 BoxDecoration으로 박스를 꾸며주었다. 배경색을 color로 호박색으로 하고 borderRadius로모서리를 둥글게 하였다. child에는 수직 수평 각각 20, 50 픽셀 패딩을 준 'Transfer' Text를 넘겨주었다.
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFF181818),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(...
Row(...
SizedBox(...
Text(...
SizedBox(...
Text(...
SizedBox(...
Row(
children: [
Container(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(45),
),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 20,
horizontal: 50,
),
child: Text('Transfer',
style: TextStyle(
fontSize: 20,
)
),
),
),
],
)
],
),
),
),
);
}
}
이 다음에 니코쌤이 Flutter에서 생산성을 올릴 수 있는 여러 팁을 설명해 주셨는데, 이 내용은 이 링크에 따로 정리해 놓았다.
그 옆에 바로 컨테이너 복사해서 넣었는데 에러가 난다. 보이듯 Flutter는 화면에 미리 어느 부분이 오류가 나는지 표시해 준다.
당장은 패딩을 줄여서 해결했고, 두 컨테이너를 감싸는 Row에 MainAxisAlignment.spaceBetween으로 두 버튼 사이를 떨어뜨려 주었다.
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF181818),
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(...
Row(...
const SizedBox(...
Text(...
const SizedBox(
height: 10,
),
const Text(...
const SizedBox(...
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(...
Container(
decoration: BoxDecoration(
color: const Color(0xFF1F2123),
borderRadius: BorderRadius.circular(45),
),
child: const Padding(
padding: EdgeInsets.symmetric(
vertical: 20,
horizontal: 50,
),
child: Text('Request',
style: TextStyle(
color: Colors.white,
fontSize: 20,
)),
),
),
],
)
],
),
),
),
);
}
}
그러나 이렇게 하면 Contatiner의 중복되는 부분을 반복적으로 사용하기 때문에 별로 보기 좋지 않다. 그렇기 때문에 Container를 별도의 위젯으로 따로 빼서 관리한다.
좌우의 버튼의 차이는 text와 background color 단 두개다. 위젯을 만드는 것은 간단하다. Container의 전구를 누른 후 Extract Widget을 누르고 이름을 지정해주면 된다.
그러면 다음 이미지들처럼 MyButton이란 새로운 커스텀 위젯을 만들어 주었다. 그러나 니코쌤은 Dart로 클래스와 컴포넌트 만드는 것이 익숙해지기 전까진 이 기능은 쓰지 않는다고 한다. 내 생각에도 이 기능이 너무 편리하고 강력해서 익숙해지기 전에 이걸 쓰면 바보가 될 것 같다.
대신 widgets 폴더를 따로 파서 이 안에 위젯을 짜주었다.
StatelessWidget을 상속받는 Button을 하나 만들고 각각에 입력 받을 Property와 이를 받아올 Constructor을 만든 다음 build 메소드를 오버라드 하여 기존에 만들었던 Container를 반환하면 된다. 그리고 앞서 선언한 Property를 적재적소에 넣어주면 완료다.
// widgets/button.dart
class Button extends StatelessWidget {
final String text;
final Color bgColor;
final Color textColor;
const Button(
{super.key,
required this.text,
required this.bgColor,
required this.textColor});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(45),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 50,
),
child: Text(text,
style: TextStyle(
color: textColor,
fontSize: 20,
)),
),
);
}
}
그리고 기존 Container를 사용한 부분을 우리가 만든 Button으로 대신 써 넣으면 끝이다.
// main.dart
const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Button(
text: 'Transfer',
bgColor: Color(0xFFF1B33B),
textColor: Colors.black,
),
Button(
text: 'Request',
bgColor: Color(0xFF1F2123),
textColor: Colors.white,
),
],
)
3.3 카드부분
마지막으로 카드 부분이다.
3.3.1 SizedBox랑 Text Row
우선 빠르게 여백이랑 텍스트를 배치한다. 여기서 볼 점은 Row에도 cross 축 main 축 다 있어서 crossAxisAlignment를 end로 두어 아래쪽으로 작은 텍스트가 붙도록 했다.
const SizedBox(
height: 100,
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Wallets',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Text(
'View All',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.8),
),
),
],
),
const SizedBox(
height: 20,
),
3.3.2 카드
카드는 아이콘과 네 방향 모두 동일하진 않은 border를 갖고있다. 카드는 Container로 만들며 카드 안에는 Padding을 가진 Row로 왼쪽엔 Text Column, 오른쪽엔 아이콘이 위치하게 된다. 우선 네 방향이 곡률이 동일한 Container를 만들고 그 안에 Text들 배치를 우선 하면 다음과 같다.
Container(
decoration: BoxDecoration(
color: const Color(0xFF1F2123),
borderRadius: BorderRadius.circular(25),
),
child: Padding(
padding: const EdgeInsets.all(30),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Euro',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(
height: 10,
),
Row(
children: [
const Text(
'6 428',
style: TextStyle(
fontSize: 20,
color: Colors.white,
),
),
const SizedBox(
width: 5,
),
Text(
'EUR',
style: TextStyle(
fontSize: 20,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
],
),
),
),
3.3.3 아이콘 추가
Flutter에는 Icon이라는 위젯이 따로 있다. Icons.을 하면 다음 이미지처럼 수많은 아이콘들이 이미 Flutter에 내장 돼 있는 것을 볼 수 있다. 이를 그대로 가져와 활용하면 된다.
이를 활용하여 아이콘을 배치하고 main axis를 spaceBetween으로 변경하면 아래 왼쪽 그림과 같은 모습이 된다. 그러나 우리가 만들어야 하는 그림을 오른쪽 그림처럼 컨테이너 영역을 벗어난 듯한 모습이다.
단순히 아이콘의 사이즈를 키우면 주변의 Container 같은 요소의 영역도 함께 커지기 때문에 Transform.scale에 Icon을 넘겨주어 아이콘의 크기만 키운다. 그 다음 Transform.translate의 offset을 건들여 주어 오른쪽 아래로 움직이도록 한다.
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(8, 15),
child: const Icon(
Icons.euro_rounded,
color: Colors.white,
size: 88,
),
),
),
Container 바깥으로 튀어나간 부분은 Container의 clipBehavior에 Clip.hardEdge를 지정하여 잘라주면 된다. clipBehavior는 Container에서 아이템이 overflow 됐을 때 어떻게 행동할지 지정하는 파라미터이다.
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: const Color(0xFF1F2123),
borderRadius: BorderRadius.circular(25),
),
child: Padding(
padding: const EdgeInsets.all(30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Euro',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(
height: 10,
),
Row(
children: [
const Text(
'6 428',
style: TextStyle(
fontSize: 20,
color: Colors.white,
),
),
const SizedBox(
width: 5,
),
Text(
'EUR',
style: TextStyle(
fontSize: 20,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(-5, 12),
child: const Icon(
Icons.euro_rounded,
color: Colors.white,
size: 88,
),
),
),
],
),
),
),
3.3.4 카드 위젯화
Container를 위젯화하여 좀더 보기 좋은 코드로 만든다.
import 'package:flutter/material.dart';
class CurrencyCard extends StatelessWidget {
final String name, code, amount;
final IconData icon;
final bool isInverted;
final _blackColor = const Color(0xFF1F2123);
const CurrencyCard({
super.key,
required this.name,
required this.code,
required this.amount,
required this.icon,
required this.isInverted,
});
@override
Widget build(BuildContext context) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: isInverted ? Colors.white : const Color(0xFF1F2123),
borderRadius: BorderRadius.circular(25),
),
child: Padding(
padding: const EdgeInsets.all(30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
color: isInverted ? _blackColor : Colors.white,
),
),
const SizedBox(
height: 10,
),
Row(
children: [
Text(
amount,
style: TextStyle(
fontSize: 20,
color: isInverted ? _blackColor : Colors.white,
),
),
const SizedBox(
width: 5,
),
Text(
code,
style: TextStyle(
fontSize: 20,
color: isInverted
? _blackColor
: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(-5, 12),
child: Icon(
icon,
color: isInverted ? _blackColor : Colors.white,
size: 88,
),
),
),
],
),
),
);
}
}
const CurrencyCard(
name: 'Euro',
code: 'EUR',
amount: '6 428',
icon: Icons.euro_rounded,
isInverted: false,
),
const CurrencyCard(
name: 'Bitcoin',
code: 'BTC',
amount: '9 785',
icon: Icons.currency_bitcoin,
isInverted: true,
),
const CurrencyCard(
name: 'Dollar',
code: 'USD',
amount: '428',
icon: Icons.attach_money_outlined,
isInverted: false,
),
그러나 위 이미지에 보이듯 Container가 영역을 벗어나기 때문에 스크롤 기능을 추가한다.
3.3.5 스크롤
스크롤은 간단하게 그냥 Scaffold body에 SingleChildScrollView를 감싸주면 된다. 그리고 카드끼리 겹치도록 해야하는데, 이건 Transform.translate로 해 준다.
const CurrencyCard(
name: 'Euro',
code: 'EUR',
amount: '6 428',
icon: Icons.euro_rounded,
isInverted: false,
),
Transform.translate(
offset: const Offset(0, -20),
child: const CurrencyCard(
name: 'Bitcoin',
code: 'BTC',
amount: '9 785',
icon: Icons.currency_bitcoin,
isInverted: true,
),
),
Transform.translate(
offset: const Offset(0, -40),
child: const CurrencyCard(
name: 'Dollar',
code: 'USD',
amount: '428',
icon: Icons.attach_money_outlined,
isInverted: false,
),
),
'공부한 내용 정리 > Flutter' 카테고리의 다른 글
Flutter 기초 공부 4: Pomodoro App (0) | 2024.07.22 |
---|---|
Flutter 기초 공부 3: 반응형 앱 (0) | 2024.07.17 |
Flutter 생산성 올리는 팁 (0) | 2024.07.05 |
Flutter 기초 공부 1: 작동방식 개요 (0) | 2024.01.14 |
Dart 기초 (0) | 2023.07.02 |