Skip to content

GDGOC-SeoulTech/5th_Flutter_Session_9

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

App Session 9주차

담당 Core: 임도협

GDGoC Header

이번 주에는

  • Notification이 작동하는 원리 배우기
  • flutter_local_notifications를 사용해 notification 구현하기
  • 플랫폼별 알림 권한 설정하기
  • 알림 초기화하기
  • 즉시 알림 구현하기
  • 예약 알림 구현하기

Notification이 작동하는 원리

알림은 앱이 직접 띄우는 것이 아니라, OS(Android / iOS)가 대신 관리하고 표시하는데요, 그래서 앱이 꺼져 있어도 알림 표시되거나, 시스템 설정에서 알림 ON/OFF를 할 수 있는 것입니다!

Android 8.0(API 26)+부터는 모든 알림이 Channel 기반입니다.

App → Notification → Channel → OS → User

Channel이란 "알림을 그룹화하는 단위"라고 할 수 있는데요, Channel은 한 번 생성되면 수정 불가하고, importance, sound 변경도 불가합니다.

Channel을 쓰는 이유: 사용자 입장에서 채팅, 댓글, 광고 등 서로 다른 알림 종류에 대해 정교하게 제어할 수 있도록 구분하기 위함입니다!

image

그래서 안드로이드에는 오늘 구현할 알림 두 종류가 각각 표시되는 것을 확인할 수 있습니다!


반대로 iOS는 Channel 개념이 없고, UNUserNotificationCenter라는 클래스에서 처리하도록 만들어졌습니다!

App → UNUserNotificationCenter → OS → User

그래서 iOS에서는 알림 제어가 단순한데, 앱 단위 ON/OFF하거나 소리 / 배너 / 배지 설정만 할 수 있습니다.

image

실습

Step 1

1. 프로젝트 생성 및 실행

flutter create --empty noti
code .

VSCode에서 lib/main.dart를 열고, 화면 하단 상태바에서 사용할 디바이스(시뮬레이터)를 선택한 후, 창 상단 Start Debugging을 눌러 앱을 실행합니다.

image

2. 패키지 설치

다음 패키지를 설치합니다.

flutter_local_notifications: ^21.0.0
timezone: ^0.11.0

VSCode에서 cmd+shift+p 키를 누르고, Dart: Add Dependency를 입력한 후 enter

image

flutter_local_notifications를 입력한 후 enter

image

timezone을 입력한 후 enter

image

pubspec.yaml에 설치된 모습

image

Step 2

3. 앱 알림 권한 설정

푸쉬 알림을 사용하려면 플랫폼별 권한 설정이 필요한데요, 특히 iOS는 반드시 권한 요청 코드가 필요하고, Android도 최신 버전에서는 알림 권한을 명시해야 합니다!

Android 설정

android/app/src/main/AndroidManifest.xml파일에 아래 권한을 추가합니다.

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />

그리고 <application ...> 태그 내부에 다음 receiver도 추가합니다.

<receiver
    android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />

<receiver
    android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
        <action android:name="android.intent.action.QUICKBOOT_POWERON" />
        <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
    </intent-filter>
</receiver>

<receiver
    android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsReceiver"/>
image
iOS 설정

ios/Runner/Info.plist에 아래 내용을 추가합니다.

<key>NSUserNotificationUsageDescription</key>
<string>푸쉬 알림을 수신하기 위해 권한이 필요합니다.</string>
image

그 다음, ios/Runner/AppDelegate.swift를 아래로 바꿔줍니다.

기본적으로 iOS는 앱이 실행 중이면 알림을 받아도 표시하지 않는데요, UNUserNotificationCenter.current().delegate = self로 설정함으로서 알림을 항상 수신할 수 있게 하는 과정입니다!

import Flutter
import UIKit
import flutter_local_notifications

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
        GeneratedPluginRegistrant.register(with: registry)
    }
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
  }

  @available(iOS 12.0, *)
  override func userNotificationCenter(
      _ center: UNUserNotificationCenter,
      openSettingsFor notification: UNNotification?
  ) {
      let controller = window?.rootViewController as! FlutterViewController
      let channel = FlutterMethodChannel(
          name: "com.example.noti/settings",
          binaryMessenger: controller.binaryMessenger)

      channel.invokeMethod("showNotificationSettings", arguments: nil)
  }
}

Step 3

4. 알림 서비스 flutter_local_notifications 초기화 및 권한 요청하기

lib/main.dart를 아래처럼 작성합니다.

import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  tz.initializeTimeZones();
  tz.setLocalLocation(tz.getLocation('Asia/Seoul'));

  const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');

  const darwinSettings = DarwinInitializationSettings();

  const initializationSettings = InitializationSettings(
    android: androidSettings,
    iOS: darwinSettings,
  );

  await flutterLocalNotificationsPlugin.initialize(
    settings: initializationSettings,
  );

  await requestNotificationPermissions();

  runApp(const MyApp());
}

Future<void> requestNotificationPermissions() async {
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin
      >()
      ?.requestNotificationsPermission();

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        IOSFlutterLocalNotificationsPlugin
      >()
      ?.requestPermissions(alert: true, badge: true, sound: true);
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Notification Demo',
      home: const NotificationPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('9주차 세션')),
      body: const Center(),
    );
  }
}

