[Flutter] Android에서 FCM 연동(2) - 알림 클릭 시 웹 링크로 이동하기

구현 목표

사용자가 알림을 클릭했을 때, 지정된 웹 링크로 이동하는 기능을 구현한다.

 

FCM 메시지 수신 시 기기 상태

상태 설명
Foreground 앱이 켜져 있고 사용자에게 보여지고 있는 상태
Background 앱은 실행 중이지만 백그라운드로 전환된 상태 (예: 홈 버튼 누름)
Terminated 앱이 완전히 종료된 상태

각 상태에 따라 알림 수신 및 처리 방식이 다르므로, 상태에 맞는 코드를 구현해야 한다.

 

기본 설정

기본적인 FCM 연동 방법은 다음 글을 참고한다.

[Flutter] Android에서 FCM 연동(1) - 푸시 알림 수신 테스트

 

flutter pub add url_launcher

`url_launcher` 패키지를 설치한다.

 

 

import 'package:url_launcher/url_launcher.dart';

Future<void> openUrlInExternalApp(Uri url) async {
  if (!await launchUrl(
    url,
    mode: LaunchMode.externalApplication,
  )) {
    throw Exception('Could not launch $url');
  }
}

`url_launcher` 패키지를 이용해서 링크를 실행하는 유틸 함수를 만들었다.

 

{
  "notification": {
    "title": "새 소식!",
    "body": "지금 확인해보세요!"
  },
  "data": {
    "link": "https://www.google.com/"
  }
}

다음과 같은 구조로 메시지를 전송했다고 가정하고 설명한다.

 

Foreground 상태

Flutter는 포그라운드 상태에서 FCM 알림을 자동으로 띄우지 않기 때문에,

`flutter_local_notifications` 패키지를 이용해서 로컬 알림을 통해 사용자에게 보여줘야 한다.

 

초기화 및 수신 처리

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

Future<void> _initLocalNotification() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  const InitializationSettings initializationSettings =
      InitializationSettings(android: initializationSettingsAndroid);

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    // 포그라운드 알림 클릭 시 링크로 이동
    onDidReceiveNotificationResponse: (NotificationResponse response) {
      final link = response.payload;
      if (link != null && link.isNotEmpty) {
        openUrlInExternalApp(Uri.parse(link));
      }
    },
  );
}

 

알림 표시

Future<void> _showNotification(RemoteMessage message) async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'fcm_default_channel', // 채널 ID
    'FCM 알림', // 채널 이름
    importance: Importance.max,
    priority: Priority.high,
  );

  await flutterLocalNotificationsPlugin.show(
    0,
    message.notification?.title ?? '알림',
    message.notification?.body ?? '메시지가 도착했습니다.',
    NotificationDetails(android: androidNotificationDetails);
    payload: message.data['link'], // 클릭 시 사용할 링크
  );
}

 

포그라운드 수신 리스너

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  if (message.notification != null) {
    _showNotification(message);
  }
});

 

Background 상태

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  final link = message.data['link'];
  if (link != null) {
    openUrlInExternalApp(
      Uri.parse(link),
    );
  }
});

알림 클릭 시 Firebase가 자동으로 앱을 열고, `onMessageOpenedApp`에서 메시지를 전달받는다.

 

Terminated 상태

RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
    final link = initialMessage.data['link'];
    if (link != null) {
      openUrlInExternalApp(Uri.parse(link));
    }
}

앱이 완전히 종료된 상태에서 알림을 클릭한 경우, `getInitialMessage()`로 메시지를 가져와야 한다.

 

전체 코드

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'firebase_options.dart';
import 'utils/launch_utils.dart';
import 'screens/root_screen.dart';

final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
    FlutterLocalNotificationsPlugin();

// 로컬 알림 초기화
Future<void> _initLocalNotification() async {
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  const InitializationSettings initializationSettings =
      InitializationSettings(android: initializationSettingsAndroid);

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    // 포그라운드 알림 클릭 시 링크로 이동
    onDidReceiveNotificationResponse: (NotificationResponse response) {
      final link = response.payload;
      if (link != null && link.isNotEmpty) {
        openUrlInExternalApp(Uri.parse(link));
      }
    },
  );
}

// 포그라운드 알림 표시
Future<void> _showNotification(RemoteMessage message) async {
  const AndroidNotificationDetails androidNotificationDetails =
      AndroidNotificationDetails(
    'fcm_default_channel', // 채널 ID
    'FCM 알림', // 채널 이름
    channelDescription: '포그라운드 FCM 알림',
    importance: Importance.max,
    priority: Priority.high,
    ticker: 'ticker',
  );

  const NotificationDetails notificationDetails =
      NotificationDetails(android: androidNotificationDetails);

  await flutterLocalNotificationsPlugin.show(
    0,
    message.notification?.title ?? '알림',
    message.notification?.body ?? '메시지가 도착했습니다.',
    notificationDetails,
    payload: message.data['link'], // 클릭 시 사용할 링크
  );
}

// 백그라운드 알림 리스너
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  print("Handling a background message: ${message.messageId}");
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  // 앱이 종료된 상태에서 알림 클릭하는 경우 대비
  RemoteMessage? initialMessage =
      await FirebaseMessaging.instance.getInitialMessage();
  if (initialMessage != null) {
    final link = initialMessage.data['link'];
    if (link != null) {
      openUrlInExternalApp(Uri.parse(link));
    }
  }
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _token = '';

  @override
  void initState() {
    super.initState();
    _initLocalNotification();
    _initFCM();
  }

  void _initFCM() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;

    messaging.subscribeToTopic('all'); // FCM 모든 사용자의 토큰 구독

    // 권한 요청 (Android 13 이상)
    NotificationSettings settings = await messaging.requestPermission();
    print('User granted permission: ${settings.authorizationStatus}');

    // 포그라운드 알림 리스너
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      print('Message data: ${message.data}');

      if (message.notification != null) {
        print('Message also contained a notification: ${message.notification}');
        _showNotification(message);
      }
    });

    // 알림 클릭 시 링크로 이동
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      final link = message.data['link'];
      if (link != null) {
        openUrlInExternalApp(
          Uri.parse(link),
        );
      }
    });

    // 토큰 확인
    String? token = await messaging.getToken();
    print('token: $token');
    setState(() {
      _token = token;
    });
  }

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

 

구현 결과

 

플러터 FCM 연동 시리즈