[앱개발]/Flutter, Dart, Figma

[플러터] D-Day 앱 만들기 (feat. 폰트 반영하기, Cupertino 위젯)

기록자_Recordian 2025. 5. 23. 14:52
728x90
반응형
이전 내용
 

[플러터] 이미지 롤링 기능 구현하기

이전 내용 [플러터] 웹 앱 만들어보기에러 관련 [플러터] 에러 발생 및 해결1. Gradle build failed to produce an .apk file...import 'package:flutter/material.dart';void main() { runApp( MaterialApp( // 머터리얼 디자인 위젯 h

puppy-foot-it.tistory.com

 

◆ 에러 관련

 

[플러터] 에러 발생 및 해결

1. Gradle build failed to produce an .apk file...import 'package:flutter/material.dart';void main() { runApp( MaterialApp( // 머터리얼 디자인 위젯 home: Scaffold( // Scaffold 위젯 body: Center( // 가운데 정렬 child: Text( // Text 위젯 'He

puppy-foot-it.tistory.com

 

◆ 깃허브 업로드 관련

 

 

[플러터] 대용량 파일 깃허브 업로드하기 (안드로이드 스튜디오)

대용량 파일 깃허브 업로드(안드로이드 스튜디오) 플러터 공부를 하다가 만들어둔 작업물들을 깃허브에 올리려고 하는데, 파일 용량이 크고, 파일 갯수도 많아서 GUI로 업로드가 안 된다. 이전에

puppy-foot-it.tistory.com

 


D-Day 앱 만들기

 

날짜를 지정하여 해당 날짜로부터 경과된 또는 남은 일자를 알려주는 앱을 만들어 본다.

주요 기능은

  • 사용자가 직접 날짜 선택
  • 해당 날짜를 기준으로 D-Day 및 경과일 (또는 남은 일자) 업데이트

폰트 추가하기

 

지난번에 이어 이번에는 폰트를 추가해 본다.

폰트 역시 이미지처럼 asset 디렉터리 내에 font 라는 디렉터리를 만들어 넣어주면 된다.

 

[폰트 다운로드 방법: 구글 폰트]

먼저 구글 폰트에 접속해서 

https://fonts.google.com/

 

Browse Fonts - Google Fonts

Making the web more beautiful, fast, and open through great typography

fonts.google.com

 

 

필자는 'Jua' 라는 폰트를 다운받아 보려 한다.

오른쪽 상단에 [Get font] 버튼을 누르고

[Download all] 버튼 클릭하여 다운로드

 

안드로이드 스튜디오로 돌아와서 asset 디렉터리 생성하고, 그 안에 font 디렉터리를 생성하여 폰트 파일(ttf 또는 oft)을 넣어준다.

 

그리고나서 pubsepc.yaml 파일에 폰트를 추가해주면 된다.

flutter:
  uses-material-design: true
  
  # 폰트 추가
  fonts:
  - family: jua    
    fonts:
      - asset: asset/font/Jua-Regular.ttf # 등록할 폰트 파일 위치

 

★ 만약 하나의 폰트에 여러 버전이 있다면?

아래의 Sour Gummy 폰트의 경우, 다양한 버전이 있다.

https://fonts.google.com/specimen/Sour+Gummy?lang=en_Latn&categoryFilters=Feeling:%2FExpressive%2FCute

 

Sour Gummy - Google Fonts

 

fonts.google.com

 

이 중에서 사용할 폰트 몇 가지만 골라 font 폴더에 넣어주고, pubspec.yaml에 추가해 준다.

  - family: SourGummy
    fonts:
      - asset: asset/font/SourGummy-Bold.ttf
        weight: 700
      - asset: asset/font/SourGummy-Light.ttf
        weight: 500 # 폰트 두께
      - asset: asset/font/SourGummy-Medium.ttf

 

weight는 폰트 두께를 의미하는데, 폰트의 두께별로 파일이 따로 제공되기 때문에 같은 폰트라도 다른 두께를 표현하는 파일은 weight 값을 따로 표현해줘야 한다.

※ 두께값은 100부터 900까지 100단위 (숫자가 높을수록 두꺼움)

 

폰트를 적용했으면 터미널에 pub get 명령어를 입력하여 변경 사항을 반영하면 된다.

flutter pub get

프로젝트 구조

 

프로젝트의 주요 구조는 아래와 같다.

 

asset 디렉터리: 폰트, 이미지 등 파일이 담겨 있는 폴더

- font: 폰트 파일이 담겨 있는 폴더

- img: 이미지 파일이 담겨 있는 폴더

 

lib: Flutter 애플리케이션의 핵심 코드와 기능이 담겨 있는 디렉터리 

-screen 디렉터리: 화면 구성과 관련된 모든 위젯이 모여있는 디렉터리

  • home_screen.dart: 홈 화면 구성
  • main.dart: 애플리케이션 진입점

pubspec.yaml: Flutter 프로젝트 관련 설정


D-Day 앱 구현 관련 중요 사항

 

◆ setState() 함수:

  • 상태를 변경하고 build()를 재호출하여 UI를 다시 그릴 때 호출되는 함수로, State를 상속하는 모든 클래스는 setState() 함수를 사용할 수 있다.
  • setState() 함수를 시랭해서 원하는 속성들이 변경되면, 위젯의 상태가 dirty로 설정되며 build() 함수가 재실행된다. 그리고나서 State가 다시 클린 상태로 되돌아 온다.
  • setState() 함수는 콜백 함수를 매개변수로 입력받는데, 이 콜백 함수에 변경하고 싶은 속성들을 입력해주면 해당 코드가 반영되며 build() 함수가 실행된다. ▶ 콜백 함수는 비동기로 작성되면 안된다.

◆ Cupertino 위젯:

import 'package:flutter/cupertino.dart';

showCupertinoDialog( // 쿠퍼티노 다이얼로그 실행
      context: context,
      builder: (BuildContext context) {
		...
      },
      barrierDismissible: true, // 외부 탭 시 다이얼로그 닫기
  • showCupertinoDialog() 함수: 다이얼로그를 실행하는 함수
  • 참고로, Cupertino는 Apple 본사가 있는 곳인데, 이를 통해 해당 패키지의 작동과 애니메이션이 iOS 스타일 일 것으로 짐작할 수 있다.
  • Cupertino 위젯을 사용하기 위해서는 Cupertino 패키지를 꼭 import 해야 한다.
  • 모든 showDialog() 형태의 함수들은 BuildContext를 반드시 입력해야 한다
  • CupertinoDatePicker: Cupertino 패키지에서 기본으로 제공하는 위젯으로, 스크롤을 통해 날짜를 정할 수 있고, 정해진 값을 콜백 함수의 매개변수로 전달
  • CupertinoDatePicker의 mode 매개변수는 날짜를 고르는 모드 지정 가능. (date: 날짜 / time: 시간 / dateAndTime: 날짜와 시간)
  • 배리어(barrier): 다이얼로그 위젯 위에 흐림 처리가 된 부분.
  • barrierDismissible: true : 배리어를 눌렀을 때 다이얼로그가 닫히고 false를 입력하면 닫히지 않는다.

◆ .of 생성자:

  • .of 생성자는 상위 위젯에 접근해서 정보를 가져오는 방법으로, 위에서 전달된 정보(상위 위젯)를 하위에서 꺼내 쓰기 위해 사용한다.
  • .of(context)로 정의된 모든 생성자는 일반적으로 BuildContext를 매개변수로 받고 위젯 트리에 있는 객체의 값을 찾아 낸다.
  • .of(context)는 BuildContext가 생성된 이후에만 사용 가능하므로, initState()에서는 쓸 수 없고, build() 함수에서 사용.
  • MediaQuery.of(context): 현재 위젯트리에서 가장 가까이에 있는 MediaQuery 값을 찾아 낸다.
  • 자주 사용하는 .of 생성자로는 Theme.of(context), MediaQuery.of(context), Navigator.of(context) 등이 있다.
메서드 설명
Theme.of(context) 테마 정보 (글꼴 크기, 색상 등) 가져오기
MediaQuery.of(context) 화면 크기, 방향, 패딩 정보 등 가져오기
Navigator.of(context) 화면 전환 (페이지 이동) 기능 사용하기
ScaffoldMessenger.of(context) 스낵바(Snackbar) 띄우기
DefaultTabController.of(context) 탭(Tab) 상태 가져오기

 

◆ ThemeData:

  • ThemeData는 Flutter 앱의 전반적인 디자인 스타일을 설정하는 객체로, ThemeData는 글꼴, 색상, 버튼 스타일 등을 앱 전체에서 통일되게 관리하게 도와주는 디자인 템플릿이다.
  • ThemeData는 MaterialApp 위젯의 theme 속성에서 사용하며, 플러터가 기본으로 제공하는 대부분의 위젯의 기본 스타일을 지정할 수 있다.
  • 주요 속성
매개변수 타입 설명 예시
appBarTheme AppBarTheme 앱바 스타일 지정 AppBarTheme(color: Colors.pink)
backgroundColor Color 앱 전체의 일반 배경색 Colors.grey[200]
bottomNavigationBarTheme BottomNavigationBarThemeData 하단 네비게이션 바 스타일 BottomNavigationBarThemeData(...)
buttonTheme ButtonThemeData (구형) 버튼 스타일 지정 ButtonThemeData(...)
cardTheme CardTheme 카드(Card) 위젯 스타일 CardTheme(...)
checkboxTheme CheckboxThemeData 체크박스 스타일 CheckboxThemeData(...)
colorScheme ColorScheme 앱의 색상 체계 전반을 구성 ColorScheme.fromSwatch(...)
dialogTheme DialogTheme AlertDialog 등 대화창 스타일 DialogTheme(...)
dividerColor Color Divider(구분선)의 색상 Colors.grey
elevatedButtonTheme ElevatedButtonThemeData ElevatedButton 스타일 지정 ElevatedButtonThemeData(style: ElevatedButton.styleFrom(...))
floatingActionButtonTheme FloatingActionButtonThemeData FAB (둥근 버튼) 스타일 FloatingActionButtonThemeData(...)
fontFamily String 기본 글씨체 지정 'SourGummy', 'NotoSansKR' 등
iconTheme IconThemeData 아이콘 스타일 (색상, 크기 등) IconThemeData(color: Colors.red)
inputDecorationTheme InputDecorationTheme 텍스트 입력 필드 스타일 InputDecorationTheme(...)
primaryColor Color 주요 색상 (앱 전체에 가장 많이 사용) Colors.blue
scaffoldBackgroundColor Color Scaffold 배경색 (주요 화면 배경) Colors.white
sliderTheme SliderThemeData 슬라이더 위젯 스타일 SliderThemeData(...)
switchTheme SwitchThemeData 스위치 토글 버튼 스타일 SwitchThemeData(...)
tabBarTheme TabBarTheme 탭바(TabBar) 스타일 TabBarTheme(...)
textTheme TextTheme 전체 텍스트 스타일 정의 TextTheme(displayLarge: TextStyle(...))

 

◆ Align 위젯

  • 자식 위젯을 어떻게 위치시킬지 정할 수 있다.
  • alignment 매개변수에는 Alignment 값 입력 가능.
  • Alignment.bottomCenter 처럼 Alignment. 뒤에 정렬값을 입력하면 된다.
  • topRight, topCenter, topLeft, centerRight, center, centerLeft, bottomRight, bottomLeft, bottomCenter 등

Dart 파일 코드 및 설명

 

* main.dart *

import 'package:flutter/material.dart';
import 'package:d_day/screen/home_screen.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData( // 테마 지정 클래스
        fontFamily: 'SourGummy', // 기본 글씨체
        textTheme: TextTheme( // 글자 테마 적용 클래스
          displayLarge: TextStyle( // headline1 스타일 정의
            color: Colors.white, // 글자 색상
            fontSize: 80.0, // 글자 크기
            fontWeight: FontWeight.w700, // 글자 두께
            fontFamily: 'jua', // 글씨체
        ),
        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(),
    ),
  );
}

 

[코드 구조]

파트 설명
main() 함수 앱 실행 시작점
runApp() 최상위 위젯으로 앱 시작
MaterialApp 앱 전체를 감싸고 테마, 홈화면 등을 설정
ThemeData 전체 테마 스타일 정의
TextTheme 재사용 가능한 글자 스타일 모음
HomeScreen 첫 화면으로 보여줄 위젯 (직접 만든 클래스)

 

★ MaterialApp

  • theme: 앱 전반의 디자인 테마 설정(글꼴, 색상 등)
  • home: 앱 시작 시 첫 화면으로 보여줄 위젯 설정(HomeScreen)
theme: ThemeData( // 테마 지정 클래스
    fontFamily: 'SourGummy', // 기본 글씨체
    textTheme: TextTheme( // 글자 테마 적용 클래스
      displayLarge: TextStyle( // headline1 스타일 정의
        color: Colors.white, // 글자 색상
        fontSize: 80.0, // 글자 크기
        fontWeight: FontWeight.w700, // 글자 두께
        fontFamily: 'jua', // 글씨체
    ),

 

 

- theme: 앱 전역 글꼴/스타일 테마 설정

설정 설명
fontFamily: 'SourGummy' 전체 앱의 기본 글꼴을 ‘SourGummy’로 설정
특정 TextStyle에서 fontFamily: 'jua' 특정 스타일에서만 다른 글꼴(jua) 적용

 

- TextTheme: 앱에서 자주 쓰이는 텍스트 스타일을 한 곳에 모아 설정하는 클래스

이름 실제 용도 설정 값
displayLarge 큰 제목 (headline1) 흰색, 80pt, 굵음, jua 폰트
displayMedium 중간 제목 (headline2) 흰색, 50pt, 굵음
bodyLarge 본문 큰 글씨 흰색, 30pt
bodyMedium 본문 일반 크기 흰색, 20pt

* home_screen.dart *

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  DateTime birthday = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.pink[100], // 배경색
      body: SafeArea( // 시스템 UI 피해 UI 그리기
        top: true,
        bottom: false,
        child: Column(
          // 위아래 끝에 위젯 배치
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          // 반대축 최대 크기로 늘리기
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _DDay(
              onHeartPressed: onHeartPressed, // 하트 눌렸을 때 콜백 함수
              birthday: birthday, // 생일 전달
            ),
            _FamilyImage(),
          ],
        ),
      ),
    );
  }

  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() 함수 실행
                setState(() {
                  birthday = date;
                });
              },
          ),
        ),
        );
      },
      barrierDismissible: true, // 외부 탭 시 다이얼로그 닫기
    );
  }
}

