μ•ˆλ…•ν•˜μ„Έμš©:)
2020λ…„ 4월에 μž‘μ—…ν–ˆλ˜ [flutter] local notification Quick Start μž‘μ—… μ΄ν›„λ‘œ
이번 앱에도 μ•Œλ¦Ό κΈ°λŠ₯을 적용 μ‹œμΌœμ•Όν•΄μ„œ 2020λ…„ 11μ›” κΈ°μ€€μœΌλ‘œ λ‹€μ‹œ μž‘μ„±ν•΄λ³΄λ €ν•©λ‹ˆλ‹Ή


πŸ”₯ μ΄λ²ˆμ—” λ§₯뢁이 μžˆμœΌλ‹ˆ! iOS settingλΆ€ν„° ν•΄μ„œ iOS κΈ°μ€€μœΌλ‘œ 잘 λŒμ•„κ°€κ²Œλ” ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€πŸ”₯


0. install

https://pub.dev/packages/flutter_local_notifications/install

dependencies:
  # notification (20.11.11 κΈ°μ€€)
  flutter_local_notifications: ^3.0.1+2

μ§€μ›λ˜λŠ” ν”Œλž«νΌ

  • Android 4.1 이상
    • NotificationCompat APIλ₯Ό μ‚¬μš©ν•˜μ—¬ κ΅¬ν˜• Android μž₯치λ₯Ό μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • iOS 8.0 이상
    • 10 μ΄μ „μ˜ iOS λ²„μ „μ—μ„œ ν”ŒλŸ¬κ·ΈμΈμ€ UILocalNotification APIλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. UserNotification APIλ₯Ό (aka User Notifications Framework)λŠ” iOS 10μ΄μƒμ—μ„œ μ‚¬μš©λ©λ‹ˆλ‹€.

1. [Android] AndroidManifest.xml Settings

0. μ„€λͺ… μ „, full code

{flutter App} > android > app > src > main > AndroidManifest.xml

    ...

    <application
        ...
        >
        <activity
            ...
            android:windowSoftInputMode="adjustResize"
            android:showWhenLocked="true"
            android:turnScreenOn="true">
            <intent-filter>
                ...
            </intent-filter>
        </activity>
      
      	...
      
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
        <receiver 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>

      	...
      
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

라이브러리 곡식 μ„€λͺ…

μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ AndroidManifest.xml파일

1. receiver μΆ”κ°€

  • κΈ°κΈ°κ°€ μž κ²¨μžˆμ„ λ•Œλ„ 좜λ ₯
<activity
    ...
    android:showWhenLocked="true"
    android:turnScreenOn="true">
  • μž¬λΆ€νŒ…μ‹œμ—λ„ notifications remain scheduled μœ μ§€ν•˜λ €λ©΄ μΆ”κ°€
<receiver 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>
  • scheduled notifications 화면에 좜λ ₯ν• λ €λ©΄ μΆ”κ°€
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />

2. κΆŒν•œ μš”μ²­ (permission)

    <!-- local notification -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
  • λΆ€νŒ…μ‹œ μ„œλΉ„μŠ€(Service) μ‹€ν–‰ν•˜κΈ°
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  • 진동(VIBRATE) μ‚¬μš©
<uses-permission android:name="android.permission.VIBRATE"/>
  • νœ΄λŒ€ν°μ΄ κΊΌμ ΈμžˆλŠ” μƒνƒœμ—μ„œ μ•Œλ¦Όμ΄ λ°œμƒν•˜λ©΄ 화면을 κΉ¨μš°λŠ” κΈ°λŠ₯
    (μ•Œλ¦Ό λ°œμƒ μ‹œ ν™”λ©΄ μΌœμ§€κ²Œ ν•˜λŠ” κΈ°λŠ₯)
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>


2. [iOS] native Settings

0. μ„€λͺ… μ „, full code

{flutter App} > ios > Runner > AppDelegate.swift

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    ...
  ) -> Bool {
  
    if #available(iOS 10.0, *) {
      UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
    } 
    
    ...
  }
}

라이브러리 곡식 μ„€λͺ…


1. foreground에 μžˆλŠ” λ™μ•ˆ μ•Œλ¦Ό 처리

