Flutter – 우리 처음 만난 날 (토이 프로젝트)

  • 주요 스택
    • 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,
          ),
....