class _DDay extends StatelessWidget {

  // 하트 눌렀을 때 실행 함수
  final GestureTapCallback onHeartPressed;
  final DateTime birthday; // 출생 (예정)일

  _DDay({
    required this.onHeartPressed, // 상위에서 함수 입력 받음
    required this.birthday, // 날짜 변수 입력 받음
});

  @override
  Widget build(BuildContext context) {
    // 테마 불러오기
    final textTheme = Theme.of(context).textTheme;
    final now = DateTime.now(); // 현재 날짜 시간
    final differenceInDays = now.difference(birthday).inDays;

    String dDayText;
    if (differenceInDays < 0) {
      // 출생 전
      dDayText = 'D-${-differenceInDays}';
    } else if (differenceInDays > 0) { // 출생 이후
      dDayText = 'D+${differenceInDays}';
    } else { // 출생 당일
      dDayText = 'D-Day';
    }

    return Column(
      children: [
        const SizedBox(height: 16.0),
        Text ( // 최상단 글자
          'My Baby',
          style: textTheme.displayLarge, // headline1 스타일 적용
        ),
        Text(
          '우리 아가 만난 날',
          style: textTheme.bodyLarge, // bodyText1 적용
        ),

        Text ( // 임시 저장 날짜
          // DateTime을 년.월.일 형태로 변경
          '${birthday.year}.${birthday.month}.${birthday.day}',
          style: textTheme.bodyMedium, // bodyText2 적용
        ),
        const SizedBox(height: 16.0),
        IconButton( // 하트 아이콘 버튼
          iconSize: 60.0,
          onPressed: onHeartPressed, // 아이콘 눌렀을 때 실행 함수
          icon: Icon(
            Icons.favorite,
            color: Colors.red, // 색상 빨강
          ),
        ),
        const SizedBox(height: 16.0),
        Text( // 출생 (예정)일
          dDayText, // D-Day 계산 문자열 불러옴
          style: textTheme.displayMedium, // headline2 적용
        ),
      ],
    );
  }
}