iOSλŠ” 기본적으둜 μ–΄ν”Œμ΄ μ—΄λ €μžˆμ„ λ•Œ(=foreground) μ•ŒλžŒμ΄ μšΈλ¦¬μ§€ μ•Šλ„λ‘ μ„€μ •λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€.

  • iOS 10 μ΄μƒμ—μ„œ presentation optionsμ‚¬μš©ν•΄ foreground에 μžˆλŠ” λ™μ•ˆ μ•Œλ¦Όμ΄ 트리거 λ λŒ€ λ™μž‘ 컨트둀
if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}

3. [main.dart] init Settings

βœ… μƒˆ μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“  λ‹€μŒ 각 ν”Œλž«νΌμ— μ‚¬μš©ν•  μ„€μ •μœΌλ‘œ μ΄ˆκΈ°ν™” μž‘μ—…

void main() async {
  ...
  _initNotiSetting();

  runApp(MyApp());
}

void _initNotiSetting() async {
  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
  final initSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
  final initSettingsIOS = IOSInitializationSettings(
    requestSoundPermission: false,
    requestBadgePermission: false,
    requestAlertPermission: false,
  );
  final initSettings = InitializationSettings(
    android: initSettingsAndroid,
    iOS: initSettingsIOS,
  );
  await flutterLocalNotificationsPlugin.initialize(
    initSettings,
  );
}
  • μ΄ˆκΈ°ν™”λŠ” ν•œ 번만 μˆ˜ν–‰ν•΄μ•Ό ν•˜λ©° μ΄λŠ” main.dartμ—μ„œ κΈ°λŠ₯ μˆ˜ν–‰
    (λ˜λŠ” 앱에 ν‘œμ‹œλœ 첫 번째 νŽ˜μ΄μ§€ λ‚΄μ—μ„œμ΄ μž‘μ—…μ„ μˆ˜ν–‰ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.)
  • μ•± μ‹œμž‘ ν›„ λ°”λ‘œ κΆŒν•œμ„ 묻길 μ›ν•œλ‹€λ©΄ requestSoundPermission true둜 λ³€κ²½
    • μ €λŠ” μ•±μ‹œμž‘ ν›„ λ¬»λŠ”κ²Œ μ•„λ‹Œ μ•Œλ¦Ό μ‹œκ°„μ„€μ • ν›„ μ•Œλ¦ΌκΆŒν•œμ„ λ¬Όμ–΄λ΄€μœΌλ©΄ ν•΄μ„œ μ΄ˆκΈ°μ„€μ •μ—λŠ” false둜 μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€.

4. noti setting UI

  • UIμ½”λ“œλŠ” κΈ°ν˜Έμ— 맞좰 μž‘μ„±ν•΄μ£Όμ‹œλ©΄ λ©λ‹ˆλ‹€.
    • UIμ½”λ“œλŠ” κ΄€λ ¨λœ local notificationκ³Ό 관련없기에 λ”°λ‘œ λ…ΈμΆœν•˜μ§€μ•Šκ² μŠ΅λ‹ˆλ‹€ πŸ˜‰
  • λ‹€μŒκ³Ό μ„€μ • νŽ˜μ΄μ§€κ°€ μžˆμ„ λ•Œ "확인" λ²„νŠΌμ„ λˆ„λ₯Έ 이후 notification을 μ„€μ • μ½”λ“œ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.

code

UI

    SubmitButton(
        ...
        text: '확인',
        onPressed: isDisabled ? null : _dailyAtTimeNotification,
    )

onPressed

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


Future _dailyAtTimeNotification() async {
    final notiTitle = 'title';
    final notiDesc = 'description';

    final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    final result = await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(
          alert: true,
          badge: true,
          sound: true,
        );

    var android = AndroidNotificationDetails('id', notiTitle, notiDesc,
        importance: Importance.max, priority: Priority.max);
    var ios = IOSNotificationDetails();
    var detail = NotificationDetails(android: android, iOS: ios);

    if (result) {
      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<
              AndroidFlutterLocalNotificationsPlugin>()
          ?.deleteNotificationChannelGroup('id');
          
      await flutterLocalNotificationsPlugin.zonedSchedule(
        0, // idλŠ” uniqueν•΄μ•Όν•©λ‹ˆλ‹€. intκ°’
        notiTitle,
        notiDesc,
        _setNotiTime(),
        detail,
        androidAllowWhileIdle: true,
        uiLocalNotificationDateInterpretation:
            UILocalNotificationDateInterpretation.absoluteTime,
        matchDateTimeComponents: DateTimeComponents.time,
      );
    }
  }

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

    final now = tz.TZDateTime.now(tz.local);
    var scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day,
        10, 0);

    return scheduledDate;
  }
}