runApp(..)을 호출하기 전, await requestNotificationPermissions()으로 플랫폼별 알림 권한을 직접 요청하고 있습니다! 구현과 기기 버전에 따라 알림 권한 정책이 다르므로, 이처럼 직접 요청을 하는 것이 중요합니다.

이제 앱을 다시 빌드하면 다음과 같이 알림 권한을 묻는 것을 확인할 수 있습니다.

image image

Step 4

5. 즉시 알림 보내기

먼저 버튼을 눌렀을 때 바로 알림이 오도록 해보겠습니다.

showSimpleNotification() 펑션을 lib/main.dart에 추가해주세요.

Future<void> showSimpleNotification() async {
  const androidDetails = AndroidNotificationDetails(
    'basic_channel',
    'Basic Notifications',
    channelDescription: 'Basic noti channel',
    importance: Importance.max,
    priority: Priority.high,
  );

  const notificationDetails = NotificationDetails(
    android: androidDetails,
    iOS: DarwinNotificationDetails(),
  );

  await flutterLocalNotificationsPlugin.show(
    id: 0,
    title: 'GDGoC 알림',
    body: '알림이 도착했습니다!.',
    payload: 'payload',
    notificationDetails: notificationDetails,
  );
}

그리고 이를 호출하는 버튼을 메인 화면에 추가해주면 됩니다!

body: Center(
  child: ElevatedButton(
    onPressed: showSimpleNotification,
    child: const Text('즉시 알림 보내기'),
  ),
),
image

6. 예약 알림 보내기

이번에는 몇 초 뒤에 알림이 오도록 예약해보겠습니다. 이 기능을 위해 timezone 패키지를 함께 사용합니다.

Future<void> scheduleNotification() async {
  const androidDetails = AndroidNotificationDetails(
    'schedule_channel',
    'Scheduled Notifications',
    channelDescription: 'Scheduled noti channel',
    importance: Importance.max,
    priority: Priority.high,
  );

  const notificationDetails = NotificationDetails(
    android: androidDetails,
    iOS: DarwinNotificationDetails(),
  );

  await flutterLocalNotificationsPlugin.zonedSchedule(
    id: 1,
    title: 'GDGoC 예약 알림',
    body: '5초 뒤에 도착한 알림입니다.',
    payload: 'payload',
    scheduledDate: tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
    notificationDetails: notificationDetails,
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
  );
}

그리고 이를 호출하는 버튼을 즉시 알림 버튼 아래에 추가해줍니다!

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      ElevatedButton(
        onPressed: showSimpleNotification,
        child: const Text('즉시 알림 보내기'),
      ),
      const SizedBox(height: 16),
      ElevatedButton(
        onPressed: scheduleNotification,
        child: const Text('5초 뒤 알림 보내기'),
      ),
    ],
  ),
),
image

최종 main.dart

import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  tz.initializeTimeZones();
  tz.setLocalLocation(tz.getLocation('Asia/Seoul'));

  const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');

  const darwinSettings = DarwinInitializationSettings();

  const initializationSettings = InitializationSettings(
    android: androidSettings,
    iOS: darwinSettings,
  );

  await flutterLocalNotificationsPlugin.initialize(
    settings: initializationSettings,
  );

  await requestNotificationPermissions();

  runApp(const MyApp());
}

Future<void> requestNotificationPermissions() async {
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin
      >()
      ?.requestNotificationsPermission();

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        IOSFlutterLocalNotificationsPlugin
      >()
      ?.requestPermissions(alert: true, badge: true, sound: true);
}

Future<void> showSimpleNotification() async {
  const androidDetails = AndroidNotificationDetails(
    'basic_channel',
    'Basic Notifications',
    channelDescription: 'Basic noti channel',
    importance: Importance.max,
    priority: Priority.high,
  );

  const notificationDetails = NotificationDetails(
    android: androidDetails,
    iOS: DarwinNotificationDetails(),
  );

  await flutterLocalNotificationsPlugin.show(
    id: 0,
    title: 'GDGoC 알림',
    body: '알림이 도착했습니다!.',
    payload: 'payload',
    notificationDetails: notificationDetails,
  );
}

Future<void> scheduleNotification() async {
  const androidDetails = AndroidNotificationDetails(
    'schedule_channel',
    'Scheduled Notifications',
    channelDescription: 'Scheduled noti channel',
    importance: Importance.max,
    priority: Priority.high,
  );

  const notificationDetails = NotificationDetails(
    android: androidDetails,
    iOS: DarwinNotificationDetails(),
  );

  await flutterLocalNotificationsPlugin.zonedSchedule(
    id: 1,
    title: 'GDGoC 예약 알림',
    body: '5초 뒤에 도착한 알림입니다.',
    payload: 'payload',
    scheduledDate: tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)),
    notificationDetails: notificationDetails,
    androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Notification Demo',
      home: const NotificationPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('9주차 세션')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: showSimpleNotification,
              child: const Text('즉시 알림 보내기'),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: scheduleNotification,
              child: const Text('5초 뒤 알림 보내기'),
            ),
          ],
        ),
      ),
    );
  }
}

About

GDGoC 5기 App Session 9주차

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors