[Must Have 코드팩토리의 플러터 프로그래밍]

10 만난 지 며칠 U&I

avocado8 2024. 5. 4. 17:10

#상태관리 #CupertinoDatePicker #Dialog #DateTime

 

10.1 사전 지식

10.1.1 setState() 함수

State를 상속하는 모든 클래스에서 사용 가능

StatefulWidget의 렌더링이 끝난 클린 상태의 State ➡️ setSate()로 원하는 속성 변경 ➡️ 위젯이 더티 상태로 설정 ➡️ build() 재실행 ➡️ State가 클린 상태로 돌아옴

 

setState()는 매개변수로 콜백 함수를 입력받고, 이 콜백 함수에 변경하고 싶은 속성들을 입력해주면 해당 코드가 반영된 뒤 build()가 실행된다. 콜백 함수는 비동기로 작성될 수 없다.

 

10.1.2 showCupertinoDialog() 함수

다이얼로그를 실행하는 함수. 실행 시 모든 애니메이션과 작동이 iOS 스타일로 적용된다. cupertino.dart 패키지를 임포트해 사용한다.

showCupertinoDialog(
        context: context, //BuildContext 입력
        barrierDismissible: true, //외부(배리어) 탭 시 다이얼로그 닫음
        builder: (BuildContext context) { //다이얼로그에 들어갈 위젯
          return Text('dialog');
        },);

 

10.2 사전 준비

pubspec.yaml에 이미지와 폰트 추가

assets:
   - asset/img/

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

 

10.4 구현하기

10.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(
      body: SafeArea(
        top: true,
        bottom: false,
        child: Column(
          //위아래 끝에 위젯 배치
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _DDay(),
            _CoupleImage(),
          ],
        ),
      ),
    );
  }
}

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

class _CoupleImage extends StatelessWidget {
  Widget build(BuildContext context){
    return Text('CoupeImage Widget');
  }
}

 

_CoupleImage에 사진 적용

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

MediaQuery.of(context) 를 사용해 화면 크기와 관련된 기능 사용.

size 게터를 불러와 화면 전체의 너비와 높이를 가져올 수 있다.

 

* .of 생성자

.of(context)로 정의된 모든 생성자는 일반적으로 BuildContext를 매개변수로 받고 위젯트리에서 가장 가까이 있는 객체의 값을 찾아낸다.

앱이 실행되면 MaterialApp이 빌드됨과 동시에 MediaQuery가 생성되고, _CoupleImage 위젯에서 MediaQuery.of(context)를 실행하면 위젯트리를 올라가면 가장 가까운 곳에 위치한 MediaQuery (여기서는 MaterialApp에 있는 것) 값을 가져온다.

Theme.of(context), Navigator.of(context) 등에서도 사용

 

_DDay 위젯에 텍스트, 아이콘 적용


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

일단 임시값

 

main.dart 테마 설정

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

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        fontFamily: 'sunflower',
        textTheme: TextTheme(
          displayLarge: TextStyle(
            color: Colors.white,
            fontSize: 80.0,
            fontWeight: FontWeight.w700,
            fontFamily: 'parisienne',
          ),
          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(),
    ),
  );
}

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

fontFamily 기본 글씨체 지정

textTheme Text위젯테마 지정

tabBarTheme TabBar 위젯 테마 지정

cardTheme, appBarTheme, floatingActionButtonTheme, .... 등등

위젯이름Theme으로 위젯 테마 지정 가능

 

_DDay의 Text 위젯에 스타일 적용

class _DDay extends StatelessWidget {
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Column(
      children: [
        const SizedBox(height: 16),
        Text(
          'U&I',
          style: textTheme.displayLarge,
        ),
        const SizedBox(height: 16),
        Text(
          '우리 처음 만난 날',
          style: textTheme.bodyLarge,
        ),
        Text(
          '2022.02.02',
          style: textTheme.bodyMedium,
        ),
        const SizedBox(height: 16),
        IconButton(
          iconSize: 60,
          onPressed: () {},
          icon: Icon(
            Icons.favorite,
            color: Colors.red,
          ),
        ),
        const SizedBox(height: 16),
        Text(
          'D+100',
          style: textTheme.displayMedium,
        ),
      ],
    );
  }
}

Theme.of(context) 로 위젯트리 위 가장 가까운 Theme 값을 가져옴

* 변경한 theme은 MaterialApp의 매개변수에 입력했고, build()에 입력되지 않은 값이므로 핫리로드로는 반영X. 리스타트 필요

 

이미지 오버플로 해결하기

폰 크기에 따라 이미지가 잘릴 경우 Expanded 위젯 (남는 공간만큼 차지하도록 함)으로 감싸서 해결

 

10.4.2 상태 관리 연습해보기

_DDay의 하트 버튼 클릭 시 날짜를 변경할 수 있는 기능 구현

