- 주요 스택
- Font 적용
- DatePicker
- 날짜 핸들링
- 테마 적용
asset 추가 (img / font)
assets: - asset/img/ fonts: - family: parisienne fonts: - asset: asset/font/Parisienne-Regular.ttf - family: sunflower fonts: - asset: asset/font/Sunflower-Light.ttf # weight 400 - asset: asset/font/Sunflower-Medium.ttf weight: 500 - asset: asset/font/Sunflower-Bold.ttf weight: 700
home screen 뼈대 생성 / 구조 정리
import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( child: Container( width: MediaQuery.of(context).size.width, // 꼭 외우기 (풀 사이즈) child: Column( children: [ Text( 'U&I', style: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), ), Text( '우리 처음 만난날', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 30.0, ), ), Text( '2021,12,27', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), IconButton( onPressed: () {}, icon: Icon( Icons.favorite, ), ), Text( 'D+1', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontWeight: FontWeight.w700, fontSize: 50.0, ), ), ], ), ), ), ); } }
상단 파트 분리
import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( child: Container( width: MediaQuery.of(context).size.width, child: _TopPart(), ), ), ); } } class _TopPart extends StatelessWidget { // 해당 파일에서만 사용할 것이므로 private (_) 로 선언 const _TopPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Column( children: [ Text( 'U&I', style: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), ), Text( '우리 처음 만난날', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 30.0, ), ), Text( '2021,12,27', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), IconButton( iconSize: 60.0, onPressed: () {}, icon: Icon( Icons.favorite, color: Colors.red, ), ), Text( 'D+1', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontWeight: FontWeight.w700, fontSize: 50.0, ), ), ], ); } }
하단 이미지 삽입
class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( child: Container( width: MediaQuery.of(context).size.width, // 꼭 외우기 (풀 사이즈) child: Column( children: [ _TopPart(), Image.asset('asset/img/middle_image.png'), // A RenderFlex overflowed by 229 pixels on the bottom. 에러 발생 :: 하단 229 픽셀 초과 ], ), ), ), ); } }
하단 분리 / 코드 정리
import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( child: Container( width: MediaQuery.of(context).size.width, // 꼭 외우기 (풀 사이즈) child: Column( children: [ _TopPart(), // Expanded 로 감싸면 절반씩 차지 _BottomPart(), ], ), ), ), ); } } class _TopPart extends StatelessWidget { // 해당 파일에서만 사용할 것이므로 private (_) 로 선언 const _TopPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Column( children: [ Text( 'U&I', style: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), ), Text( '우리 처음 만난날', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 30.0, ), ), Text( '2021,12,27', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), IconButton( iconSize: 60.0, onPressed: () {}, icon: Icon( Icons.favorite, color: Colors.red, ), ), Text( 'D+1', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontWeight: FontWeight.w700, fontSize: 50.0, ), ), ], ), ); } } class _BottomPart extends StatelessWidget { const _BottomPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Image.asset( 'asset/img/middle_image.png', // Expanded 로 감싸면 절반씩 차지 ), ); } }
디자인 최종
import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( bottom: false, child: Container( width: MediaQuery.of(context).size.width, child: Column( children: [ _TopPart(), // Expanded 로 감싸면 절반씩 차지 _BottomPart(), ], ), ), ), ); } } class _TopPart extends StatelessWidget { // 해당 파일에서만 사용할 것이므로 private (_) 로 선언 const _TopPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( 'U&I', style: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), ), Column( // 특정 텍스트 간격을 조정하고싶다면 Column() 으로 따로 묶어서 처리해도 됨 children: [ Text( '우리 처음 만난날', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 30.0, ), ), Text( '2021,12,27', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), ], ), IconButton( iconSize: 60.0, onPressed: () {}, icon: Icon( Icons.favorite, color: Colors.red, ), ), Text( 'D+1', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontWeight: FontWeight.w700, fontSize: 50.0, ), ), ], ), ); } } class _BottomPart extends StatelessWidget { const _BottomPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Image.asset( 'asset/img/middle_image.png', // Expanded 로 감싸면 절반씩 차지 ), ); } }
DatePicker 사용
import 'package:flutter/cupertino.dart'; // ios UI 구현을 할때 불러오는 패키지
import 'package:flutter/material.dart'; // 안드로이드 관련 디자인을 할때 불러오는 패키지
아이콘을 누르면 데이터 피커를 호출하도록 !
IconButton( iconSize: 60.0, onPressed: () { // dialog showCupertinoDialog( // 새로운 dialog 창을 호출할때 사용하는 함수 (참고로 cupertino는 apple 본사 위치ㅋㅋ) context: context, barrierDismissible: true, // container 밖을 누르면 dialog 가 닫히도록 하기 위해 builder: (BuildContext context) { return Align( alignment: Alignment.bottomCenter, // 특정 사이즈를 지정했는데 위젯이 전체 크기를 차지한다면, flutter 프레임워크에서 그 위젯을 어디에 정렬해야 할지 모르기 때문이다. 그럴땐 Align 으로 감싸서 어디 정렬할지 알려주면 됨 child: Container( color: Colors.white, height: 300.0, child: CupertinoDatePicker( // ios date picker 사용 mode: CupertinoDatePickerMode.date, onDateTimeChanged: (DateTime date) { print(date); // DateTime 이 변경될때마다 로그가 찍히는것을 볼 수 있다. }, ), ), ); }, ); },
Date 가 변경될때 build() 를 하도록 Stateful 위젯으로 변경
class _TopPartState extends State<_TopPart> { DateTime selectedDate = DateTime.now(); // @override Widget build(BuildContext context) { ... Text( '${selectedDate.year}.${selectedDate.month}.${selectedDate.day}', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), .... IconButton( iconSize: 60.0, onPressed: () { // dialog showCupertinoDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.white, height: 300.0, child: CupertinoDatePicker( // ios date picker 사용 mode: CupertinoDatePickerMode.date, onDateTimeChanged: (DateTime date) { setState(() { selectedDate = date; // date를 받아서 setState를 통해 build 호출 }); }, ), ), ); }, ); },
상태 관리 최상위로 변경 / 최종
데이터가 나눠져있으면 흐름을 보기가 힘들고, 찾기가 힘들다. 왠만하면 데이터를 한곳에다 모으고, 가장 부모인 클래스에서 데이터를 관리하는게 좋다. 부모 > 자식으로만 데이터를 보내줄 수 있기 때문인데, 예를 들어 _BottomPart() 에서도 _TopPart() 에서 사용하는 데이터가 필요하다고 했을때… 기존 구조로는 데이터 흐름을 만들 수 없다. (자식 > 부모 클래스로 데이터를 올려줄 수 없음)
그래서 항상 미래지향적(?)으로 조금 복잡하더라도 부모 클래스에서 데이터를 관리하는 습관을 들이는게 좋다.
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({Key? key}) : super(key: key); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { DateTime selectedDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pink[100], body: SafeArea( bottom: false, child: Container( width: MediaQuery.of(context).size.width, child: Column( children: [ _TopPart( selectedDate: selectedDate, onPressed: onHeartPressed, ), _BottomPart(), ], ), ), ), ); } void onHeartPressed() { final DateTime now = DateTime.now(); // dialog showCupertinoDialog( // 새로운 dialog 창을 호출할때 사용하는 함수 context: context, barrierDismissible: true, // container 밖을 누르면 dialog 닫힘 builder: (BuildContext context) { return Align( alignment: Alignment.bottomCenter, // 특정 사이즈를 지정했는데 위젯이 전체 크기를 차지한다면, flutter 프레임워크에서 그 위젯을 어디에 정렬해야 할지 모르기 때문이다. 그럴땐 Align 으로 어디 정렬할지 알려주면 됨 child: Container( color: Colors.white, height: 300.0, child: CupertinoDatePicker( // ios date picker 사용 mode: CupertinoDatePickerMode.date, initialDateTime: selectedDate, // 처음 선택 되어있는 날짜 maximumDate: DateTime( // 미래 날짜 선택 막기 now.year, now.month, now.day, ), onDateTimeChanged: (DateTime date) { setState(() { selectedDate = date; }); }, ), ), ); }, ); } } class _TopPart extends StatelessWidget { final DateTime selectedDate; final VoidCallback onPressed; _TopPart({required this.selectedDate, required this.onPressed, Key? key}) : super(key: key); @override Widget build(BuildContext context) { final now = DateTime.now(); return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( 'U&I', style: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), ), Column( children: [ Text( '우리 처음 만난날', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 30.0, ), ), Text( '${selectedDate.year}.${selectedDate.month}.${selectedDate.day}', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontSize: 20.0, ), ), ], ), IconButton( iconSize: 60.0, onPressed: onPressed, icon: Icon( Icons.favorite, color: Colors.red, ), ), Text( 'D+${DateTime( now.year, now.month, now.day, ).difference(selectedDate).inDays + 1}', style: TextStyle( color: Colors.white, fontFamily: 'sunflower', fontWeight: FontWeight.w700, fontSize: 50.0, ), ), ], ), ); } } class _BottomPart extends StatelessWidget { const _BottomPart({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Image.asset( 'asset/img/middle_image.png', ), ); } }
[참고] 테마 데이터 한곳에서 관리
theme: ThemeData( // theme 데이터를 한곳에서 관리할 수 있다. fontFamily: 'sunflower', textTheme: TextTheme( headline1: TextStyle( color: Colors.white, fontFamily: 'parisienne', fontSize: 80.0, ), headline2: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 50.0, ), bodyText1: TextStyle( color: Colors.white, fontSize: 30.0, ), bodyText2: TextStyle( color: Colors.white, fontSize: 20.0, ), ), ),
// 필요한 구간에서 final theme = Theme.of(context); // 테마를 가져올 수 있다. final textTheme = theme.textTheme; .... Text( 'U&I', style: textTheme.headline1, ), ....