나는 이렇게 논다/Flutter 로 간단한 노트 앱을 만들어보자

Flutter로 멋진 노트 앱을 만들어보자 ④ - 노트 알림 UI 추가하기

daco2020 2024. 11. 26. 23:28
반응형

지난 글 마지막 말에 앱 출시를 다룬다고 했었습니다. 현재는 앱 출시를 위해 검수 및 비공개 테스트 중입니다. 비공개 테스트의 경우 2주 동안 진행되기 때문에 해당 기간 동안 노트 알림 기능을 추가로 구현해 보겠습니다.

 

노트 알림 기능은 작성한 노트를 다시 리마인드 하는 기능입니다. 제가 만들고 있는 노트 앱은 단순히 기록하는 것뿐만 아니라 자신의 노트를 다시 발견하고, 과거 아이디어를 재활용할 수 있도록 돕는 것이 목표입니다. (그래야 멋진 노트 앱이죠~😉)

 

알림 기능은 크게 두 가지 기능을 기획했습니다. 하나는 예약 알림으로 특정 날짜와 시간에 특정 노트를 사용자에게 띄워주는 것입니다. 다른 하나는 반복 알림입니다. 특정 노트를 일정한 간격으로 반복적인 알림을 주는 기능입니다. 

 

예약 알림 특정 날짜, 시간에 떠올려야 하는 아이디어가 있는 경우 사용합니다. 반복 알림은 규칙적으로 떠올려야 하는 아이디어가 있을 때 사용합니다.

 

이번 글에서는 노트 앱의 알림 기능, 그중에서 UI 부분을 구현해보겠습니다.

 

최종 결과는 다음과 같습니다.

 

- 노트 카드를 누르면 알림을 설정할 수 있는 종 모양의 아이콘을 추가합니다.

- 하단 내비게이션 바에 노트 알림 탭을 추가합니다.

- 노트 알림 탭을 누르면 알림이 설정된 알림 카드들을 볼 수 있습니다.

 

 

노트 알림 UI 구현은 다음 목차 순서대로 진행합니다.

목차

  1. 알림 탭 추가
  2. 알림 관리 화면 구현
  3. 알림 버튼 UI 추가
  4. 알림 설정 다이얼로그 구현

 

이번 글부터는 변경된 모든 코드를 보여주기보다는 주요 변경점 위주로 설명하겠습니다. 그 이유는 제가 보여드리는 코드는 생략된 내용이 많기 때문에 그대로 사용하기보다는 구현의 주요 흐름만 보는 것을 추천드립니다.

 

 

1. 알림 탭 추가

하단 내비게이션 바에 새로운 탭 "노트 알림"을 추가합니다. 이 탭은 알림 관리 화면(노트 알림)으로 연결됩니다.

 

먼저 BottomNavBar 컴포넌트에 BottomNavigationBarItem을 추가합니다.

BottomNavigationBarItem(
  icon: Padding(
    padding: EdgeInsets.only(top: 4, bottom: 4),
    child: Icon(Icons.notifications_outlined),
  ),
  label: '노트 알림',
)

 

 

그리고 TabItem 이넘에 noteAlarm이라는 요소를 추가합니다.

enum TabItem { settings, noteAlarm, randomNote, noteList }

 

 

라우팅 설정에서는 TabItem.noteAlarm에 대응하는 경로를 추가합니다.

GoRoute(
  path: '/noteAlarm',
  name: TabItem.noteAlarm.name,
  pageBuilder: (context, state) => CustomTransitionPage(
    key: state.pageKey,
    child: NoteAlarmView(), // 알림 관리 화면
    transitionDuration: const Duration(milliseconds: 200),
    transitionsBuilder: _fadeTransition,
  ),
),

 

 

2. 알림 관리 화면 구현

알림 관리 화면은 기존 노트 목록처럼 ListView를 이용해 알림 카드들을 리스트 형태로 나열하겠습니다.

 

