[Flutter] Flutter 로 웹툰 앱 만들기: 헤더/버튼/카드/컴포넌트
리액트 시작 이유와 마찬가지로 플러터도 공부 시작~~~~
강의는 노마드 코더의 Flutter로 웹툰 앱 만들기이다.ㅎㅎ
목차
- 컴포넌트
- 이름 없는 인자
- 이름 있는 인자
- UI CHALLENGE
- 헤더
- 버튼
- 카드
1. 컴포넌트
1-1, 이름 없는 인자
class Player {
String name;
Player(this.name);
}
void main() {
var min0 = Player('minWachya');
min0.name;
}
1-2, 이름 있는 인자
class Player {
String name;
Player({required this.name});
}
void main() {
var min0 = Player(name: 'minWachya');
min0.name;
runApp(App());
}
생성자에 {}를 사용해 인자를 넣어주면 이름을 사용해 인자를 건네받을 수 있다.
required를 사용하면 필수 입력값이 된다.
2. UI CHALLENGE
위 ui와 동일한 ui를 만들어 볼 것이다!!
만들 순서는
1. 헤더
2. 버튼
3. 카드(유로, 달러 부분)이며,
버튼과 카드는 동일한 ui에 색이나 text만 다른 것이기 때문에
재사용을 위해 컴포넌트로 만들 것이다.
완성 화면은 아래와 같음
2-1. 헤더
대충 그림으로 그려보자면,,이런 너낌
MaterialApp(
home: Scaffold(
// 배경색
backgroundColor: Color(0xFF181818),
body:
// 양 옆 40 패딩: 초록색 부분
Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child:
// 열(세로)
Column(
children: [
// 헤더 위 공간
SizedBox(
height: 80,
),
// 행(가로)
Row(
// 열 오른쪽 정렬
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 열(세로)
Column(
// 텍스트 아이템 오른쪽 정렬
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('Hey, minWachya',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
)),
Text('Welcome back',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 18,
)),
],
), // 열 끝
],
) // 행 끝
],
), // 열 끝
), // 패딩 끝
),
);
코드만 보면 구조가 굉장히 복잡해보이는데...
직관적으로 위젯 구조를 확인해보면 위 사진과 같다.
2-2. 버튼
버튼은 버튼 글씨(text), 배경색(bgColor), 글씨 색(textColor)만 다르고,
나머지(border, padding...)는 동일하기 때문에
컴포넌트로 만들어보자
import 'package:flutter/material.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) {
// 컨테이너 생성(div같은 존재)
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(
fontSize: 20,
color: textColor,
)),
),
);
}
}
사용법은 아래와 같다.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Button(
text: 'transfer',
bgColor: Color(0xFFF1B33B),
textColor: Colors.black,
),
Button(
text: 'Request',
bgColor: Color(0xFF1F2123),
textColor: Colors.white,
),
],
),
3. 카드
import 'package:flutter/material.dart';
class CurrencyCard extends StatelessWidget {
final String name, code, amount; // ex) Dollar, USD, 12 334
final IconData icon; // 통화 아이콘
final bool isInverted; // 색반전 여부
final double cardOrder; // 카드 순서: offsetY를 사용해 겹쳐짐 효과 주기 위함.
final _blackColor = const Color(0xFF1F2123);
const CurrencyCard(
{super.key,
required this.name,
required this.code,
required this.amount,
required this.icon,
required this.isInverted,
required this.cardOrder});
@override
Widget build(BuildContext context) {
// 카드 순서에 따라 겹칩 효과 주기
return Transform.translate(
offset: Offset(0, -20 * cardOrder),
child: Container(
clipBehavior: Clip.hardEdge, // 컨테이너 밖까지 overflow된 거 자르기
decoration: BoxDecoration(
// 색반전 여부에 따라 색 변경
color: isInverted ? Colors.white : _blackColor,
borderRadius: BorderRadius.circular(25),
),
// 패딩
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 통화 이름
Text(
name,
style: TextStyle(
color: isInverted ? _blackColor : Colors.white,
fontSize: 30,
fontWeight: FontWeight.w600,
),
),
const SizedBox(
height: 10,
),
Row(
children: [
// 통화 양
Text(
amount,
style: TextStyle(
color: isInverted ? _blackColor : Colors.white,
fontSize: 18,
),
),
const SizedBox(width: 5),
// 통화 단위
Text(
code,
style: TextStyle(
color: isInverted ? _blackColor : Colors.white,
fontSize: 18,
),
),
],
),
],
),
// 아이콘 + 크기 확대
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(-5, 12),
child: Icon(
icon,
color: isInverted ? _blackColor : Colors.white,
size: 80,
),
),
)
],
),
),
),
);
}
}
따로 코드 설명할 부분은 없는 듯..ㅎㅎ
레이아웃 구조만 이해하면 너무나 직관적인 코드이기 때문에...
+
나는 Android Studio를 사랑하고,,, Android의 개발법에 익숙하기 때문에 비교를 안 할 수가 없다..ㅋㅋ
Column, Row만으로 레이아웃을 짜는 거 너무나도 Linear Layout의 그것과 같다...
안드에도 다양한 레이아웃이 있듯이 플러터에도 다른 레이아웃이 있길 바란다. 있어야만 한다...
이런 레이아웃 보기 너무 힘들고 만들기도 힘들어ㅎㅎ
제일 좋았던 경험은 역시 저장할 때마다 앱의 달라진 점을 바로바로 확인할 수 있다는 점!!
안드는 코드 조금만 고쳐도 빌드까지 다시 해야하는데,
플러터는 엔진으로 돌아가서 그런가,,, 그냥 저장만 해도 바뀐 게 바로바로 보인다...
빌드를 다시 안해도 됨...!!!!! 그냥 저장만 누르면 된다..!!!!
이건 혁명이야,,
너무,,너무 대박이다 이건 진짜 대박이다.
그리고 또 좋았던 점은 json같이 key: value로 이루어진 코드다.
안드 개발할 때 깃허브에서 코드를 보면 인자 중에 '이건 인자 이름이 뭐지,,'하면서 궁금할 때가 참 많았다.
물론 인자 작성할 때 Player(name="wachya") 이런 식으로 써주면 되긴 하지만, 안스에서 기본적으로 인자명을 써주기 때문에 생략하는 경우가 좀 있단말임...
근데 플러터는 인자마다 이름이 있는 경우가 대부분이라 직관적이어서 보기 좋은 거 같다.ㅎㅎ
정말 너무나 직관적이어서 개발자의 서러움을 꽁꽁 뭉쳐서 만들어낸 언어라는 걸 느꼈음...
기본 아이콘이나, 클래스명 위에 커서 올리면 해당 클래스의 문서를 보여준다거나, 이런 건 안드에도 있어서
그렇게 큰 매리트는,, 잘 모르겠음
물론 짱이긴 합니다. 짱!!
전체코드는 아래와 같다.
import 'package:flutter/material.dart';
import 'package:toonflix/widgets/Button.dart';
import 'package:toonflix/widgets/currency_card.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFF181818),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 80,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text('Hey, minWachya',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w800,
)),
Text('Welcome back',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 18,
)),
],
),
],
),
const SizedBox(
height: 40,
),
Text(
'Total Balance',
style: TextStyle(
fontSize: 22,
color: Colors.white.withOpacity(0.8),
),
),
const SizedBox(
height: 10,
),
const Text(
'\$5 194 482',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(
height: 30,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Button(
text: 'transfer',
bgColor: Color(0xFFF1B33B),
textColor: Colors.black,
),
Button(
text: 'Request',
bgColor: Color(0xFF1F2123),
textColor: Colors.white,
),
],
),
const SizedBox(
height: 40,
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Wallets',
style: TextStyle(
color: Colors.white,
fontSize: 36,
fontWeight: FontWeight.w600),
),
Text(
'View All',
style: TextStyle(
color: Colors.white.withOpacity(0.8), fontSize: 18),
),
],
),
const SizedBox(
height: 20,
),
const CurrencyCard(
name: 'Euro',
code: 'EUR',
amount: '6 428',
icon: Icons.euro_rounded,
isInverted: false,
cardOrder: 0,
),
const CurrencyCard(
name: 'Dollar',
code: 'USD',
amount: '55 622',
icon: Icons.attach_money_rounded,
isInverted: true,
cardOrder: 1,
),
const CurrencyCard(
name: 'Yen',
code: 'JPY',
amount: '15 344',
icon: Icons.currency_yen_rounded,
isInverted: false,
cardOrder: 2,
),
],
),
),
),
),
);
}
}