외로운 Nova의 작업실

Flutter 프로그래밍 - 6(만난지 며칠 앱 만들기) 본문

Programming/Flutter

Flutter 프로그래밍 - 6(만난지 며칠 앱 만들기)

Nova_ 2024. 1. 8. 20:31

이번 장에서는 만난지 며칠지난지 알려주는 앱을 만들어보겠습니다. 이미지, 폰트등을 써서 예쁘게 만들어볼 예정입니다. 그리고 처음 만난날을 지정할때는 다이얼로그를 사용해서 만들어보겠습니다.

 

- 사전지식

다이얼로그중에서 IOS 스타일로된 showCupertinoDialog()함수를 사용할 예정입니다. 이 함수는 IOS 스타일로 실행되며 실행 시 모든 애니메이션과 작동이 iOS 스타일로 적용됩니다.

 

 

- 사전 준비

프로젝트 이름 : u_and_i

네이티브 언어 : 코틀린, 스위프트

 

<이미지 폰트추가>

 

<pubspec.yaml 설정>

이미지와 폰트를 pubspec.yaml 파일에 추가하겠습니다.

 

<프로젝트 초기화>

아래는 home_screen.dart 파일입니다.

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget{
  const HomeScreen({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context){
    return Scaffold(
      body: Text('HomeScreen'),
    );
  }
}

아래는 main.dart 파일입니다.

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

void main() {
  runApp(
      MaterialApp(
        home: HomeScreen(),
      )
  );
}

 

- 홈스크린 UI 구현

분홍색 배경에다가 화면을 반절로 나누어 위쪽에는 _DDay 클래스로 날짜를 표시하고 아래에는 _CoupleImage 클래스로 예쁜 이미지를 넣어보도록 하겠습니다.

 

먼저 배경을 먼저 핑크색으로 넣고 간단하게 클래스를 표현해보겠습니다.

아래는 home_screen.dart 파일입니다.

import 'package:flutter/material.dart';

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

  @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(),
            _CoupleImage()
          ],
        ),
      ),
    );
  }
}

class _DDay extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Text('DDay Widget');
  }
}

class _CoupleImage extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Text('Couple Tmage widget');
  }
}

 

 

이제 글자랑 이미지를 넣어보도록 하겠습니다.

import 'package:flutter/material.dart';

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

  @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(),
            _CoupleImage()
          ],
        ),
      ),
    );
  }
}

class _DDay extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Column(
      children: [
        const SizedBox(height: 16.0,),
        Text(//최상단 UI 글자
            'U&I'
        ),

        const SizedBox(height: 16.0,),
        Text(//두번쨰 글자//
            '우리 처음 만난 날'
        ),
        //임시로 만난날
        Text('2021.11.23'),

        const SizedBox(height: 16.0,),
        IconButton(onPressed: () {},
          icon: Icon(Icons.favorite),
          iconSize: 60.0,),

        const SizedBox(height: 16.0,),

        Text(// 임시 만난 후
            'D+465'
        )


      ],
    );
  }
}

class _CoupleImage extends StatelessWidget{
  @override
  Widget build(BuildContext context){

    return Center(
      child: Image.asset(
        'asset/img/middle_image.png',

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

 

글씨가 안예쁘기떄문에 main.dart파일에 텍스트와 IconButton 테마를 다운로드 받은 글씨체로 정의하고 사용해보겠습니다. 아래는 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(     // ➋ 글짜 테마를 적용할 수 있는 클래스
            headline1: TextStyle(   //  headline1 스타일 정의
              color: Colors.white,  //  글 색상
              fontSize: 80.0,       //  글 크기
              fontWeight: FontWeight.w700, //  글 두께
              fontFamily: 'parisienne',    //  글씨체
            ),
            headline2: TextStyle(
              color: Colors.white,
              fontSize: 50.0,
              fontWeight: FontWeight.w700,
            ),
            bodyText1: TextStyle(
              color: Colors.white,
              fontSize: 30.0,
            ),
            bodyText2: TextStyle(
              color: Colors.white,
              fontSize: 20.0,
            ),
          )
      ),
      home: HomeScreen(),
    ),
  );
}

 

아래는 home_screen.dart 파일입니다.

import 'package:flutter/material.dart';

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

  @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(),
            _CoupleImage()
          ],
        ),
      ),
    );
  }
}

class _DDay extends StatelessWidget{
  @override
  Widget build(BuildContext context){

    final textTheme = Theme.of(context).textTheme;

    return Column(
      children: [
        const SizedBox(height: 16.0,),
        Text(//최상단 UI 글자
            'U&I',
          style: textTheme.headline1,
        ),

        const SizedBox(height: 16.0,),
        Text(//두번쨰 글자//
            '우리 처음 만난 날',
          style: textTheme.bodyText1,
        ),
        //임시로 만난날
        Text('2021.11.23',
          style: textTheme.bodyText2,
        ),

        const SizedBox(height: 16.0,),
        IconButton(onPressed: () {},
          icon: Icon(Icons.favorite,
          color: Colors.red,),
          iconSize: 60.0,),

        const SizedBox(height: 16.0,),

        Text(// 임시 만난 후
            'D+465',
          style: textTheme.headline2,
        )


      ],
    );
  }
}

class _CoupleImage extends StatelessWidget{
  @override
  Widget build(BuildContext context){

    return Center(
      child: Image.asset(
        'asset/img/middle_image.png',

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

 

예쁘게 잘나왔습니다. 

 

<화면의 비율과 해상도에 따른 오버플로 해결>

핸드폰 화면의 비율과 해상도가 모두 달라서 이미지가 위쪽으로 올라와 오버플로 되는 상황이 나옵니다. 이러한 상황을 오버플로라고 합니다. 이때 Expanded 위젯을 사용하면 해결됩니다.

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,
          ),
        )
    );
  }
}

 

 - 상태 관리 연습해보기