class _FamilyImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded( // 오버플로 해결
      child: Center( // 이미지 중앙 정렬
        child: Image.asset(
          'asset/img/happy_family_without_bg.png',

          // 화면의 반만큼 높이 구현 (Expanded 우선 순위)
          height: MediaQuery.of(context).size.height / 2,
        ),
      ),
    );
  }
}

 

파일 구조

  • HomeScreen: 앱의 메인 화면
  • _DDay: D-Day 정보를 찾아주는 위젯
  • _FamilyImage: 하단에 표시되는 (가족) 이미지

- HomeScreen 클래스

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {

  DateTime birthday = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.pink[100], // 배경색
      body: SafeArea( // 시스템 UI 피해 UI 그리기
        top: true,
        bottom: false,
        child: Column(
          // 위아래 끝에 위젯 배치
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          // 반대축 최대 크기로 늘리기
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _DDay(
              onHeartPressed: onHeartPressed, // 하트 눌렸을 때 콜백 함수
              birthday: birthday, // 생일 전달
            ),
            _FamilyImage(),
          ],
        ),
      ),
    );
  }

  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() 함수 실행
                setState(() {
                  birthday = date;
                });
              },
          ),
        ),
        );
      },
      barrierDismissible: true, // 외부 탭 시 다이얼로그 닫기
    );
  }
}

 

