티스토리 뷰

Tip and Error

[Flutter, Android] FCM (push notification)

geonwoopaeng@gmail.com 2023. 11. 25. 23:19

[Flutter, Android] FCM (push notification)

Push Notification은 Firebase에서 각 핸드폰으로 알림 메세지를 전달하는 것을 구현해보려고 합니다.
push notification을 하는 방법이 많아서 선택을 하여야 하는데 쉽게 여러 핸드폰으로 알림을 보낼 수 있는 방법이
Firebase를 이용한 것입니다. 

삽질을 많이해서 다른 분들은 쉽게 진행 할 수 있기를 바라며 😄
공통 부분을 설정하고 Background or Foreground 방법을 선택해서 순서에 맞게 실행하면 됩니다.
전체 코드를 보실 분은 공통 + Foreground 방법을 보시면 됩니다.

개발환경

  • IDE: Android Studio
  • Flutter
  • Webview

Android Studio로 개발을 진행을 진행하고 Flutter을 Webview로 개발하였습니다.

공통

우선 기본 세팅을 진행해야 합니다.

1. Firebase 프로젝트 생성

프로젝트 이름: push notification
애널리틱스: 자유

 

진행을 완료하면 다음과 같이 프로젝트가 생성됩니다.

 

2. Firebase를 프로젝트에 적용

여기서 Android 아이콘을 눌러 Android앱에 Firebase 추가합니다.

Android 패키지 이름은 android/app/build.gradledefaultConfig 객체의 applicationId를 사용하면 됩니다. (이미지의 빨간색 칠한 부분입니다)

google-services.json을 다운받아서 자신의 프로젝트 android/app 에 넣어줍니다.

이후 작업을 꼭 다 진행하시면 설정이 완료가 됩니다.
(해당 부분은 코드 복사 붙여넣기 부분이라 생략했습니다.)

 

3. 프로젝트 내부 설정

firebase_core, firebase_messaging을 다음과 같이 terminal에서 설치합니다.

$ flutter pub add firebase_core
$ flutter pub add firebase_messaging

설치를 하면 pubspec.yaml 폴더에 설치된 것을 확인 할 수 있습니다.

android/app/build.gradle 에서
minSdkVersion flutter.minSdkVersion => minSdkVersion 19
로 고정시킵니다.
(버전 호환문제가 발생할 수 있기 때문에 중요합니다.)

 

4. 연결 파일 구현 (firebase_api.dart)

lib/firebase_api.dart 폴더를 만들어 코드를 작성합니다.

[firebase_api.dart]

import 'package:firebase_messaging/firebase_messaging.dart';

class FirebaseApi {
  //create an instance of Firebase Msg
  final _firebaseMessaging = FirebaseMessaging.instance;

  //function to init notification
  Future<void> initNotifications() async {
    //request permission from user (will prompt user)
    await _firebaseMessaging.requestPermission(
      badge: true,
      alert: true,
      sound: true
    );

    //fetch the FCM token for this device
    final FCMToken = await _firebaseMessaging.getToken();

    //print the token (normally you would send this to your server)
    print('Token: $FCMToken');
  }
}

이젠 main.dart에서 사용을 해봅시다.
웹뷰로 개발을 진행하게 되어 해당 부분 제외하고 진행을 하여도 됩니다.
(웹뷰로 하니 뭔가 많이 떠 있어 좋았습니다.)

[main.dart]

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'notification.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_api.dart';

final home_url = Uri.parse("웹뷰할 URL");

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); //앱 실행할 준비가 완료될 때까지 기다린다.
  await Firebase.initializeApp();
  await FirebaseApi().initNotifications();

  runApp(const MyApp());
}

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

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

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

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

class _MyHomePageState extends State<MyHomePage> {
  static WebViewController controller = WebViewController()
    ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..addJavaScriptChannel(
        'Toaster',
        onMessageReceived: (JavaScriptMessage msg) async {
          // Webview 에서 받아온 javascript message
        }
    )
    ..loadRequest(home_url);

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

  // State 객체가 최초 생성될 때 호출되는 메소드 (한번만 호출)
  // 초기화 하는 부분 주로 사용
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: WebViewWidget(
            controller: controller
        )
    );
  }
}

해당 작업을 완료한 후 애뮬레이터를 실행을 하여 Token을 파악 후 테스트를 진행해 보겠습니다.
다음 이미지의 순서에 맞춰서 진행하면 됩니다.

토큰은 Logcat 에서 Filter에 Token을 입력하여 찾으시면 됩니다.

 

번외 - 1 테스트하는 방법 (Firebase)

해당 과정은 밑에서 세팅을 한 후에 진행하는 과정이므로 번외로 만들었습니다.

참여(Engage)의 Messaging을 눌러 첫 번째 캠페인을 만듭니다.

Firebase 알림 메세지로 들어갑니다.

알림 제목, 알림 텍스트를 적어두고 텍스트 메시지 전송을 선택합니다.

파악한 Token을 해당 input에 넣고 테스트를 진행하면 됩니다.

 

번외 - 2 테스트하는 방법 (postman)