이때, 알림 카드는 노트 내용과 알림 설정 상태를 표시하며 알림 활성화 스위치를 조작할 수 있습니다. 

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('노트 알림'),
      ),
      body: Consumer<NoteService>(
        builder: (context, noteService, child) {
          final notes = noteService.notes;

          if (notes.isEmpty) {
            return Center(
              child: Text(
                '알림 설정된 노트가 없습니다.',
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
                    ),
              ),
            );
          }

          return ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: notes.length,
            itemBuilder: (context, index) {
              final note = notes[index];
              return Container(
                margin: const EdgeInsets.only(bottom: 16),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surface,
                  borderRadius: BorderRadius.circular(12),
                  boxShadow: [
                    BoxShadow(
                      color: Theme.of(context)
                          .colorScheme
                          .shadow
                          .withOpacity(0.08),
                      blurRadius: 8,
                      offset: const Offset(0, 2),
                    ),
                  ],
                ),
                child: ListTile(
                  contentPadding: const EdgeInsets.all(16),
                  title: Text(
                    note.content,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  subtitle: Row(
                    children: [
                      Icon(
                        Icons.access_time,
                        size: 16,
                        color: Theme.of(context).colorScheme.onSurfaceVariant,
                      ),
                      const SizedBox(width: 4),
                      Text(
                        '매일 오전 9시', // 임시 텍스트
                        style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              color: Theme.of(context).colorScheme.onSurfaceVariant,
                            ),
                      ),
                    ],
                  ),
                  trailing: Switch(
                    value: true, // 알림 활성화 상태
                    onChanged: (bool value) {
                      // TODO: 알림 활성화/비활성화 로직 추가
                    },
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

 

아직 알림에 대한 데이터가 없기 때문에 알림 카드의 내용을 하드 코딩으로 작성합니다. 이번 글에서는 UI 느낌을 먼저 본다고 생각하시면 됩니다.

 

UI 구현이 정상적으로 되었다면 아래와 같은 모습이 그려집니다.

 

 

 

3. 알림 버튼 UI 추가

'노트 카드'와 '랜덤 카드'에 알림 설정 버튼을 추가합니다. 사용자가 이 버튼을 누르면 알림 설정 다이얼로그를 표시하도록 만듭니다.

_buildActionButton(
  context: context,
  icon: Icons.notifications_outlined,
  color: Theme.of(context).colorScheme.noteGreen,
  onTap: () {
    if (onNotification != null) {
      onNotification!(note.id); // 알림 설정 다이얼로그 호출
    }
  },
),

 

 

onNotification 콜백은 아래와 같은 임시 알림 다이얼로그를 표시하도록 연결합니다.

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('알림 설정'),
    content: const Text('알림 설정 기능은 준비 중입니다.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('확인'),
      ),
    ],
  ),
);

 

 

아래 이미지처럼 종 모양 아이콘이 추가되고 아이콘을 누르면 임시 다이얼로그가 표시됩니다.

 

 

 

4. 알림 설정 다이얼로그 구현

사용자가 노트별로 알림을 예약하거나 반복 알림을 설정할 수 있도록 알림 설정 다이얼로그를 구현해 봅시다. 

 

두 가지 알림 유형(예약 알림과 반복 알림)을 선택하고 설정할 수 있는 다이얼로그 UI 컴포넌트를 만들겠습니다.

 

 

1) 알림 설정 다이얼로그 UI

class NotificationSettingsDialog extends StatefulWidget {
  final String noteId;
  final Function(DateTime dateTime)? onScheduleNotification;
  final Function(List<int> weekdays, TimeOfDay time)? onRecurringNotification;

  const NotificationSettingsDialog({
    Key? key,
    required this.noteId,
    this.onScheduleNotification,
    this.onRecurringNotification,
  }) : super(key: key);
  
  ...
  
}

 

이렇게 만든 다이얼로그 클래스를 랜덤 카드 컴포넌트노트 목록 화면에 적용해 줍니다. 아까 구현한 알림 설정 버튼의 onNotification 에 넣어주면 됩니다.

onNotification: (id) {
  showDialog(
    context: context,
    builder: (context) => NotificationSettingsDialog(
      noteId: id,
      onScheduleNotification: (dateTime) {
        print('예약 알림: $dateTime');
      },
      onRecurringNotification: (weekdays, time) {
        print('반복 알림: $weekdays, ${time.hour}:${time.minute}');
      },
    ),
  );
},

 

다이얼로그를 연결했으니 이제 내부 UI를 채우겠습니다. 예약 알림 설정 UI반복 알림 설정 UI를 넣어봅시다.

 

 

2) 예약 알림 설정 UI