[코드 구조]

구성요소 역할
HomeScreen 메인 화면 클래스
_HomeScreenState 상태를 관리하고 UI를 그리는 부분
birthday 사용자가 선택한 날짜 (출생일)
onHeartPressed() 하트를 누르면 날짜 선택 창(CupertinoDatePicker)을 띄움
  • StatefulWidget: 출생일이 변경되면 화면을 다시 그려야 하므로, 동적 UI에 사용할 수 있는 위젯인 StatefulWidget 사용.
  • State<HomeScreen>: HomeScreen의 상태를 관리하며, 모든 화면의 내용은 이 클래스의 build() 함수 안에서 결정된다.
  • DateTime 타입의 변수인 birthday 에는 기본값으로 현재 날짜(DateTime.now())를 넣어주며, 사용자가 날짜를 바꾸면 setState()를 통해서 업데이트 된다.
  • build()는 앱의 화면을 그리는 코드
 build() 함수
- Scaffold: 화면 기본 구조 (앱바, 바디, 플로팅 버튼 등 삽입 가능)
- SafeArea: 시스템 UI(노치, 상태바 등)를 피해 콘텐츠 배치
- Column: 세로로 위젯 배치. (위쪽: _DDay / 아래쪽: _FamilyImage)
- mainAxisAlignment.spaceBetween: 맨 위와 맨 아래에 하나씩 배치
- crossAxisAlignment.stretch: 가로로 최대한 늘림
  • onHeartPressed() 함수는 하트를 눌렀을 때 실행되는 함수로, HomeScreen에서 받은 생일과 하트 클릭 롤백을 사용