이제 기능을 추가해보겠습니다. StatefulWidet에서 setState() 함수를 사용해서 상태관리를 하는 방법을 알아보겠습니다. 먼저 HomeScreen을 StatefulWidget으로 변경하고 하트를 누르면 "클릭"이 프린트되게 해보겠습니다.

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 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,
            ),
            _CoupleImage()
          ],
        ),
      ),
    );
  }
}

void onHeartPressed(){

  //하트 눌럿을떄 함수
  print('클릭');
}

class _DDay extends StatelessWidget {
  final GestureTapCallback onHeartPressed;

  _DDay({
    required this.onHeartPressed,
  }); // <- 세미콜론 빠진 부분

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Column(
      children: [
        const SizedBox(height: 16.0),
        Text(
          'U&I',
          style: textTheme.headline1,
        ),

        const SizedBox(height: 16.0),
        Text(
          '우리 처음 만난 날',
          style: textTheme.bodyText1,
        ),

        Text(
          '2021.11.23',
          style: textTheme.bodyText2,
        ),

        const SizedBox(height: 16.0),
        IconButton(
          onPressed: onHeartPressed,
          icon: Icon(
            Icons.favorite,
            color: Colors.red,
          ),
          iconSize: 60.0,
        ),

        const SizedBox(height: 16.0),

        Text(
          'D+465',
          style: textTheme.headline2,
        ),
      ],
    );
  }
}

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,
          ),
        )
    );
  }
}

잘 되는 것을 확인할 수 있습니다.

 

이제 처음만난날에 first_day 변수를 넣어놓고 first_day 변수를 오늘로 초기화해놓고 하트버튼을 누르면 first_day가 하루씩 줄어들고 D+ 부분에 얼마나 시간이 지났는지 계산하는 코드를 넣어보겠습니다.

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 firstday = DateTime.now();

  void onHeartPressed(){
    //하트 눌럿을떄 함수
    setState((){
      firstday = firstday.subtract(Duration(days: 1));
    });
  }

  @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()
          ],
        ),
      ),
    );
  }
}

class _DDay extends StatefulWidget {
  final GestureTapCallback onHeartPressed;
  final DateTime firstday;

  _DDay({
    required this.onHeartPressed,
    required this.firstday,
  });

  @override
  __DDayState createState() => __DDayState();
}

class __DDayState extends State<_DDay> {
  int dDay = 0;

  @override
  void initState() {
    super.initState();
    calculateDDay();
  }

  void calculateDDay() {
    final now = DateTime.now();
    final difference = now.difference(widget.firstday).inDays + 1;
    setState(() {
      dDay = difference < 0 ? 0 : difference;
    });
  }

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Column(
      children: [
        const SizedBox(height: 16.0),
        Text(
          'U&I',
          style: textTheme.headline1,
        ),

        const SizedBox(height: 16.0),
        Text(
          '우리 처음 만난 날',
          style: textTheme.bodyText1,
        ),

        Text(
          '${widget.firstday.year}.${widget.firstday.month}.${widget.firstday.day}',
          style: textTheme.bodyText2,
        ),

        const SizedBox(height: 16.0),
        IconButton(
          onPressed: () {
            widget.onHeartPressed();
            calculateDDay();
          },
          icon: Icon(
            Icons.favorite,
            color: Colors.red,
          ),
          iconSize: 60.0,
        ),

        const SizedBox(height: 16.0),

        Text(
          'D+$dDay',
          style: textTheme.headline2,
        ),
      ],
    );
  }
}


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,
          ),
        )
    );
  }
}

잘되는 것을 확인할 수 있습니다.

 

- 하트누르면 showCupertinoDialog 띄우기

이제 하트를 누르면 CupertinoDialog를 띄워서 처음 만난날을 정해보도록 하겠습니다. 아래는 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 firstDay = 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,
              firstDay: firstDay,
            ),
            _CoupleImage(),
          ],
        ),
      ),
    );
  }

  void onHeartPressed(){  // ➍ 하트 눌렀을때 실행할 함수
    showCupertinoDialog(  // ➋ 쿠퍼티노 다이얼로그 실행
      context: context,
      builder: (BuildContext context) {
        return Align(  // ➊ 정렬을 지정하는 위젯
          alignment: Alignment.bottomCenter,  // ➋ 아래 중간으로 정렬
          child: Container(
            color: Colors.white,  // 배경색 흰색 지정
            height: 300,  // 높이 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 글자
          'U&I',
          style: textTheme.headline1,
        ),
        const SizedBox(height: 16.0),
        Text(
          // 두번째 글자
          '우리 처음 만난 날',
          style: textTheme.bodyText1,
        ),
        Text(
          // 임시로 지정한 만난 날짜
          '${firstDay.year}.${firstDay.month}.${firstDay.day}',
          style: textTheme.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(
          // 만난 후 DDay
          'D+${DateTime(now.year, now.month, now.day).difference(firstDay).inDays + 1}',
          style: textTheme.headline2,
        ),
      ],
    );
  }
}

class _CoupleImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      // Expanded 추가
      child: Center(
        // ➊ 이미지 중앙정렬
        child: Image.asset(
          'asset/img/middle_image.png',

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

잘 되는 것을 확인할 수 있습니다.

Comments