μ€‘μš”

  • idλŠ” uniqueν•΄μ•Όν•©λ‹ˆλ‹€.
  • μš°μ„  기쑴에 μ“°λ˜ schedule()이 버전업이 λ˜λ©΄μ„œ deprecated(μ‚¬μš©μ€‘λ‹¨) λμŠ΅λ‹ˆλ‹€.
  • FlutterLocalNotificationsPlugin().zonedSchedule()둜 μ‚¬μš©ν•˜λ©΄λœλ‹€.
    • κΈ°μ‘΄ : μ§€μ •λœ λ‚ μ§œ 및 μ‹œκ°„μ— ν‘œμ‹œ ν•  μ•Œλ¦Όμ„ μ˜ˆμ•½ν•©λ‹ˆλ‹€.
    • λ³€κ²½ : νŠΉμ • μ‹œκ°„λŒ€λ₯Ό κΈ°μ€€μœΌλ‘œ μ§€μ •λœ λ‚ μ§œ 및 μ‹œκ°„μ— ν‘œμ‹œλ˜λ„λ‘ μ•Œλ¦Όμ„ μ˜ˆμ•½ν•©λ‹ˆλ‹€.
      = κ°„λ‹¨νžˆ λ§ν•΄μ„œ timezone섀정이 κ°€μž₯ 큰 차이점

  • κ°€μž₯ λˆˆμ—¬κ²¨ λ³Ό κ³³ = _setNotiTime()
    • TZDateTime은 default timezone이 UTC둜 λ˜μ–΄μžˆλ‹€.
    • κ·Έλž˜μ„œ tz.initializeTimeZones(); ν›„
      ν•„μˆ˜λ‘œ tz.setLocalLocation(tz.getLocation('Asia/Seoul'));λ₯Ό ν•΄μ€˜μ•Όν•œλ‹€.
    • setLocationν• λ•Œ μ•„μ‰½μ§€λ§Œ timezone(ex. KST)을 λ„£λŠ”κ²Œ μ•„λ‹Œ
      location name(ex. Asia/Seoul)λ₯Ό λ„£μ–΄μ€˜μ•Όν•œλ‹€.

  • λ¬Όλ‘  λ‚˜μ™€μžˆλŠ” μ†ŒμŠ€μ½”λ“œ κ·ΈλŒ€λ‘œ μ„€μ •ν•˜λ©΄ ν•œκ΅­μ΄ μ•„λ‹Œ λ‹€λ₯Έ timezone에 계신 뢄듀은.. λ§κ·ΈλŒ€λ‘œ 이슈 λ°œμƒπŸ”₯πŸ”₯πŸ”₯πŸ”₯
    • https://pub.dev/packages/flutter_native_timezone
    • await FlutterNativeTimezone.getLocalTimezone(); μœ„ 라이브러리λ₯Ό μ΄μš©ν•΄μ„œ 개인적으둜 provider둜 κ΄€λ¦¬ν•΄μ„œ λ°›μ•„μ˜€μ§€λ§Œ λ”°λ‘œ μ μ§€μ•Šμ•˜λŠ”λ°
      ν•΄λ‹Ή 라이브러리λ₯Ό μ‚¬μš©ν•˜λ©΄ λ¬Έμ œμ—†μ΄ location name섀정이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

  • μ•„λž˜ λ‹€μŒ μ½”λ“œλ₯Ό μΆ”κ°€λ₯Ό μ•ˆν•˜λ©΄ 이전 NotificationChannel듀이 μ‚΄μ•„μžˆμ–΄μ„œ λ‹€μ€‘μœΌλ‘œ μ•Œλ¦Ό 지μ˜₯을 λ°›κ²Œλœλ‹€..πŸ”₯
    • 이말은 즉 μ—¬λŸ¬κ°œμ˜ μ•ŒλžŒμ„ ν•˜κ³  싢을 경우, λ‹€μŒ deleteNotificationChannelGroupλ₯Ό λΉΌμ£Όλ©΄ λœλ‹€.
