9단계 만난 지 며칠 U&I

송기영·2023년 12월 13일
0

플러터

목록 보기
11/25

9.1. 사전 지식

9.1.1. setState() 함수

setState() 함수는 매개변수 하나를 입력으로 받는다. 이 매개변수는 콜백 함수이며 비동기로 작성되면 안된다.

9.1.2. showCupertinoDialog() 함수

다이얼로그를 실행하는 함수이며, IOS 스타일로 적용된다.

import "package:clutter/cupertino.dart";

showCupertinodialog(
	context: context,
	barrierDismissible: true, // 다이얼로그 외부를 클릭시에 다이얼로그를 닫기 위함
	builder: (BuildContext context) {
		return Text("Dialog");
	}
);

9.2. 프로젝트 구성

9.2.1. 폰트 추가하기

pubspec.yaml 파일에 아래와 같이 폰트를 추가해준다. 폰트가 존재하는 경로는 asset/font 이다.

pub get을 실행해서 변경사항을 반영한다.

flutter:
  uses-material-design: true
  assets:
    - asset/img/

  fonts:
    - family: parisienne
      fonts:
        - asset: asset/font/Parisienne-Regular.ttf

    - family: sunflower
      fonts:
        - asset: asset/font/Sunflower-Light.ttf
        - asset: asset/font/Sunflower-Medium.ttf
          weight: 500
        - asset: asset/font/Sunflower-Bold.ttf
          weight: 700

9.3. 레이아웃 구상

_DDay위젯과 _CoupleImage 위젯 두가지로 나누어서 구현하며 홈 스크린 말고 CuperinoDialog를 추가로 구현하며 중앙의 하트아이콘을 누를 경우 실행되는 구조로 진행한다.

9.4. 구현하기

9.4.1. 홈 스크린 UI 구현하기

import "package:flutter/material.dart";

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);
  
  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(), _CoupleImage()],
        ),
      ),
    ));
  }
}

class _DDay extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return (Column(
      children: [
        const SizedBox(height: 16.0),
        Text("U&I"),
        const SizedBox(height: 16.0),
        Text("우리 처음 만난 날"),
        Text("2021.07.17"),
        const SizedBox(height: 16.0),
        IconButton(onPressed: () {}, icon: Icon(Icons.favorite)),
        const SizedBox(height: 16.0),
        Text("D+365")
      ],
    ));
  }
}

class _CoupleImage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Center(
      child: Image.asset(
        "asset/img/middle_image.png",
        // 화면의 반만큼 높이 구현
        height: MediaQuery.of(context).size.height / 2,
      ),
    );
  }
}

.of 생성자

위젯 트리에서 가장 가까이에 있는 객체의 값을 찾아낸다. Theme.of(context), Navigator.of(context) 등이 있음

테마지정

테마를 지정할 때 다트는 headline1, headline2, bodyText1, bodyText2는 displayMedium, displaySmall, bodyMedium, bodySmall으로 변경되었다.

// 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: const TextTheme(
          displayMedium: TextStyle(
              color: Colors.white,
              fontSize: 80.0,
              fontWeight: FontWeight.w700,
              fontFamily: "parisienne"),
          displaySmall: TextStyle(
              color: Colors.white, fontSize: 50.0, fontWeight: FontWeight.w700),
          bodyMedium: TextStyle(
            color: Colors.white,
            fontSize: 30.0,
          ),
          bodySmall: TextStyle(
            color: Colors.white,
            fontSize: 20.0,
          ),
        )),
    home: const HomeScreen(),
  ));
}

흔히 사용되는 ThemeData의 매개변수

| --- | --- |

Text 위젯에 스타일 적용

import "package:flutter/material.dart";

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);
  
  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(), _CoupleImage()],
        ),
      ),
    ));
  }
}

class _DDay extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // 테마 불러오기
    final textTheme = Theme.of(context).textTheme;

    return (Column(
      children: [
        const SizedBox(height: 16.0),
        Text("U&I", style: textTheme.displayMedium),
        const SizedBox(height: 16.0),
        Text("우리 처음 만난 날", style: textTheme.bodyMedium),
        Text(
          "2021.07.17",
          style: textTheme.bodySmall,
        ),
        const SizedBox(height: 16.0),
        IconButton(
            iconSize: 60.0,
            onPressed: () {},
            icon: const Icon(
              Icons.favorite,
              color: Colors.red,
            )),
        const SizedBox(height: 16.0),
        Text(
          "D+365",
          style: textTheme.displaySmall,
        )
      ],
    ));
  }
}