if (!_isRecurring) ...[
  ListTile(
    title: const Text('날짜 및 시간'),
    subtitle: Text(
      '${_selectedDateTime.year}년 ${_selectedDateTime.month}월 ${_selectedDateTime.day}일 '
      '${_selectedDateTime.hour}시 ${_selectedDateTime.minute}분',
    ),
    trailing: const Icon(Icons.calendar_today),
    onTap: () async {
      final date = await showDatePicker(
        context: context,
        initialDate: _selectedDateTime,
        firstDate: DateTime.now(),
        lastDate: DateTime.now().add(const Duration(days: 365)),
      );
      if (date != null) {
        final time = await showTimePicker(
          context: context,
          initialTime: TimeOfDay.fromDateTime(_selectedDateTime),
        );
        if (time != null) {
          setState(() {
            _selectedDateTime = DateTime(
              date.year,
              date.month,
              date.day,
              time.hour,
              time.minute,
            );
          });
        }
      }
    },
  ),
],

 

예약 알림 UI 에서는 showDatePicker와 showTimePicker를 사용하여 아래 이미지처럼 알림 날짜와 시간을 선택할 수 있습니다.

 

 

 

 

3) 반복 알림 설정 UI

 

반복 알림은 요일을 기준으로 반복합니다. 그렇기 때문에 요일 선택 버튼을 생성해 주는 함수부터 만들어보겠습니다.

class _NotificationSettingsDialogState
    extends State<NotificationSettingsDialog> {
  final List<bool> _selectedWeekdays = List.generate(7, (_) => false);
  ...
}

Widget _buildWeekdayButton(String label, int index) {
  final isSelected = _selectedWeekdays[index]; // 선택 여부 확인

  return InkWell(
    onTap: () {
      setState(() {
        _selectedWeekdays[index] = !_selectedWeekdays[index]; // 선택 상태 토글
      });
    },
    child: Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      decoration: BoxDecoration(
        color: isSelected
            ? Theme.of(context).colorScheme.primary // 선택된 경우 색상
            : Theme.of(context).colorScheme.surface, // 선택되지 않은 경우 색상
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        label,
        style: TextStyle(
          color: isSelected
              ? Theme.of(context).colorScheme.onPrimary // 선택된 경우 텍스트 색상
              : Theme.of(context).colorScheme.onSurface, // 선택되지 않은 경우 텍스트 색상
          fontWeight: FontWeight.w500,
        ),
      ),
    ),
  );
}

 

_selectedWeekdaysisSelected 라는 상태 값을 만들어 토글 형식의 요일 선택 버튼을 구현합니다. 이렇게 하면 시각적으로 선택여부를 명확히 알 수 있습니다.

 

다음으로 반복 알림 설정 UI를 그려줍니다.

// 반복 알림 설정
if (_isRecurring) ...[
  const Text('반복할 요일 선택'),
  const SizedBox(height: 8),
  Center(
    child: Wrap(
      alignment: WrapAlignment.center,
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: 8,
      runSpacing: 8,
      children: [
        _buildWeekdayButton('월', 0),
        _buildWeekdayButton('화', 1),
        _buildWeekdayButton('수', 2),
        _buildWeekdayButton('목', 3),
        _buildWeekdayButton('금', 4),
        _buildWeekdayButton('토', 5),
        _buildWeekdayButton('일', 6),
      ],
    ),
  ),
  const SizedBox(height: 16),
  ListTile(
    title: const Text('시간'),
    subtitle: Text('${_selectedTime.hour}시 ${_selectedTime.minute}분'),
    trailing: const Icon(Icons.access_time),
    onTap: () async {
      final time = await showTimePicker(
        context: context,
        initialTime: _selectedTime,
      );
      if (time != null) {
        setState(() {
          _selectedTime = time; // 선택된 시간 업데이트
        });
      }
    },
  ),
],

 

앞서 만든 요일 선택 버튼 함수 _buildWeekdayButton를 이용해 요일 버튼을 생성하고, 화면 하단에는 예약 알림 설정 UI와 마찬가지로 showTimePicker 를 이용해 시간을 선택할 수 있도록 합니다.

 

 

 

마무리

이번 글에서는 노트 알림 UI 구현 과정을 다루었습니다. 하단 탭에 '노트 알림' 탭을 추가하고 알림 관리 화면과 알림 설정 버튼을 구현했습니다.

 

알림 설정 다이얼로그는 예약 알림/반복 알림에 따라 날짜, 시간, 요일 등을 선택할 수 있도록 구현했습니다.

 

다음 글에서는 알림 기능 로직을 구현해 보겠습니다.

 

 

[Flutter로 멋진 노트 앱을 만들어보자] 시리즈는 직접 독학으로 하나씩 만들어나가는 과정이므로 틀리거나 부족한 내용이 있을 수 있습니다. 조언과 피드백을 댓글로 남겨주시면 적극 반영하겠습니다. 감사합니다.
반응형