★ onHeartPressed() 함수
- showCupertinoDialog(: iOS 스타일 다이얼로그를 화면에 띄움.
- alignment: Alignment.bottomCenter,: 다이얼로그 정렬 (하단 중앙)
- mode: CupertinoDatePickerMode.date,: 시간없이 날짜만 고르는 선택창
- onDateTimeChanged: 날짜를 바꾸면 setState()가 실행되어 birthday 값을 새로 저장하고 setState()가 호출되면 플러터는 화면 전체를 다시 그려 변경 사항이 반영됨.
- barrierDismissible: true,: 다이얼로그 외부를 누르면 닫힘.

- _DDay 클래스

class _DDay extends StatelessWidget {

  // 하트 눌렀을 때 실행 함수
  final GestureTapCallback onHeartPressed;
  final DateTime birthday; // 출생 (예정)일

  _DDay({
    required this.onHeartPressed, // 상위에서 함수 입력 받음
    required this.birthday, // 날짜 변수 입력 받음
});

  @override
  Widget build(BuildContext context) {
    // 테마 불러오기
    final textTheme = Theme.of(context).textTheme;
    final now = DateTime.now(); // 현재 날짜 시간
    final differenceInDays = now.difference(birthday).inDays;

    String dDayText;
    if (differenceInDays < 0) {
      // 출생 전
      dDayText = 'D-${-differenceInDays}';
    } else if (differenceInDays > 0) { // 출생 이후
      dDayText = 'D+${differenceInDays}';
    } else { // 출생 당일
      dDayText = 'D-Day';
    }

    return Column(
      children: [
        const SizedBox(height: 16.0),
        Text ( // 최상단 글자
          'My Baby',
          style: textTheme.displayLarge, // headline1 스타일 적용
        ),
        Text(
          '우리 아가 만난 날',
          style: textTheme.bodyLarge, // bodyText1 적용
        ),

        Text ( // 임시 저장 날짜
          // DateTime을 년.월.일 형태로 변경
          '${birthday.year}.${birthday.month}.${birthday.day}',
          style: textTheme.bodyMedium, // bodyText2 적용
        ),
        const SizedBox(height: 16.0),
        IconButton( // 하트 아이콘 버튼
          iconSize: 60.0,
          onPressed: onHeartPressed, // 아이콘 눌렀을 때 실행 함수
          icon: Icon(
            Icons.favorite,
            color: Colors.red, // 색상 빨강
          ),
        ),
        const SizedBox(height: 16.0),
        Text( // 출생 (예정)일
          dDayText, // D-Day 계산 문자열 불러옴
          style: textTheme.displayMedium, // headline2 적용
        ),
      ],
    );
  }
}

 

[코드 구조]

구성요소 역할
StatelessWidget 상태를 갖지 않는 위젯
GuestureTapCallback Material 패키지에서 기본으로 제공하는 Typedef
onHeartPressed 하트 버튼을 누르면 실행되는 함수 (날짜 선택창 띄우기)
birthday 기준 날짜 (출생일)
differenceInDays 현재 날짜와 birthday의 차이 일수
dDayText D+N, D-N, D-Day 등으로 표시되는 문자열
UI 요소 텍스트, 하트 아이콘 등
  • class _DDay extends StatelessWidget {: 화면에 표시되는 D-Day 관련 정보만 보여주는 화면 조각으로, 상태를 바꾸지 않는 StatelessWidget 이다. ▶ 상태(날짜)가 바뀌면 부모 위젯이 다시 만들어준다.
  • 생성자와 필드
final GestureTapCallback onHeartPressed;
final DateTime birthday;

_DDay({
  required this.onHeartPressed,
  required this.birthday,
});
- onHeartPressed: 하트 누를 때 실행할 함수 → 부모가 넘겨줌
- birthday: 기준 날짜 (출생일, 예정일 등) → 부모가 넘겨줌
  • build() 함수: 화면에 어떤 UI를 그릴지 정의하는 함수
  • final textTheme = Theme.of(context).textTheme;: MaterialApp에서 정의한 테마 (폰트 크기, 스타일 등) 불러오기
  • 현재 날짜와 차이 계산
final now = DateTime.now();
final differenceInDays = now.difference(birthday).inDays;
- now: 오늘 날짜
- birthday: 사용자가 선택한 날짜
- differenceInDays:  오늘 - 출생일 = 날짜 차이 (양수면 출생 후, 음수면 출생 전)
  • dDayText: D-Day 표시 문자열 함수
String dDayText;
if (differenceInDays < 0) {
  dDayText = 'D-${-differenceInDays}';
} else if (differenceInDays > 0) {
  dDayText = 'D+${differenceInDays}';
} else {
  dDayText = 'D-Day';
}
  • 타이틀 텍스트: Text('My Baby', style: textTheme.displayLarge) ▶ 큰 글씨 / Text('우리 아가 만난 날', style: textTheme.bodyLarge), ▶ 부제목 (작게)
  • 하트 버튼
IconButton(
  iconSize: 60.0,
  onPressed: onHeartPressed,
  icon: Icon(Icons.favorite, color: Colors.red),
),
하트 아이콘을 누르면 부모에서 받은 onHeartPressed() 함수가 실행되며, 이 함수는 날짜 선택 다이얼로그를 연다.
  • D-Day 결과 출력
const SizedBox(height: 16.0),
    Text( // 출생 (예정)일
      dDayText, // D-Day 계산 문자열 불러옴
      style: textTheme.displayMedium, // headline2 적용
    ),

 

- _FamilyImage 클래스

class _FamilyImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded( // 오버플로 해결
      child: Center( // 이미지 중앙 정렬
        child: Image.asset(
          'asset/img/happy_family_without_bg.png',

          // 화면의 반만큼 높이 구현 (Expanded 우선 순위)
          height: MediaQuery.of(context).size.height / 2,
        ),
      ),
    );
  }
}

 