class _CoupleImage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Expanded(
        child: Center(
      child: Image.asset(
        "asset/img/middle_image.png",
        // 화면의 반만큼 높이 구현, Expanded가 우선 순위를 갖게 되어 무시된다.
        height: MediaQuery.of(context).size.height / 2,
      ),
    ));
  }
}

💡Tips: 핸드폰 화면 비율과 해상도가 모두 다르기 때문에 다른 핸드폰 화면에서는 UI 배치가 다르게 나올 때는 글자나 이미지의 크기를 임의로 조절하거나, 이미지가 남는 공간만큼만 차지하도록 코드를 작성하는 Expanded 위젯을 사용한다.

9.4.2. 상태 관리 연습해보기

import "package:flutter/material.dart";

// 상태관리를 위한 Stateful위젯 선언
class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime firstDay = DateTime.now();
  
  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() {
    // 상태 변경 시 setState() 함수 실행
    setState(() {
      // firstDay 변수에서 하루 빼기
      firstDay = firstDay.subtract(const Duration(days: 1));
    });
  }
}

class _DDay extends StatelessWidget {
  final GestureTapCallback onHeartPressed;
  final DateTime firstDay;

  const _DDay({
    required this.onHeartPressed,
    required this.firstDay,
  });

  
  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.displayMedium),
        const SizedBox(height: 16.0),
        Text("우리 처음 만난 날", style: textTheme.bodyMedium),
        Text(
          "${firstDay.year}.${firstDay.month}.${firstDay.day}",
          style: textTheme.bodySmall,
        ),
        const SizedBox(height: 16.0),
        IconButton(
            iconSize: 60.0,
            onPressed: onHeartPressed,
            icon: const 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.displaySmall,
        )
      ],
    ));
  }
}

class _CoupleImage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Expanded(
        child: Center(
      child: Image.asset(
        "asset/img/middle_image.png",
        // 화면의 반만큼 높이 구현, Expanded가 우선 순위를 갖게 되어 무시된다.
        height: MediaQuery.of(context).size.height / 2,
      ),
    ));
  }
}

9.4.3. CupertinoDatePicker로 날짜 선택 구현하기

showCupertinoDialog() 함수와 CupertinoDatePicker 위젯을 사용

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

// 상태관리를 위한 Stateful위젯 선언
class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);
  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  DateTime firstDay = DateTime.now();
  
  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() {
    // 상태 변경 시 setState() 함수 실행

    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;

  const _DDay({
    required this.onHeartPressed,
    required this.firstDay,
  });

  
  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.displayMedium),
        const SizedBox(height: 16.0),
        Text("우리 처음 만난 날", style: textTheme.bodyMedium),
        Text(
          "${firstDay.year}.${firstDay.month}.${firstDay.day}",
          style: textTheme.bodySmall,
        ),
        const SizedBox(height: 16.0),
        IconButton(
            iconSize: 60.0,
            onPressed: onHeartPressed,
            icon: const 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.displaySmall,
        )
      ],
    ));
  }
}

class _CoupleImage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Expanded(
        child: Center(
      child: Image.asset(
        "asset/img/middle_image.png",
        // 화면의 반만큼 높이 구현, Expanded가 우선 순위를 갖게 되어 무시된다.
        height: MediaQuery.of(context).size.height / 2,
      ),
    ));
  }
}

9.4.4. 심화

오늘 이후의 날짜는 선택하지 못하게 막는 코드

class _HomeScreenState extends State<HomeScreen> {
	<!-- 생략 -->
  void onHeartPressed() {
    // 상태 변경 시 setState() 함수 실행

    showCupertinoDialog(
      context: context,
      builder: (BuildContext context) {
        // 날짜 선택 다이얼로그
        final now = DateTime.now();
        return (Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              color: Colors.white,
              height: 300,
              child: CupertinoDatePicker(
                // 시간 제외 후 날짜만 선택
                mode: CupertinoDatePickerMode.date,
                onDateTimeChanged: (DateTime date) {
                  setState(() {
                    firstDay = date;
                  });
                },
                initialDateTime: now,
                maximumDate: now,
              ),
            )));
      },
      // 외부 탭할 경우 다이얼로그 닫기
      barrierDismissible: true,
    );
  }
}
profile
업무하면서 쌓인 노하우를 정리하는 블로그🚀 풀스택 개발자를 지향하고 있습니다👻

0개의 댓글