[Flutter] 메모 앱 만들기 - 달력

천우산__ ㅣ 2024. 8. 30. 14:47

전에 React 로 메모를 작성하고, 완료한 메모를 인증할 수 있도록 하는 기능

완료한 수에 따라 경험치를 부여하여 메모 작성과 완료에 대한 동기를 부여하는 웹 앱을 만들어 본 경험이 있다.

해당 프로젝트는 Front: AWS S3 / Back: AWS EC2 를 사용해서 배포했지만,

많은 사이트들이 그렇듯, 홍보를 하지 않으니 사람들이 이용하지 않았고, 이용 해 본 사람들도 자주 이용하지 않게 되었다.

나조차도 별로 이용하지 않았았고, AWS 프리 티어 기간이 지나 유지하는 비용만 발생하니 해당 프로젝트는 내리고

앱으로 만들어보면 어떨까 해서 앱으로 재탄생 시켜보려고 한다.

 

이전에 구현했던 모든 기능들을 다 구현하지 않고 간단한 기능들만 남겨서 구현할 예정이다.

1. 일자 별로 메모를 작성하고, 완료할 수 있는 기능

2. 일자에 구애받지 않는 메모 (버킷리스트)

3. 기록한 메모들을 한 눈에 볼 수 있는 개인 화면

위와 같은 순으로 진행 해 볼 예정이고, 이번 글에서는 1번 중에서도 달력을 화면에 보여주는 과정을 기록한다.

 

1. 패키지 설치

TableCalendar 클래스를 이용해서 뷰를 구현해야 하는데, 이는 기본 패키지에 포함되어있지 않아

pubspec.ymal 파일에 패키지를 추가해야 한다.

// pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  table_calendar: ^3.0.7

 

2. 기본 뷰 만들기

Flutter 프로젝트를 시작하면, 자동으로 만들어진 화면이 있다.

이 화면 중에서 내가 사용할 부분들만 남겨놓고 나머지는 삭제했다.

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

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
  	
    const MyHomePage({
    	super.key,
  	});
  
    return const MaterialApp(
      home: MyHomePage(), // 내가 만들 뷰가 표기되는 곳
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("메모 앱"),
      ), // 화면 상단 App Bar 영역
      body: const Center(
        child: Column(
          children: [
            Text(" Body Section "),
          ], // 화면 메인 영역
        ),
      ),
    );
  }
}

위 코드에 대한 Flutter 미리보기 뷰

Stateless 위젯으로 구성하되, 달력과 메모 관련 기능들은 사용자 이용에 따라 State가 변경될 수 있으므로

Staleful 위젯으로 구현했다. 코드 중  MyHomePage 클래스가 실제 화면을 구현하는 역할을 하지 않고,

_MyHomePageState 클래스가 화면 기능을 한다. 그렇다면 언더바가 붙지 않은 클래스의 역할은 무엇일까?

해당 클래스에서는 위젯이 변하지 않는 부분을 정의한다고 한다.

이 클래스에서는 bulid 메서드가 없고, 상태를 관리하는 State 객체를 생성하고 반환하는 createState 매서드만 정의한다고 한다.

 

_MyHomePageState 클래스에서는 화면에 최상단 영역을 차지하는 Appbar 에 앱 이름을 넣어주고

화면 중앙에 보여줄 부분들은 Body를 통해 구현했다.

 

화면 구성은 모두 화면 기준 중앙 정렬으로 할 예정이라서 가장 위에 Center로 구현했으며,

구현되는 화면들은 세로 기준으로 달력, 메모 확인란이 될 것이므로 Column 으로 구현했다,

Column 에는 속하는 위젯들은 테스트를 위해 일단, Text 위젯을 넣어주었다.

 

3. 캘린더 뷰 삽입하기

캘린더를 넣어주기 전에 먼저, 위 코드에서 캘린더 패키지를 불러오는 부분에 대한 주석을 해제한다.

그 다음, _MyHomePageState  > body > Center > Column > Text 를 TableCalendar 로 변경 후 아래와 같이 입력한다.

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("메모 앱"),
      ),
      body: Center(
        child: Column(
          children: [
            TableCalendar(
              firstDay: DateTime.utc(2010, 10, 16),
              lastDay: DateTime.utc(2030, 3, 14),
              focusedDay: DateTime.now(),
            ),
          ],
        ),
      ),
    );
  }
}