await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
      ?.deleteNotificationChannelGroup('id');


+. remove local noficiation

uniqueν•œ idκ°’ ν† λŒ€λ‘œ .cancle(id) κ°’ ν˜ΈμΆœμ‹œ μ•ŒλžŒ μ‚­μ œλ©λ‹ˆλ‹€.

await FlutterLocalNotificationsPlugin().cancel(id)


5.result

setting

foreground

background

profile
𝙸 πšŠπš– 𝚊 πšŒπšžπš›πš’πš˜πšžπšœ πšπšŽπšŸπšŽπš•πš˜πš™πšŽπš› πš πš‘πš˜ πšŽπš—πš“πš˜πš’πšœ πšπšŽπšπš’πš—πš’πš—πš 𝚊 πš™πš›πš˜πš‹πš•πšŽπš–. πŸ‡°πŸ‡·πŸ‘©πŸ»β€πŸ’»

10개의 λŒ“κΈ€

comment-user-thumbnail
2021λ…„ 1μ›” 17일

감삼돠 3.0.1+4 이상 버전에선 AndroidManifest.xml μ—μ„œ μˆ˜μ •ν•΄μ•Ό 될게 많이 μ€„μ—ˆλ„€μš”
https://pub.dev/packages/flutter_local_notifications#-android-setup

λ‹΅κΈ€ 달기
comment-user-thumbnail
2021λ…„ 2μ›” 28일

μ•ˆλ“œλ‘œμ΄λ“œμ—μ„œ 초기 μ…‹νŒ… λ•Œ λ‹€μŒκ³Ό 같이 μ—λŸ¬λ‚ λ•Œ μ°Έκ³ 

platformException (PlatformException(INVALID_ICON, The resource app_icon could not be found. Please make sure it has been added as a drawable resource to your Android head project., null, null))

solution

https://stackoverflow.com/questions/55820299/dont-know-what-file-error-is-referring-to-in-flutter-local-notifications-plugin

λ‹΅κΈ€ 달기
comment-user-thumbnail
2021λ…„ 3μ›” 2일

정말 κ°μ‚¬ν•©λ‹ˆλ‹€! μ‹œκ°„ μ„€μ • μ–΄λ–»κ²Œ ν•΄μ€˜μ•Όν•˜λ‚˜.. κ³ λ―Όν•˜κ³  μžˆμ—‡λŠ”λ° γ…Žγ…Ž

1개의 λ‹΅κΈ€
comment-user-thumbnail
2021λ…„ 4μ›” 26일

ν•„μš”ν•œκ±Έ κ²€μƒ‰ν•˜λ©΄, λ‹€λ³΄λ‹˜ λΈ”λ‘œκ·Έμ— 이미 λ‹€ μ„€λͺ…이 λ˜μžˆλ„€μš”! μ˜€λŠ˜λ„ κ°μ‚¬ν•©λ‹ˆλ‹€!

1개의 λ‹΅κΈ€
comment-user-thumbnail
2021λ…„ 5μ›” 1일

{flutter App} > android > app > src > main > AppDelegate.swift λΆ€λΆ„ μ˜€νƒ€κ°€ μžˆλŠ” 것 κ°™μŠ΅λ‹ˆλ‹€!
AppDelegate.swift -> AndroidManifest.xml

1개의 λ‹΅κΈ€
comment-user-thumbnail
2021λ…„ 7μ›” 16일

android 5 κ³Ό 같이 ꡬ λ²„μ „μ—μ„œ μ—λŸ¬ λ°œμƒν•˜λŠ” κ²½μš°λŠ” μ—†μœΌμ…¨λ‚˜μš”?
flutter 둜 κ°œλ°œν•˜λ©΄μ„œ μ΅œμ‹ λ²„μ „μ—μ„œλŠ” λ¬Έμ œκ°€ μ—†λŠ”λ° ꡬ λ²„μ „μ—μ„œλŠ” μ΄μŠˆκ°€ λ°œμƒν•˜λ„€μš”.

λ‹΅κΈ€ 달기
comment-user-thumbnail
2022λ…„ 8μ›” 4일

κ°μ‚¬ν•©λ‹ˆλ‹€!!

λ‹΅κΈ€ 달기