해당 부분은 너무 잘 나와있는 영상이 있어 영상으로 대체합니다.
Youtube: https://www.youtube.com/watch?app=desktop&v=rQzexLu0eLU

----------------------------------------------------------------------------------

Background Message

앱은 열려있지만, 뒷단에서 동작하고 있는 경우

  1. firebase_api.dart를 수정
  2. 애뮬레이터를 켜서 Token 받기
  3. 테스트 하기(번외)

1. firebase_api.dart를 수정

다음과 같이 /lib/firebase_api.dart 파일에 코드를 추가합니다.

[firebase_api.dart]

import 'package:firebase_messaging/firebase_messaging.dart';

<!-- 추가된 부분 -->
Future<void> handleBackgroundMessage(RemoteMessage message) async {
  print('mattabu: Title: ${message.notification?.title}');
  print('mattabu: Body: ${message.notification?.title}');
  print('mattabu: Payload: ${message?.data}');
}

class FirebaseApi {
    ...
    <!-- 추가된 부분 -->
    FirebaseMessaging.onBackgroundMessag(handleBackgroundMessage);
}   

 

Foreground Message

앱이 열려있고, 화면을 차지하고 있는 경우

  1. flutter_local_notifications 설치
  2. firebase_api.dart 수정
  3. android/app/src/main/AndroidManifest.xml 수정
  4. 테스트 하기(번외)

 

1. flutter_local_notifications 설치

$ flutter pub add flutter_local_notifications

 

2. firebase_api.dart 수정

다음과 같이 /lib/firebase_api.dart 파일에 코드를 추가합니다.
(구조를 background에서 살짝 바꿨으니 확인하며 사용하시면 됩니다.)

[firebase_api.dart]

import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';

<!-- 추가된 부분 -->
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class FirebaseApi {
  //create an instance of Firebase Msg
  final _firebaseMessaging = FirebaseMessaging.instance;

  <!-- 추가된 부분 -->
  final _androidChannel = const AndroidNotificationChannel(
    'high_importance_channel',
    'High Importance Notifications',
    description: 'This channel is used for notification',
    importance: Importance.defaultImportance,
  );

  final _localNotifications = FlutterLocalNotificationsPlugin();

  //function to init notification
  Future<void> initNotifications() async {
    //request permission from user (will prompt user)
    await _firebaseMessaging.requestPermission();

    //fetch the FCM token for this device
    final FCMToken = await _firebaseMessaging.getToken();

    //print the token (normally you would send this to your server)
    print('mattabu: Token: $FCMToken');

    <!-- 추가된 부분 -->
    initPushNotifications();
    initLocalNotification();
  }

  <!-- 추가된 부분 -->
  Future initLocalNotification() async {
    const android = AndroidInitializationSettings('@drawable/ic_launcher');
    const settings = InitializationSettings(android: android);

    await _localNotifications.initialize(settings);

    final platform = _localNotifications.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();
    await platform?.createNotificationChannel(_androidChannel);
  }

  <!-- 추가된 부분 -->
  //function to handle received Msg
  void handleMessage(RemoteMessage? message) {
    //if the message is null , do nothing
    if (message == null) return ;
    print('mattabu: $message');
  }

  <!-- 추가된 부분 -->
  Future initPushNotifications() async {
    await FirebaseMessaging.instance
        .setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );
    FirebaseMessaging.instance.getInitialMessage().then(handleMessage);
    FirebaseMessaging.onMessageOpenedApp.listen(handleMessage);

    //foreground
    FirebaseMessaging.onMessage.listen((message) {
      final notification = message.notification;
      if (notification == null) return;

      _localNotifications.show(
          notification.hashCode,
          notification.title,
          notification.body,
          NotificationDetails(
            android: AndroidNotificationDetails(
              _androidChannel.id,
              _androidChannel.name,
              channelDescription:  _androidChannel.description,
              icon: '@drawable/ic_launcher', //android/app/src/main/res/drawable 에 들어있는 아이콘
            ),
          ),
          payload: jsonEncode(message.toMap())
      );
    });
  }
}

 

3. AndroidManifest.xml 수정

다음과 같이 android/app/src/main/AndroidManifest.xml 파일에 코드를 추가합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <!--FCM-->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="USE_FULL_SCREEN_INTENT"/>
    <application
        android:label="push notification"
        android:name="${applicationName}"
        android:icon="@drawable/ic_launcher.png"
        android:usesCleartextTraffic="true">
        <activity
          ...
        >
          ...
            <!--추가된 부분-->
            <meta-data
                android:name="com.google.firebase.messaging.default_notification_channel_id"
                android:value="high_importance_channel" />
        </activity>
    </application>
</manifest>


마치며

오랜만에 개발 글을 작성하게 되었는데 많은 도움이 되었으면 좋겠습니다. 
항상 느끼는 거지만 쉽게 설명하고 작성하는 것이 쉽지 않은 것 같습니다.
그래도 이 글을 보고 한번에 해결을 하셨으면 하는 바람이 있습니다.
추가로 궁금한 점이 있으시면 연락 주세요!!! 

반응형
공지사항
최근에 올라온 글