이전 코드와 비교했을 때 보면, TableCalendar 가 추가된 것 외에도 다른 변경 사항이 있는데,

Center 위젯 앞에 const가 지워져있다.

const의 역할은, 앱이 실행되기 전에 미리 알고 있는 값인 경우, const로 지정해주면 컴파일이 더 빠르다는 것인데,

이는 모든 하위 구성이 모두 변하지 않는, 미리 알고 있어야 하는 값이여야 한다. 

TableCalendar 의 focusedDay 값이 오늘로 지정되어 있으므로, 실행 전에 미리 알 수 있는 값이 아니기 때문에

const를 해제했고, 그 위 부모 요소에도 const가 지정되어 있으면 해제해야 한다.

이후 저장을 하면 미리보기 뷰에 캘린더가 나타나게 된다.

캘린더가 표기된 Flutter 앱 미리보기

 

위 화면이 구현된 후 달력 월을 바꾸는 경우에는 정상적으로 동작하지만, 달력의 날짜를 클릭하면 아무런 동작도 하지 않는다.

이유는 달력 위젯을 만들 때 날짜가 선택되었을 때 어떻게 동작할지에 대한 정의를 내려주지 않았기 때문이다.

그렇다면 focusedDay 는 무엇일까? 이는 달력 위젯이 초기 로드 되었을 때, 어떤 날짜를 기준으로 달력을 보여줄 것인가에 대한 설정이다.

focuesedDay 값을 다른 월로 변경하면 초기 로드되는 월이 달라진다.

 

내 경우, 달력의 날짜를 클릭하면 해당 날짜에 하이라이트 되는 기능은 넣으려고 한다.

해당 방법은 TableCalendar 소개 사이트 에서 확인 가능하다.

 

table_calendar | Flutter package

Highly customizable, feature-packed calendar widget for Flutter.

pub.dev

가이드에 따라서 선택한 날짜에 하이라이트 될 수 있도록 코드를 수정했다.

먼저, 포커스 되는 날짜와 선택되는 날짜를 오늘로 지정, 앱이 실행될 때 해당 화면을 보이게끔 한다.

 

선택한 날짜에 하이라이트 되도록 하는 함수 : selectDayPredicate 

 

날짜를 선택했을 때 선택한 날짜와 포커스되는 날짜를 변경하는 함수 _onDaySelected 를 넣어주었다.

class _MyHomePageState extends State<MyHomePage> {
  DateTime _focusedDay = DateTime.now();
  DateTime _selectedDay = DateTime.now();

  void _onDaySelected(DateTime selecetedDay, DateTime focusedDay){
    setState(() {
      _selectedDay = selecetedDay;
      _focusedDay = focusedDay;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("메모 앱"),
      ),
      body: Center(
        child: Column(
          children: [
            TableCalendar(
              firstDay: DateTime.utc(2010, 10, 16),
              lastDay: DateTime.utc(2030, 3, 14),
              focusedDay: _focusedDay,
              selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
              onDaySelected: _onDaySelected,
            ),
          ],
        ),
      ),
    );
  }
}

오늘과 다른 날짜를 선택했을 때 예시 화면

원했던 것은, 날짜를 클릭했을 때 하이라이트 되는 날짜는 하나이길 원했는데,

가이드 문서에도 초기에 지정한 날짜가 남아있는 것으로 봐서는 오류는 아닌 것 같다. 문제가 된다면 나중에 수정하자.

 

4. 전체 코드

// Copyright 2019 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

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

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {

  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  DateTime _focusedDay = DateTime.now();
  DateTime _selectedDay = DateTime.now();

  void _onDaySelected(DateTime selecetedDay, DateTime focusedDay){
    setState(() {
      _selectedDay = selecetedDay;
      _focusedDay = focusedDay;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("메모 앱"),
      ),
      body: Center(
        child: Column(
          children: [
            TableCalendar(
              firstDay: DateTime.utc(2010, 10, 16),
              lastDay: DateTime.utc(2030, 3, 14),
              focusedDay: _focusedDay,
              selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
              onDaySelected: _onDaySelected,
            ),
          ],
        ),
      ),
    );
  }
}

오늘 목표로 했던 기능들은 구현이 끝난 것 같다.

다음으로는, 유저가 할 일을 입력하면 그 값을 받아서 화면에 표기하는 기능을 추가해야 한다.