[코드 구조]

구성 요소 설명
StatelessWidget 상태를 가지지 않는 위젯
Expanded 남은 공간을 모두 차지함
Center 자식 위젯을 가운데 정렬
Image.asset 앱 내부에 저장된 이미지 표시
MediaQuery 디바이스 화면 크기를 가져오는 도구
  • class _FamilyImage extends StatelessWidget {: '_'로 시작하므로 이 위젯은 private 위젯이며, 무상태 위젯으로 항상 같은 UI를 보여줌
  • return Expanded(: 부모 Column의 남은 공간을 전부 차지
  • Center: 안에 있는 위젯(Image.asset)을 가운데 정렬하고, Image.asset 내부에 있는 이미지 파일 경로를 출력 (단, 해당 경로는 pubspec.yaml 파일에 등록되어 있어야 함)
child: Center( // 이미지 중앙 정렬
    child: Image.asset(
      'asset/img/happy_family_without_bg.png',
  • height: MediaQuery.of(context).size.height / 2
- MediaQuery.of(context).size.height: 현재 기기 화면의 전체 높이 (단위: 픽셀)
- / 2: 화면의 절반 높이만큼 이미지 높이를 설정
▶ 이렇게 하면 화면이 커지거나 작아져도 자동으로 조절 → 반응형 UI

Image.asset: 내부에 저장된 이미지 불러옴.
MediaQuery: 기기의 화면 크기 가져옴.

 

[화면 예시]

|-------------------------------------|
|          My Baby (텍스트)          |
|     우리 아가 만난 날 (텍스트)     |
|         2025.5.23 (텍스트)         |
|           ❤️ (하트 버튼)          |
|            D+3 (텍스트)           |
|-------------------------------------|
|             [이미지]               |
|        (화면 절반 높이 정도)       |
|-------------------------------------|

 


완성화면

 

 

하트를 클릭하면 다이얼로그가 열리면서 날짜를 선택할 수 있고, 선택된 날짜에 맞춰 D-Day가 변경된다.


[참고]

코드팩토리의 플러터 프로그래밍


다음 내용

 

728x90
반응형