본문 바로가기

일::개발

Flutter Push Notification (2) - Heads up notification

플러터 푸시 알림 (2) - 상단 알림창 (Heads up notification) 띄우기

지난 포스트에서 언급했지만, 안드로이드/ iOS 모두 foreground 모드에서는 푸시 알림 수신 시에도 시스템 알림창이 뜨지 않는다.

앱 사용 중에 수신되는 푸시 알림에 반응하고 싶다면 FirebaseMessaging.onMessage 에서 직접 처리해줘야 한다.

 

간단하게 하려면 FirebaseMessagong.onMessage 에서 alert dialog 표시하도록 해주기만 하면 된다.

    FirebaseMessaging.onMessage.listen((RemoteMessage rm) {
      message.value = rm;
      Get.dialog(AlertDialog(
        title: Text(rm.notification?.title ?? 'TITLE'),
        content: Text(rm.notification?.body ?? 'BODY'),
      ));
    });

안드로이드, iOS 모두 알림음 없이 기본 팝업 노출

하지만, 이것으로 뭔가 부족하다 싶거나 화면 상단에 잠시 나타나는 알림 ("heads up notification")을 표시하고 싶다면 좀 더 깊이 들어가야 한다.

 

먼저, FlutterFire 문서의 관련 내용(https://firebase.flutter.dev/docs/messaging/notifications/#advanced-usage)을 간단히 정리하면 다음과 같다.

  • 안드로이드와 iOS는 foreground 모드에서 푸시 알림 받았을 때 동작하는 방식이 다르다.
  • iOS에서는 setForegroundNotificationPresentationOptions(alert: true) 해주면 된다. (1)
  • 안드로이드는 기본적으로 foreground일 때는 시각적인 알림이 나오지 않고, background/ terminated 일 때는 앱에 할당된 "Notification Channel"의 설정을 따른다.
    • 안드로이드에서 heads up notification을 보여주기 위해서는 Notification Channel의 중요도를 "max"로 설정해야 한다. 
    • 먼저 중요도가 max인 채널을 만들고, 수신되는 FCM 알림을 이 채널에 할당해야 한다. 이 작업은 flutter local notifications 패키지를 이용한다. (2)
  • Notification Channel 설정을 해도 앱이 foreground 상태일 때에는 Firebase Android SDK가 FCM알림이 화면에 표시되지 않도록 막기 때문에 onMessage 에서 local notification을 하나 생성해서 보여주도록 처리해줘야 한다. (3)

그럼 순서대로 처리해보자.

 

(1) iOS에서 Foreground heads up notification 처리

app_controller의 initialize() 에서 setForegroundNotificationPresentationOptions를 호출해준다.

  Future<bool> initialize() async {
    // Firebase 초기화부터 해야 Firebase Messaging 을 사용할 수 있다.
    await Firebase.initializeApp();

    // Android 에서는 별도의 확인 없이 리턴되지만, requestPermission()을 호출하지 않으면 수신되지 않는다.
    await FirebaseMessaging.instance.requestPermission(
      alert: true,
      announcement: true,
      badge: true,
      carPlay: true,
      criticalAlert: true,
      provisional: true,
      sound: true,
    );

    await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
      alert: true, // Required to display a heads up notification
      badge: true,
      sound: true,
    );

    FirebaseMessaging.onMessage.listen((RemoteMessage rm) {
      message.value = rm;
    });

    return true;
  }

iOS, foreground에서 heads up notification 표시됨

 

(2) 안드로이드 기본 Notification Channel 생성

먼저 flutter_local_notifications 패키지를 pubspec.yaml에 추가하고,

AppController의 initialize()에

    // Android용 새 Notification Channel
    const AndroidNotificationChannel androidNotificationChannel = AndroidNotificationChannel(
      'high_importance_channel', // 임의의 id
      'High Importance Notifications', // 설정에 보일 채널명
      'This channel is used for important notifications.', // 설정에 보일 채널 설명
      importance: Importance.max,
    );

    // Notification Channel을 디바이스에 생성
    final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
    await flutterLocalNotificationsPlugin
        .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(androidNotificationChannel);

AndroidManifest.xml 에

<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="high_importance_channel" /> 를 넣어준다.

   <application
        android:label="notification_sample"
        android:icon="@mipmap/ic_launcher">

        <meta-data
          android:name="com.google.firebase.messaging.default_notification_channel_id"
          android:value="high_importance_channel" />

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">

 

이렇게 하면 안드로이드 기본 Notification Channel이 코드에서 생성한 'high_importance_channel' 로 변경되어서 background, terminated 상태에서 메시지 수신했을 때 heads up notification이 활성화된다.

안드로이드 backgroudn, terminated mode에서 수신 시 heads up notification 보임

(3) 안드로이드 foreground 에서 heads up notification 활성화

앞에서 언급했듯이 Firebase Android SDK는 foreground모드에서 FCM알림이 화면에 표시하지 않기 때문에 별도로 처리를 해줘야 한다. foreground 상태에서 메시지를 수신하면 호출되는 onMessage() 에서 수신한 Remote Notification Message를 이용해서 Local Notification 을 하나 생성하는 방식으로 처리한다.

    // FlutterLocalNotificationsPlugin 초기화. 이 부분은 notification icon 부분에서 다시 다룬다.
    await flutterLocalNotificationsPlugin.initialize(
        InitializationSettings(
            android: AndroidInitializationSettings('@mipmap/ic_launcher'), iOS: IOSInitializationSettings()),
        onSelectNotification: (String? payload) async {});

    FirebaseMessaging.onMessage.listen((RemoteMessage rm) {
      message.value = rm;
      RemoteNotification? notification = rm.notification;
      AndroidNotification? android = rm.notification?.android;

      if (notification != null && android != null) {
        flutterLocalNotificationsPlugin.show(
          0,
          notification.title,
          notification.body,
          NotificationDetails(
            android: AndroidNotificationDetails(
              'high_importance_channel', // AndroidNotificationChannel()에서 생성한 ID
              'High Importance Notifications', 
              'This channel is used for important notifications.',
              // other properties...
            ),
          ),
        );
      }
    });

일단 이렇게 해주면 안드로이드 foreground 모드에서도 heads up notification이 뜬다.

flutterLocalNotificationsPlugin.initialize() 부분의 파라미터는 안드로이드 notification icon 관련해서 다음에 다시 정리하고, 일단 이렇게 해 두면 수신은 문제 없이 동작한다.

flutterLocaoNotificationsPlugin.show() 의 첫번째 파라미터는 FlutterFire 문서에 있는 것처럼 notification.hashCode 를 사용하면 문제가 생기니 일단 0으로 처리하면 별 문제 없다.

 

자, 이제 여기까지 하면 안드로이드, iOS 모두 foreground, background, terminated 상태에서 푸시 수신 시 heads up notification 이 나타나게 되었다.

 

여기까지의 코드는 역시 github에 올려놓았다. (https://github.com/nicejhkim/flutter_fcm_sample/commit/a4be37ce150cd46e688dade9ca6faaf6e2bd8c87)