π Today I Learned 10νμ°¨ - [Flutter] U&I μ±μΌλ‘ D-Day κ³μ°νκΈ° π
π Flutter D-Day μ± κ°λ°
π― νμ΅ λͺ©ν
- StatefulWidgetμ μ΄μ©ν μν κ΄λ¦¬ μ΅νκΈ°
- setState ν¨μ μ¬μ©λ² μμ μ΄ν΄νκΈ°
- Cupertino λμμΈ μμ€ν νμ©νκΈ°
- MediaQueryλ₯Ό ν΅ν λ°μν UI ꡬννκΈ°
- ThemeDataλ₯Ό νμ©ν μΌκ΄λ λμμΈ μμ€ν ꡬμΆνκΈ°
π μ£Όμ λ΄μ©
β νλ‘μ νΈ κ°μ
U&I μ±μ μ¬κ·κΈ° μμν λ μ§λ₯Ό μ ννλ©΄ νμ¬κΉμ§ λ©°μΉ μ΄ μ§λ¬λμ§ κ³μ°ν΄μ£Όλ D-Day μ±μ λλ€.
π‘ ν΅μ¬ κΈ°λ₯: λ μ§ μ ν, D-Day κ³μ°, iOS μ€νμΌ UI ꡬν
μ΄λ² νλ‘μ νΈμμ μ§μ€ν ν΅μ¬ μμ:
StatefulWidget
μ μ΄μ©ν μν κ΄λ¦¬setState
ν¨μ μ¬μ© λ°©λ²- Cupertino (iOS μ€νμΌ) λμμΈ μμ€ν
- Flutterλ 2κ°μ§ λμμΈ μμ€ν
μ μ§μν©λλ€
- ꡬκΈμ Material Design (Android μ€νμΌ)
- μ νμ Cupertino Design (iOS μ€νμΌ)
- Flutterλ 2κ°μ§ λμμΈ μμ€ν
μ μ§μν©λλ€
β‘ ν΅μ¬ κ°λ 1: setState ν¨μ
State
λ₯Ό μμνλ λͺ¨λ ν΄λμ€λ setState ν¨μλ₯Ό μ¬μ©ν μ μμ΅λλ€.
setState ν¨μ μ€ν κ³Όμ :
- Clean μν - λ λλ§ μλ£ μν
- setState() - μν λ³κ²½ ν¨μ νΈμΆ
- Dirty μν - μ¬λ λλ§ νμ μν
- build() - μμ ― λ€μ λΉλ
- Clean μν - λ λλ§ μλ£
π‘ μ€μ: StatefulWidgetμ λ λλ§μ΄ λλλ©΄ Clean μνκ° λ©λλ€. μνλ₯Ό λ³κ²½νλ €λ©΄ λ°λμ setState ν¨μλ₯Ό μ¬μ©ν΄μΌ ν©λλ€!
setStateλ μ½λ°± ν¨μλ₯Ό λ°μΌλ©°, μ΄ μ½λ°± ν¨μ λ΄λΆμμ μν λ³κ²½ μμ μ μ§νν©λλ€.
β‘ ν΅μ¬ κ°λ 2: showCupertinoDialog
iOS μ€νμΌμ λ€μ΄μΌλ‘κ·Έλ₯Ό μ€ννλ ν¨μμ λλ€.
showCupertinoDialog(
context: context,// BuildContext μ
λ ₯
barrierDismissible: true, // μΈλΆ νν΄μ λ€μ΄μΌλ‘κ·Έ λ«μ μ μκ² νκΈ°
builder: (BuildContext context) { // λ€μ΄μΌλ‘κ·Έμ λ€μ΄κ° μμ ―
return Text('Dialog');
}
);
π» μ€μ΅ λ° μμ
π§ μ¬μ μ€λΉ: ν°νΈ λ±λ‘
ν°νΈ λ±λ‘μ μν΄ pubspec.yaml
νμΌμ μμ ν©λλ€:
fonts:
- family: parisienne # family ν€μ ν°νΈ μ΄λ¦μ μ§μ
fonts:
- asset: asset/font/Parisienne-Regular.ttf # λ±λ‘ν ν°νΈμ μμΉ
- family: sunflower
fonts:
- asset: asset/font/Sunflower-Light.ttf
- asset: asset/font/Sunflower-Medium.ttf
weight: 500 # ν°νΈμ λκ». FontWeight ν΄λμ€μ κ°κ³Ό κ°λ€.
- asset: asset/font/Sunflower-Bold.ttf
weight: 700
π‘ ν: Weightλ ν°νΈμ λκ»λ³λ‘ νμΌμ΄ λ°λ‘ μ 곡λκΈ° λλ¬Έμ κ°μ ν°νΈλΌλ λ€λ₯Έ λκ»λ₯Ό νννλ νμΌμ
weight
κ°μ λ°λ‘ μ§μ ν΄μΌ ν©λλ€. μ΄ν FlutterμμFontWeight
ν΄λμ€μ κ°κ³Ό λ§€μΉλ©λλ€ (μ:FontWeight.w500
)
π¨ λ μ΄μμ ꡬμνκΈ°
MediaQuery.of(context)λ₯Ό μ¬μ©νλ©΄ νλ©΄ ν¬κΈ°μ κ΄λ ¨λ κ°μ’ κΈ°λ₯μ μ¬μ©ν μ μμ΅λλ€:
size
κ²ν°λ‘ νλ©΄ μ 체μ λλΉμ λμ΄λ₯Ό μ½κ² κ°μ Έμ¬ μ μμ- νλ©΄ μ 체 λμ΄λ₯Ό 2λ‘ λλ μ νλ©΄ λμ΄μ μ λ°λ§νΌ μ΄λ―Έμ§κ° μ°¨μ§νλλ‘ μ€μ
π€ .of(context) μμ±μλ?
.of(context)
μμ±μλ μ΄λ³΄μλ€μ΄ λ§μ΄ ν·κ°λ €νλ κ°λ
μ
λλ€.
ν΅μ¬ μ리:
of(context)
λ‘ μ μλ λͺ¨λ μμ±μλBuildContext
λ₯Ό λ§€κ°λ³μλ‘ λ°μ- μμ ― νΈλ¦¬μμ κ°μ₯ κ°κΉμ΄μ μλ κ°μ²΄μ κ°μ μ°Ύμλ
λμ λ°©μ:
MediaQuery.of(context)
β νμ¬ μμ ― νΈλ¦¬μμ κ°μ₯ κ°κΉμ΄MediaQuery
κ°μ μ°Ύμ- μ± μ€ν μ
MaterialApp
μ΄ λΉλλλ©΄μMediaQuery
λ ν¨κ» μμ± - μμ ― νΈλ¦¬ μλμμ νΈμΆνλ©΄ μλ‘ μ¬λΌκ°λ©° κ°μ₯ κ°κΉμ΄ MediaQuery κ°μ κ°μ Έμ΄
π‘ λΉμ·ν μμ:
Theme.of(context)
,Navigator.of(context)
λ±
π¨ ν λ§(Theme) μμ€ν
Text
μμ ―μ κΈ°λ³Έ μ€νμΌμ λ³κ²½νκ³ μΆμ λλ ν
λ§λ₯Ό μ¬μ©νλ©΄ νΈλ¦¬ν©λλ€!
ν λ§μ μ₯μ :
- 13κ°μ§ Text μ€νμΌμ 미리 μ μνμ¬ νλ‘μ νΈ μ 체μμ μ¬μ¬μ© κ°λ₯
- μΌκ΄λ λμμΈ μμ€ν ꡬμΆ
μ£Όμ κ΅¬μ± μμ:
- ThemeData:
MaterialApp
μtheme
λ§€κ°λ³μλ‘, Flutterκ° κΈ°λ³Έ μ 곡νλ λλΆλΆ μμ ―μ κΈ°λ³Έ μ€νμΌ μ§μ - TextTheme: κΈμ ν λ§λ₯Ό μ ν μ μλ λ§€κ°λ³μ
μ£Όμ ThemeData λ§€κ°λ³μ:
λ§€κ°λ³μ | μ€λͺ |
---|---|
fontFamily | μ± μ 체 κΈ°λ³Έ ν°νΈ μ€μ |
textTheme | ν μ€νΈ μ€νμΌ ν λ§ μ μ |
tabBarTheme | νλ° μ€νμΌ ν λ§ |
cardTheme | μΉ΄λ μμ ― ν λ§ |
appBarTheme | μ±λ° ν λ§ μ€μ |
floatingActionButtonTheme | FAB ν λ§ |
elevatedButtonTheme | μΉκ²© λ²νΌ ν λ§ |
checkboxTheme | 체ν¬λ°μ€ ν λ§ |
π‘ ν¨ν΄: μμ ― μ΄λ¦ + Theme κ·μΉμ μ΄μ©ν΄μ νΉμ μμ ―μ ν λ§λ₯Ό μ½κ² μ€μ ν μ μμ΅λλ€.
π μ¬ν νμ΅
π¨ λ€μν νλ©΄ λΉμ¨κ³Ό ν΄μλμ λ°λ₯Έ μ€λ²νλ‘ ν΄κ²°νκΈ°
λ¬Έμ μν©:
- νΈλν°λ§λ€ νλ©΄μ λΉμ¨κ³Ό ν΄μλκ° λͺ¨λ λ€λ¦
- νλμ νλ©΄ κΈ°μ€μΌλ‘ UIλ₯Ό μμ νλ©΄ λ€λ₯Έ ν¬κΈ° νΈλν°μμ λ μ΄μμμ΄ κΉ¨μ§ μ μμ
ꡬ체μ μΈ μμ:
- νΈλν° ν¬κΈ°κ° μμμ μλ¨ κΈμλ€μ΄ νλ©΄μ μ λ° μ΄μμ μ°¨μ§
- μλμͺ½ μ΄λ―Έμ§κ° λ¨μ 곡κ°λ³΄λ€ λ λ§μ λμ΄λ₯Ό μꡬ
- λ¨μ 곡κ°μ΄ λΆμ‘±νλ° μ΄λ―Έμ§ ν¬κΈ°λ₯Ό νλ©΄μ μ λ°μΌλ‘ κ³ μ νκΈ° λλ¬Έ
π¨ Flutterμμλ μ΄λ₯Ό μ€λ²νλ‘(Overflow)λΌκ³ ν©λλ€
ν΄κ²° λ°©λ²:
- κΈμλ μ΄λ―Έμ§μ ν¬κΈ°λ₯Ό λμ μΌλ‘ μ‘°μ
- μ΄λ―Έμ§κ° λ¨μ 곡κ°λ§νΌλ§ μ°¨μ§νλλ‘ κ΅¬ν β
π‘ κΆμ₯ λ°©λ²: μ΄λ―Έμ§κ° λ¨μ 곡κ°λ§νΌ μ°¨μ§νλλ‘
Expanded
μμ ―μ μ¬μ©νλ κ²μ΄ μΌλ°μ μ λλ€.
π€ μΆκ° κ°λ λ€
GestureTapCallback:
- Material ν¨ν€μ§μμ κΈ°λ³Έ μ 곡νλ Typedef
- λ²νΌμ
onPressed
λλonTap
μ½λ°± ν¨μλ€μ΄GestureTapCallback
νμ μΌλ‘ μ μλ¨
Align μμ ―:
- μμ μμ ―μ μμΉλ₯Ό μ ν μ μλ μμ ―
- μμ:
Alignment.bottomCenter
β μλ μ€κ°μΌλ‘ μ λ ¬
λ€μ΄μΌλ‘κ·Έ μ΅μ :
barrierDismissible: true
β μΈλΆ νν κ²½μ° λ€μ΄μΌλ‘κ·Έ λ«κΈ°
Alignmentμ μ λ ¬ κ°:
μμ± | μμΉ |
---|---|
Alignment.topRight |
μ μ€λ₯Έμͺ½ |
Alignment.topCenter |
μ μ€μ |
Alignment.topLeft |
μ μΌμͺ½ |
Alignment.centerRight |
μ€μ μ€λ₯Έμͺ½ |
Alignment.center |
μ€μ |
Alignment.centerLeft |
μ€μ μΌμͺ½ |
Alignment.bottomRight |
μλ μ€λ₯Έμͺ½ |
Alignment.bottomCenter |
μλ μ€μ |
Alignment.bottomLeft |
μλ μΌμͺ½ |
π» μμ±λ μμ€ μ½λ
main.dart - μ±μ μ§μ μ κ³Ό ν λ§ μ€μ :
import 'package:flutter/material.dart';
import 'package:u_and_i/screen/home_screen.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(
// ν
λ§λ₯Ό μ§μ ν μ μλ ν΄λμ€
fontFamily: 'sunflower', // κΈ°λ³Έ κΈμ¨μ²΄
textTheme: TextTheme(
// κΈμ ν
λ§λ₯Ό μ μ©ν μ μλ ν΄λμ€
displayLarge: TextStyle(
// headline1 μ€νμΌ μ μ
color: Colors.white, // κΈ μμ
fontSize: 80.0, // κΈ ν¬κΈ°
fontWeight: FontWeight.w700, // κΈ λκ»
fontFamily: 'parisienne', // κΈμ¨μ²΄
),
displayMedium: TextStyle(
color: Colors.white,
fontSize: 50.0,
fontWeight: FontWeight.w700,
),
bodyLarge: TextStyle(color: Colors.white, fontSize: 30.0),
bodyMedium: TextStyle(color: Colors.white, fontSize: 20.0),
),
),
home: HomeScreen(),
),
);
}
home_screen.dart - λ©μΈ νλ©΄κ³Ό μν κ΄λ¦¬:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime firstDay = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink[100],
body: SafeArea(
top: true,
bottom: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // μ μλ λμ μμ ― λ°°μΉ
crossAxisAlignment: CrossAxisAlignment.stretch, // λ°λμΆ μ΅λ ν¬κΈ°λ‘ λ리기
children: [
_DDay(onHeartPressed: onHeartPressed, firstDay: firstDay),
_CoupleImage(),
],
),
),
);
}
void onHeartPressed() {
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.white,
height: 300,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
onDateTimeChanged: (DateTime date) {
setState(() {
firstDay = date;
});
},
),
),
);
},
barrierDismissible: true, // μΈλΆ νν κ²½μ° λ€μ΄μΌλ‘κ·Έ λ«κΈ°
);
}
}
class _DDay extends StatelessWidget {
final GestureTapCallback onHeartPressed;
final DateTime firstDay;
_DDay({required this.onHeartPressed, required this.firstDay});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final now = DateTime.now();
return Column(
children: [
const SizedBox(height: 16.0),
Text('U&I', style: textTheme.displayLarge),
const SizedBox(height: 16.0),
Text('μ°λ¦¬ μ²μ λ§λ λ ', style: textTheme.bodyLarge),
Text(
'${firstDay.year}.${firstDay.month}.${firstDay.day}',
style: textTheme.bodyMedium,
),
const SizedBox(height: 16.0),
IconButton(
iconSize: 60,
onPressed: onHeartPressed,
icon: Icon(Icons.favorite, color: Colors.red),
),
const SizedBox(height: 16.0),
Text(
'D+${DateTime(now.year, now.month, now.day).difference(firstDay).inDays + 1}',
style: textTheme.displayMedium,
),
],
);
}
}
class _CoupleImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: Center(
child: Image.asset(
'asset/img/middle_image.png',
// νλ©΄μ μ λ°λ§νΌ λμ΄ κ΅¬ν
height: MediaQuery.of(context).size.height / 2,
),
),
);
}
}
π λ§λ¬΄λ¦¬
β μ€λ λ°°μ΄ κ²
- StatefulWidgetκ³Ό setState: Flutterμμ μνλ₯Ό κ΄λ¦¬νλ ν΅μ¬ κ°λ
- Cupertino λμμΈ: iOS μ€νμΌ UI μ»΄ν¬λνΈ νμ©λ²
- MediaQuery: νλ©΄ ν¬κΈ°μ λ°λ₯Έ λ°μν UI ꡬν
- ThemeData: μΌκ΄λ λμμΈ μμ€ν κ΅¬μΆ λ°©λ²
- μ€λ²νλ‘ ν΄κ²°: Expanded μμ ―μ ν΅ν μ μ°ν λ μ΄μμ
λκΈλ¨κΈ°κΈ°