하트버튼의 onPressed가 _DDay 위젯에 있어서, _HomeScreenState에서 버튼이 눌렸을 때 콜백을 받을 수 없음. -> _DDay 위젯에 하트 아이콘을 눌렀을 때 실행할 콜백함수를 매개변수로 받아서 _HomeScreenState에서 상태관리를 할 수 있도록 해야 함


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();

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

 _DDay 위젯에 매개변수로 onHeartPressed 함수를 전달

void onHeartPressed() {
  print('pressed');
}
class _DDay extends StatelessWidget {
  final GestureTapCallback onHeartPressed; //탭했을 때 실행할 함수

  _DDay({
    required this.onHeartPressed, //상위에서 함수 입력받기
  });

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

    return Column(
      children: [
        const SizedBox(height: 16),
        Text(
          'U&I',
          style: textTheme.displayLarge,
        ),
        const SizedBox(height: 16),
        Text(
          '우리 처음 만난 날',
          style: textTheme.bodyLarge,
        ),
        Text(
          '2022.02.02',
          style: textTheme.bodyMedium,
        ),
        const SizedBox(height: 16),
        IconButton(
          iconSize: 60,
          onPressed: onHeartPressed, //눌렀을 때 실행할 함수 설정
          icon: Icon(
            Icons.favorite,
            color: Colors.red,
          ),
        ),
        const SizedBox(height: 16),
        Text(
          'D+100',
          style: textTheme.displayMedium,
        ),
      ],
    );
  }
}

실행 확인

생성자에 날짜값 추가, 콜백함수 수정해 날짜를 지금 기준으로 설정, 하트 클릭시 디데이 + 되도록 설정

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 = firstDay.subtract(Duration(days: 1));
    });
  }

  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 StatelessWidget {
  final GestureTapCallback onHeartPressed; //탭했을 때 실행할 함수
  final DateTime firstDay;

  _DDay({
    required this.onHeartPressed, //상위에서 함수 입력받기
    required this.firstDay, //firstDay 입력받기
  });

  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    final now = DateTime.now();

    return Column(
      children: [
        const SizedBox(height: 16),
        Text(
          'U&I',
          style: textTheme.displayLarge,
        ),
        const SizedBox(height: 16),
        Text(
          '우리 처음 만난 날',
          style: textTheme.bodyLarge,
        ),
        Text(
          '${firstDay.year}.${firstDay.month}.${firstDay.day}',
          style: textTheme.bodyMedium,
        ),
        const SizedBox(height: 16),
        IconButton(
          iconSize: 60,
          onPressed: onHeartPressed, //눌렀을 때 실행할 함수 설정
          icon: Icon(
            Icons.favorite,
            color: Colors.red,
          ),
        ),
        const SizedBox(height: 16),
        Text(
          //현재날짜 - firstDay로 디데이 설정
          'D+${DateTime(now.year, now.month, now.day).difference(firstDay).inDays + 1}',
          style: textTheme.displayMedium,
        ),
      ],
    );
  }
}


class _CoupleImage extends StatelessWidget (...생략)

 

10.4.3 CupertinoDatePicker로 날짜 선택 구현하기

void onHeartPressed() {
  showCupertinoDialog(context: context, builder: (BuildContext context) {
    return CupertinoDatePicker(onDateTimeChanged: (DateTime date) {},
      mode: CupertinoDatePickerMode.date,);
  },);
  setState(() {
    //firstDay에서 하루 빼기
    firstDay = firstDay.subtract(Duration(days: 1));
  });
}

builder 매개변수에 입력되는 함수에 다이얼로그에 보여줄 위젯 반환

CupertinoDatePicker : 스크롤을 통해 날짜를 pick. 정해진 값을 onDateTimeChange의 매개변수로 전달

mode는 날짜를 고르는 모드 지정 (date, time, dateAndTime...)

다이얼로그를 연 모습

DatePicker 수정, 닫기 구현

barrierDismissible : 외부 탭 시 다이얼로그 닫기 설정

Align : 자식 위젯을 어떻게 위치시킬지 정할 수 있음. alignment 매개변수에 Alignment값 입력해 배치 설정. Alignment의 정렬값은 topRight, topCenter, ceenter, centerRight, bottomRight, .... 등으로 지정

 

10.4.4 CupertinoDatePicker 변경 값 상태관리에 적용하기

CupertinoDatePicker 값이 변경되면 firstDay 값 변경

-> onDateTimeChanged에 setState 실행

void onHeartPressed() {
  showCupertinoDialog(
    context: context,
    builder: (BuildContext context) {
      return Align(
        alignment: Alignment.bottomCenter,
        child: Container(
          color: Colors.white,
          height: 300,
          child: CupertinoDatePicker(
            onDateTimeChanged: (DateTime date) {
              setState(() {
                firstDay = date;
              });
            },
            mode: CupertinoDatePickerMode.date,
          ),
        ),
      );
    },
    barrierDismissible: true,
  );
}

DatePicker로 설정한 날짜가 디데이에 적용됨

 

maximumDate를 현재날짜 + 1 (그냥 now를 쓰면 오류난다)로 설정해 미래 날짜를 설정할 수 없도록 할 수 있다.