ทำไม Push Notification ถึงเป็นฟีเจอร์ที่ขาดไม่ได้ในแอปมือถือยุคนี้?
ลองนึกภาพดูครับ — คุณสร้างแอปที่ยอดเยี่ยม ฟีเจอร์ครบ UI สวย แต่ผู้ใช้กลับลืมเปิดแอปหลังจากติดตั้งไปสัปดาห์เดียว เจ็บปวดไหมล่ะ? สถิติจาก Firebase บอกว่าแอปที่มีระบบ Push Notification ที่ดีมี retention rate สูงกว่าแอปที่ไม่มีถึง 3-10 เท่า ไม่ว่าจะเป็นการแจ้งเตือนออเดอร์ใหม่ ข้อความแชท โปรโมชัน หรืออัปเดตสำคัญ — push notification คือสะพานเชื่อมระหว่างแอปกับผู้ใช้
ในบทความนี้ ผมจะพาคุณตั้งค่าระบบ Push Notification ใน .NET MAUI ด้วย Firebase Cloud Messaging (FCM v1) ตั้งแต่ต้นจนจบ รองรับทั้ง Android และ iOS ผมเคยเสียเวลาไปหลายวันกับเรื่องนี้ตอนที่ Google เพิ่ง deprecate Legacy API เลยรวบรวมทุกอย่างมาไว้ที่นี่ให้ครบเลย
สิ่งที่คุณจะได้จากบทความนี้:
- ตั้งค่า Firebase Project สำหรับ FCM v1 (Legacy API ถูกยกเลิกแล้วตั้งแต่กลางปี 2024)
- คอนฟิก Android และ iOS แบบ step-by-step ไม่ต้องเดา
- จัดการ notification ทั้ง foreground, background และเมื่อผู้ใช้แตะ notification
- Topic subscriptions สำหรับส่งแจ้งเตือนแบบกลุ่ม
- Rich notifications ที่มีรูปภาพและ action buttons
- Deep linking จาก notification ไปหน้าเฉพาะในแอป
สถาปัตยกรรมของระบบ Push Notification ใน .NET MAUI
ก่อนลงมือเขียนโค้ด เรามาดูภาพรวมของระบบกันก่อนดีกว่า เพราะถ้าไม่เข้าใจตรงนี้ พอเจอ bug จะ debug ยากมาก ระบบ Push Notification ประกอบด้วย 3 ส่วนหลักๆ:
1. Firebase Cloud Messaging (FCM)
FCM ทำหน้าที่เป็นตัวกลางในการส่งข้อความ สำหรับ Android นั้น FCM จัดการทุกอย่างโดยตรง ส่วน iOS จะใช้ FCM เป็นตัวกลางที่ส่งต่อไปยัง Apple Push Notification service (APNs) อีกทีหนึ่ง
2. Device Registration
เมื่อแอปเปิดครั้งแรก จะลงทะเบียนกับ FCM และได้รับ registration token ซึ่งเป็น identifier เฉพาะของอุปกรณ์นั้น token นี้อาจเปลี่ยนได้ตลอดเวลา (ใช่ครับ ตลอดเวลาจริงๆ) ดังนั้นแอปต้องจัดการ token refresh ให้ถูกต้องด้วย
3. Backend Server
เซิร์ฟเวอร์ของคุณจะเก็บ device token และใช้ FCM v1 API เพื่อส่ง notification ผ่าน HTTP endpoint:
POST https://fcm.googleapis.com/v1/projects/{project-id}/messages:send
Authorization: Bearer {oauth2-token}
Content-Type: application/json
จุดสำคัญที่ต้องจำ: FCM Legacy API ถูกยกเลิกอย่างเป็นทางการตั้งแต่ 20 มิถุนายน 2024 ถ้าคุณยังใช้ Legacy API อยู่ ต้องย้ายมาใช้ FCM v1 ทันที มิฉะนั้นระบบ notification จะหยุดทำงาน พูดตรงๆ คือใช้ไม่ได้เลยครับ
ขั้นตอนที่ 1: สร้างและตั้งค่า Firebase Project
เริ่มจากการตั้งค่า Firebase ให้พร้อมก่อน ส่วนนี้ไม่ยากแต่ขาดไม่ได้:
- ไปที่ Firebase Console แล้ว sign in ด้วย Google Account
- กด "Add project" ตั้งชื่อโปรเจกต์ เช่น
MauiPushDemo - เปิดหรือปิด Google Analytics ตามต้องการ แล้วกด "Create project"
- เมื่อโปรเจกต์พร้อม ไปที่ Project Settings → Cloud Messaging ตรวจสอบว่า Firebase Cloud Messaging API (V1) แสดงสถานะ Enabled
สร้าง Service Account Key สำหรับ FCM v1
สำหรับการส่ง notification จากเซิร์ฟเวอร์ คุณต้องมี Service Account Key:
- ใน Firebase Console ไปที่ Project Settings → Service accounts
- กด "Generate new private key"
- ดาวน์โหลดไฟล์ JSON เก็บไว้อย่างปลอดภัย — ไฟล์นี้มี credentials สำหรับเข้าถึง FCM v1 API ห้ามเอาไป commit ใน git เด็ดขาดนะครับ
ขั้นตอนที่ 2: ตั้งค่า .NET MAUI Project
สร้างโปรเจกต์และติดตั้ง NuGet Package
เอาล่ะ มาเริ่มเขียนโค้ดกันเลย สร้างโปรเจกต์ .NET MAUI แล้วติดตั้ง library สำหรับ Firebase Push Notifications:
# สร้างโปรเจกต์ .NET MAUI ใหม่
dotnet new maui -n MauiPushDemo
# ติดตั้ง Plugin.FirebasePushNotifications
cd MauiPushDemo
dotnet add package Plugin.FirebasePushNotifications
Plugin.FirebasePushNotifications รองรับ .NET 8 ขึ้นไป และทำงานได้ทั้ง Android และ iOS ผมแนะนำตัวนี้เพราะมันจัดการ platform-specific code ให้เราเกือบทั้งหมด ประหยัดเวลาได้เยอะ
ทางเลือกอื่นที่ใช้ได้:
- Plugin.Firebase.CloudMessaging — รองรับ .NET 9+ มี native SDK ที่อัปเดตกว่า
- Shaunebu.MAUI.FirebasePushNotifications — lightweight สำหรับโปรเจกต์ที่ต้องการแค่ push notification
- Manual implementation — ควบคุมได้ทุกอย่าง แต่ต้องเขียน platform-specific code เอง (เหมาะกับคนที่ชอบลุยเอง)
ขั้นตอนที่ 3: ตั้งค่า Android สำหรับ FCM
ลงทะเบียนแอป Android ใน Firebase
- ใน Firebase Console กด "Add app" → เลือก Android
- กรอก Android package name ให้ตรงกับ
ApplicationIdในไฟล์.csprojของคุณ - ดาวน์โหลด google-services.json
- วางไฟล์ไว้ที่ Platforms/Android/ ในโปรเจกต์
แก้ไขไฟล์ .csproj
เพิ่มการอ้างอิง google-services.json ในไฟล์ .csproj:
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0-android'">
<GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>
ตั้งค่า AndroidManifest.xml
เพิ่ม permission สำหรับ notification ใน Platforms/Android/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true"
android:icon="@mipmap/appicon"
android:roundIcon="@mipmap/appicon_round"
android:supportsRtl="true">
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
หมายเหตุสำคัญ: ตั้งแต่ Android 13 (API 33) ขึ้นไป แอปต้องขอ permission POST_NOTIFICATIONS จากผู้ใช้แบบ runtime ด้วย ไม่ใช่แค่ประกาศใน manifest เท่านั้น ตรงนี้หลายคนพลาดกันเยอะมาก
สร้าง FirebaseMessagingService สำหรับ Android
สร้างไฟล์ Platforms/Android/Services/MauiFirebaseMessagingService.cs นี่คือหัวใจของฝั่ง Android เลย:
using Android.App;
using Android.Content;
using AndroidX.Core.App;
using Firebase.Messaging;
namespace MauiPushDemo.Platforms.Android.Services;
[Service(Exported = true)]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class MauiFirebaseMessagingService : FirebaseMessagingService
{
public override void OnNewToken(string token)
{
base.OnNewToken(token);
// ส่ง token ไปเก็บที่เซิร์ฟเวอร์ของคุณ
System.Diagnostics.Debug.WriteLine($"FCM Token: {token}");
}
public override void OnMessageReceived(RemoteMessage message)
{
base.OnMessageReceived(message);
var title = message.GetNotification()?.Title ?? "แจ้งเตือน";
var body = message.GetNotification()?.Body ?? "";
SendLocalNotification(title, body, message.Data);
}
private void SendLocalNotification(string title, string body,
IDictionary<string, string> data)
{
var channelId = "default_channel";
var notificationId = (int)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
// แนบ data จาก notification เพื่อใช้ deep linking
foreach (var kvp in data)
{
intent.PutExtra(kvp.Key, kvp.Value);
}
var pendingIntent = PendingIntent.GetActivity(
this, 0, intent, PendingIntentFlags.Immutable | PendingIntentFlags.UpdateCurrent);
var builder = new NotificationCompat.Builder(this, channelId)
.SetAutoCancel(true)
.SetContentTitle(title)
.SetContentText(body)
.SetSmallIcon(Resource.Drawable.notification_icon)
.SetContentIntent(pendingIntent)
.SetPriority(NotificationCompat.PriorityHigh);
var notificationManager = NotificationManagerCompat.From(this);
notificationManager.Notify(notificationId, builder.Build());
}
}
ตั้งค่า Notification Channel ใน MainActivity
ตั้งแต่ Android 8.0 (Oreo) ขึ้นไป ต้องสร้าง Notification Channel ก่อนส่ง notification ถ้าลืมตั้งค่าตรงนี้ notification จะไม่แสดงเลยนะครับ ไม่มี error อะไรด้วย แค่เงียบไปเฉยๆ แก้ไข Platforms/Android/MainActivity.cs:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
namespace MauiPushDemo;
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
ConfigurationChanges = ConfigChanges.ScreenSize
| ConfigChanges.Orientation
| ConfigChanges.UiMode
| ConfigChanges.ScreenLayout
| ConfigChanges.SmallestScreenSize
| ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
CreateNotificationChannel();
}
private void CreateNotificationChannel()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O) return;
var channel = new NotificationChannel(
"default_channel",
"การแจ้งเตือนทั่วไป",
NotificationImportance.High)
{
Description = "ช่องแจ้งเตือนหลักของแอป"
};
var manager = (NotificationManager?)GetSystemService(NotificationService);
manager?.CreateNotificationChannel(channel);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
// จัดการเมื่อผู้ใช้แตะ notification ขณะแอปเปิดอยู่
if (intent?.Extras != null)
{
var navigateTo = intent.Extras.GetString("navigate_to");
if (!string.IsNullOrEmpty(navigateTo))
{
// ส่ง event ไปยัง shared code สำหรับ deep linking
MessagingCenter.Send<object, string>(
this, "NotificationTapped", navigateTo);
}
}
}
}
ขั้นตอนที่ 4: ตั้งค่า iOS สำหรับ APNs + FCM
ฝั่ง iOS จะยุ่งยากกว่า Android นิดหน่อย เพราะต้องตั้งค่า APNs เพิ่มอีกชั้นหนึ่ง แต่ไม่ต้องกลัวครับ ทำตามนี้ได้เลย
ลงทะเบียนแอป iOS ใน Firebase
- ใน Firebase Console กด "Add app" → เลือก iOS
- กรอก Bundle ID ให้ตรงกับ
ApplicationIdในไฟล์.csproj - ดาวน์โหลด GoogleService-Info.plist
- วางไฟล์ไว้ที่ Platforms/iOS/ ในโปรเจกต์
เพิ่มไฟล์ plist ใน .csproj
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0-ios'">
<BundleResource Include="Platforms\iOS\GoogleService-Info.plist" />
</ItemGroup>
ตั้งค่า APNs Key ใน Firebase
iOS ใช้ Apple Push Notification service (APNs) เป็น transport layer โดย FCM จะส่งต่อ notification ไปยัง APNs ให้อัตโนมัติ แต่คุณต้องตั้งค่า APNs Key ก่อน:
- ไปที่ Apple Developer → Certificates, Identifiers & Profiles → Keys
- สร้าง Key ใหม่ ติ๊กเลือก "Apple Push Notifications service (APNs)"
- ดาวน์โหลด .p8 key file และจดจำ Key ID
- กลับไปที่ Firebase Console → Project Settings → Cloud Messaging → Apple app configuration
- อัปโหลด APNs Authentication Key (.p8) พร้อมกรอก Key ID และ Team ID
สร้าง Entitlements.plist
สร้างไฟล์ Platforms/iOS/Entitlements.plist เพื่อเปิดใช้งาน Push Notifications:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
หมายเหตุ: เมื่อจะ publish ไป App Store ให้เปลี่ยน development เป็น production อย่าลืมเปลี่ยนนะครับ ไม่งั้น notification จะไม่เข้าบน production
ตั้งค่า AppDelegate สำหรับ iOS
แก้ไข Platforms/iOS/AppDelegate.cs เพื่อลงทะเบียนรับ remote notifications:
using Foundation;
using UIKit;
using UserNotifications;
namespace MauiPushDemo;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override bool FinishedLaunching(
UIApplication application, NSDictionary launchOptions)
{
// ขอ permission แสดง notification
UNUserNotificationCenter.Current.RequestAuthorization(
UNAuthorizationOptions.Alert
| UNAuthorizationOptions.Badge
| UNAuthorizationOptions.Sound,
(granted, error) =>
{
if (granted)
{
InvokeOnMainThread(() =>
{
UIApplication.SharedApplication
.RegisterForRemoteNotifications();
});
}
});
UNUserNotificationCenter.Current.Delegate =
new NotificationDelegate();
return base.FinishedLaunching(application, launchOptions);
}
// รับ APNs token แล้วส่งให้ Firebase
public override void RegisteredForRemoteNotifications(
UIApplication application, NSData deviceToken)
{
Firebase.CloudMessaging.Messaging.SharedInstance.ApnsToken = deviceToken;
}
}
public class NotificationDelegate : UNUserNotificationCenterDelegate
{
// จัดการ notification เมื่อแอปเปิดอยู่ (foreground)
public override void WillPresentNotification(
UNUserNotificationCenter center,
UNNotification notification,
Action<UNNotificationPresentationOptions> completionHandler)
{
completionHandler(
UNNotificationPresentationOptions.Banner
| UNNotificationPresentationOptions.Sound);
}
// จัดการเมื่อผู้ใช้แตะ notification
public override void DidReceiveNotificationResponse(
UNUserNotificationCenter center,
UNNotificationResponse response,
Action completionHandler)
{
var userInfo = response.Notification.Request.Content.UserInfo;
var navigateTo = userInfo.ObjectForKey(
new NSString("navigate_to"))?.ToString();
if (!string.IsNullOrEmpty(navigateTo))
{
MessagingCenter.Send<object, string>(
new object(), "NotificationTapped", navigateTo);
}
completionHandler();
}
}
ขั้นตอนที่ 5: ตั้งค่า MauiProgram.cs และ Shared Code
ลงทะเบียน Firebase Services
ถึงตรงนี้เราตั้งค่า platform-specific เสร็จแล้ว ทีนี้มาดู shared code กันบ้าง แก้ไข MauiProgram.cs เพื่อเริ่มต้นระบบ notification:
using Plugin.FirebasePushNotifications;
namespace MauiPushDemo;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseFirebasePushNotifications()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// ลงทะเบียน service สำหรับจัดการ notification
builder.Services.AddSingleton<IPushNotificationService,
PushNotificationService>();
return builder.Build();
}
}
สร้าง Push Notification Service
สร้าง service กลางสำหรับจัดการ notification ที่ใช้ได้ทั้ง Android และ iOS ผมชอบแยก service ออกมาแบบนี้ เพราะทำให้ test ง่ายและ ViewModel ไม่ต้องรู้เรื่อง platform-specific เลย:
namespace MauiPushDemo.Services;
public interface IPushNotificationService
{
Task<string?> GetTokenAsync();
Task SubscribeToTopicAsync(string topic);
Task UnsubscribeFromTopicAsync(string topic);
event EventHandler<PushNotificationEventArgs> NotificationReceived;
event EventHandler<PushNotificationEventArgs> NotificationTapped;
}
public class PushNotificationEventArgs : EventArgs
{
public string? Title { get; set; }
public string? Body { get; set; }
public IDictionary<string, string> Data { get; set; } = new Dictionary<string, string>();
}
public class PushNotificationService : IPushNotificationService
{
public event EventHandler<PushNotificationEventArgs>? NotificationReceived;
public event EventHandler<PushNotificationEventArgs>? NotificationTapped;
public PushNotificationService()
{
// ลงทะเบียน event handlers
CrossFirebasePushNotification.Current.OnNotificationReceived += (s, e) =>
{
var args = new PushNotificationEventArgs
{
Title = e.Data.ContainsKey("title")
? e.Data["title"]?.ToString() : null,
Body = e.Data.ContainsKey("body")
? e.Data["body"]?.ToString() : null,
Data = e.Data.ToDictionary(
k => k.Key, v => v.Value?.ToString() ?? "")
};
NotificationReceived?.Invoke(this, args);
};
CrossFirebasePushNotification.Current.OnNotificationOpened += (s, e) =>
{
var args = new PushNotificationEventArgs
{
Data = e.Data.ToDictionary(
k => k.Key, v => v.Value?.ToString() ?? "")
};
NotificationTapped?.Invoke(this, args);
};
}
public async Task<string?> GetTokenAsync()
{
await CrossFirebasePushNotification.Current
.RegisterForPushNotificationsAsync();
return CrossFirebasePushNotification.Current.Token;
}
public async Task SubscribeToTopicAsync(string topic)
{
CrossFirebasePushNotification.Current.SubscribeTo(topic);
await Task.CompletedTask;
}
public async Task UnsubscribeFromTopicAsync(string topic)
{
CrossFirebasePushNotification.Current.UnsubscribeFrom(topic);
await Task.CompletedTask;
}
}
ขั้นตอนที่ 6: จัดการ Notification ใน ViewModel
สร้าง MainViewModel สำหรับแสดงผลและจัดการ Notification
นี่คือ ViewModel ที่รวมฟีเจอร์หลักทั้งหมดของระบบ notification ไว้ในที่เดียว โค้ดอาจดูยาวหน่อย แต่แต่ละส่วนทำหน้าที่ชัดเจนครับ:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace MauiPushDemo.ViewModels;
public class MainViewModel : INotifyPropertyChanged
{
private readonly IPushNotificationService _pushService;
public ObservableCollection<NotificationItem> Notifications { get; } = new();
private string? _fcmToken;
public string? FcmToken
{
get => _fcmToken;
set { _fcmToken = value; OnPropertyChanged(); }
}
private string? _statusMessage;
public string? StatusMessage
{
get => _statusMessage;
set { _statusMessage = value; OnPropertyChanged(); }
}
public ICommand RegisterCommand { get; }
public ICommand SubscribeCommand { get; }
public ICommand CopyTokenCommand { get; }
public MainViewModel(IPushNotificationService pushService)
{
_pushService = pushService;
RegisterCommand = new Command(async () => await RegisterAsync());
SubscribeCommand = new Command<string>(
async (topic) => await SubscribeAsync(topic));
CopyTokenCommand = new Command(async () => await CopyTokenAsync());
// ฟัง notification events
_pushService.NotificationReceived += (s, e) =>
{
MainThread.BeginInvokeOnMainThread(() =>
{
Notifications.Insert(0, new NotificationItem
{
Title = e.Title ?? "ไม่มีหัวข้อ",
Body = e.Body ?? "",
ReceivedAt = DateTime.Now
});
StatusMessage = $"ได้รับ notification: {e.Title}";
});
};
_pushService.NotificationTapped += (s, e) =>
{
if (e.Data.TryGetValue("navigate_to", out var route))
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await Shell.Current.GoToAsync(route);
});
}
};
}
private async Task RegisterAsync()
{
try
{
FcmToken = await _pushService.GetTokenAsync();
StatusMessage = "ลงทะเบียนสำเร็จ!";
}
catch (Exception ex)
{
StatusMessage = $"เกิดข้อผิดพลาด: {ex.Message}";
}
}
private async Task SubscribeAsync(string topic)
{
await _pushService.SubscribeToTopicAsync(topic);
StatusMessage = $"สมัครรับแจ้งเตือนหัวข้อ '{topic}' แล้ว";
}
private async Task CopyTokenAsync()
{
if (FcmToken != null)
{
await Clipboard.Default.SetTextAsync(FcmToken);
StatusMessage = "คัดลอก Token แล้ว";
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(
[CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(name));
}
public class NotificationItem
{
public string Title { get; set; } = "";
public string Body { get; set; } = "";
public DateTime ReceivedAt { get; set; }
}
ขั้นตอนที่ 7: ขอ Permission สำหรับ Android 13+
ตั้งแต่ Android 13 ขึ้นไป แอปต้องขอ runtime permission จากผู้ใช้ก่อนแสดง notification ตรงนี้สำคัญมากครับ ถ้าไม่ขอ permission notification จะไม่แสดงเลยแม้ว่าจะตั้งค่าอย่างอื่นถูกต้องหมดแล้วก็ตาม:
public static class PermissionHelper
{
public static async Task<bool> RequestNotificationPermissionAsync()
{
#if ANDROID
if (OperatingSystem.IsAndroidVersionAtLeast(33))
{
var status = await Permissions.CheckStatusAsync<
Permissions.PostNotifications>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<
Permissions.PostNotifications>();
}
return status == PermissionStatus.Granted;
}
#endif
// iOS จัดการ permission ผ่าน UNUserNotificationCenter ใน AppDelegate
return true;
}
}
เรียกใช้ method นี้ตอนแอปเปิดครั้งแรก หรือก่อนลงทะเบียนรับ notification:
// ใน App.xaml.cs หรือ MainPage
protected override async void OnAppearing()
{
base.OnAppearing();
var granted = await PermissionHelper.RequestNotificationPermissionAsync();
if (!granted)
{
await DisplayAlert("แจ้งเตือน",
"กรุณาอนุญาตการแจ้งเตือนเพื่อรับข่าวสารล่าสุด",
"ตกลง");
}
}
ขั้นตอนที่ 8: Topic Subscriptions — ส่งแจ้งเตือนแบบกลุ่ม
Topic subscription เป็นฟีเจอร์ที่ผมว่าคุ้มค่ามากที่สุดของ FCM เลย ช่วยให้คุณส่ง notification ไปยังกลุ่มผู้ใช้ที่สนใจหัวข้อเดียวกัน โดยไม่ต้องเก็บ token ของทุกคนเอง FCM จัดการให้หมด
ตัวอย่างการใช้งาน Topic
// สมัครรับข่าวสารตามหมวดหมู่
await pushService.SubscribeToTopicAsync("promotions");
await pushService.SubscribeToTopicAsync("order_updates");
await pushService.SubscribeToTopicAsync("news_th"); // เฉพาะข่าวภาษาไทย
// ยกเลิกการสมัคร
await pushService.UnsubscribeFromTopicAsync("promotions");
ส่ง Notification ไปยัง Topic จากเซิร์ฟเวอร์
ตัวอย่างการส่ง notification ผ่าน FCM v1 API จากเซิร์ฟเวอร์ C# สังเกตว่าเราสามารถกำหนดค่าเฉพาะ platform ได้ด้วย (Android ใช้ channel, iOS ใช้ badge):
using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using Google.Apis.Auth.OAuth2;
// เริ่มต้น Firebase Admin SDK (ทำครั้งเดียว)
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromFile("service-account-key.json")
});
// ส่ง notification ไปยัง topic
var message = new Message
{
Topic = "promotions",
Notification = new Notification
{
Title = "โปรโมชันพิเศษ!",
Body = "ลด 50% สำหรับสมาชิกใหม่วันนี้เท่านั้น"
},
Data = new Dictionary<string, string>
{
{ "navigate_to", "//promotions/detail?id=123" },
{ "promo_code", "MAUI50" }
},
Android = new AndroidConfig
{
Priority = Priority.High,
Notification = new AndroidNotification
{
ChannelId = "default_channel",
Icon = "notification_icon",
Color = "#4A90D9"
}
},
Apns = new ApnsConfig
{
Aps = new Aps
{
Badge = 1,
Sound = "default"
}
}
};
var response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
Console.WriteLine($"ส่งสำเร็จ: {response}");
ขั้นตอนที่ 9: Rich Notifications — รูปภาพและ Action Buttons
Notification ธรรมดาอาจไม่ดึงดูดพอ โดยเฉพาะแอป e-commerce ที่อยากโชว์รูปสินค้า Rich notification ช่วยให้คุณแนบรูปภาพและเพิ่มปุ่มกดได้
ส่ง Rich Notification จากเซิร์ฟเวอร์
var richMessage = new Message
{
Token = deviceToken,
Notification = new Notification
{
Title = "สินค้าใหม่มาแล้ว!",
Body = "iPhone 17 Pro Max พร้อมส่งแล้ววันนี้",
ImageUrl = "https://example.com/images/iphone17.jpg"
},
Android = new AndroidConfig
{
Notification = new AndroidNotification
{
ChannelId = "default_channel",
ImageUrl = "https://example.com/images/iphone17.jpg",
ClickAction = "OPEN_PRODUCT"
}
},
Apns = new ApnsConfig
{
Aps = new Aps
{
MutableContent = true
},
FcmOptions = new ApnsFcmOptions
{
ImageUrl = "https://example.com/images/iphone17.jpg"
}
}
};
ข้อควรรู้สำหรับ iOS: การแสดงรูปภาพใน notification บน iOS ต้องสร้าง Notification Service Extension เพิ่มเติม ซึ่งเป็น extension แยกต่างหากที่ดาวน์โหลดรูปภาพก่อนแสดง notification ต้องตั้งค่า MutableContent = true เพื่อให้ extension ทำงานได้ ตรงนี้อาจจะดูยุ่งยากนิดหน่อยแต่ก็จำเป็นจริงๆ ครับ
ขั้นตอนที่ 10: Deep Linking จาก Notification
Deep linking ช่วยให้ผู้ใช้แตะ notification แล้วไปหน้าที่ต้องการได้ทันที แทนที่จะเปิดหน้าแรกของแอปเสมอ จากประสบการณ์ผม ฟีเจอร์นี้ทำให้ engagement ดีขึ้นเยอะเลย
ตั้งค่า Shell Routes
// ใน AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// ลงทะเบียน routes สำหรับ deep linking
Routing.RegisterRoute("orders/detail", typeof(OrderDetailPage));
Routing.RegisterRoute("promotions/detail", typeof(PromoDetailPage));
Routing.RegisterRoute("chat", typeof(ChatPage));
}
}
จัดการ Navigation จาก Notification Data
public class NotificationNavigationService
{
public static async Task HandleNotificationNavigation(
IDictionary<string, string> data)
{
if (!data.TryGetValue("navigate_to", out var route))
return;
// รอให้ Shell พร้อมก่อน navigate
await Task.Delay(500);
await MainThread.InvokeOnMainThreadAsync(async () =>
{
try
{
// สร้าง query parameters จาก notification data
var queryParams = new Dictionary<string, object>();
foreach (var kvp in data.Where(
d => d.Key != "navigate_to"))
{
queryParams[kvp.Key] = kvp.Value;
}
await Shell.Current.GoToAsync(
route, queryParams);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Navigation error: {ex.Message}");
// fallback ไปหน้าแรกถ้า route ไม่ถูกต้อง
await Shell.Current.GoToAsync("//main");
}
});
}
}
การทดสอบ Push Notification
เอาจริงๆ แล้ว การทดสอบ push notification อาจจะเป็นส่วนที่น่าหงุดหงิดที่สุด เพราะต้องอาศัย external service และอุปกรณ์จริง (โดยเฉพาะ iOS) แต่มีหลายวิธีที่ช่วยได้:
1. ทดสอบผ่าน Firebase Console
วิธีนี้ง่ายที่สุดสำหรับการทดสอบเบื้องต้น:
- ไปที่ Firebase Console → Engage → Messaging
- กด "Create your first campaign" → Notifications
- กรอกหัวข้อและข้อความ
- กด "Send test message" แล้ววาง FCM token ของอุปกรณ์
2. ทดสอบด้วย cURL
ส่ง notification ตรงผ่าน FCM v1 API เหมาะสำหรับทดสอบระหว่าง develop:
# ต้อง generate OAuth2 token ก่อน
# ใช้ gcloud CLI:
TOKEN=$(gcloud auth application-default print-access-token)
curl -X POST \
"https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"token": "DEVICE_FCM_TOKEN",
"notification": {
"title": "ทดสอบ",
"body": "ทดสอบการส่ง notification จาก cURL"
},
"data": {
"navigate_to": "orders/detail",
"order_id": "12345"
}
}
}'
3. ทดสอบผ่าน Unit Test
[Fact]
public async Task SendNotification_ShouldReturnMessageId()
{
// Arrange
var message = new Message
{
Topic = "test_topic",
Notification = new Notification
{
Title = "Test",
Body = "Unit test notification"
}
};
// Act
var response = await FirebaseMessaging.DefaultInstance
.SendAsync(message, dryRun: true);
// Assert — dryRun จะ validate โดยไม่ส่งจริง
Assert.NotNull(response);
Assert.StartsWith("projects/", response);
}
Troubleshooting: ปัญหาที่พบบ่อยและวิธีแก้
ส่วนนี้ผมรวบรวมจากประสบการณ์จริงและปัญหาที่เห็นคนถามกันบ่อยในชุมชน .NET MAUI ครับ
1. ไม่ได้รับ Notification บน Android
- ตรวจสอบ Notification Channel: ต้องสร้าง channel ก่อนส่ง notification (Android 8.0+) ถ้าลืมตรงนี้จะไม่มี error ใดๆ เลย
- ตรวจสอบ Permission: Android 13+ ต้องขอ
POST_NOTIFICATIONSpermission - Battery Optimization: บาง manufacturer (Xiaomi, Huawei, Samsung) จำกัด background services ผู้ใช้อาจต้องปิด battery optimization สำหรับแอป ปัญหานี้แก้ยากเพราะแต่ละยี่ห้อตั้งค่าไม่เหมือนกัน
- ตรวจสอบ google-services.json: ต้องมี package name ที่ตรงกับ ApplicationId
2. ไม่ได้รับ Notification บน iOS
- Debug mode ไม่ทำงาน: FCM บน iOS ทำงานได้เฉพาะ Release mode เท่านั้น ทดสอบผ่าน TestFlight หรือ Ad Hoc distribution ตรงนี้ทำให้คนเสียเวลา debug กันเยอะมาก
- APNs Key: ตรวจสอบว่าอัปโหลด .p8 key ไปที่ Firebase Console แล้ว
- Entitlements.plist: ตรวจสอบว่าเพิ่ม
aps-environmentแล้ว - Physical device เท่านั้น: push notification ไม่ทำงานบน iOS Simulator
3. Token เป็น null
- ตรวจสอบว่า Firebase initialized สำเร็จ
- ตรวจสอบว่าอุปกรณ์มีการเชื่อมต่ออินเทอร์เน็ต
- สำหรับ iOS ต้องลงทะเบียน APNs token ก่อนจึงจะได้ FCM token
4. OnMessageReceived ไม่ถูกเรียกหลัง migrate เป็น FCM v1
ปัญหานี้พบบ่อยมากหลังจาก migrate จาก Legacy API ถ้าเจอปัญหานี้ลองตรวจสอบ:
- รูปแบบ JSON ของ data messages ใน FCM v1 แตกต่างจาก Legacy
- ต้องลงทะเบียน device ใหม่ภายใต้ platform FCM v1
- ใช้
FcmV1OutcomeCountstelemetry เพื่อตรวจสอบว่าใช้ platform ถูกต้อง
คำถามที่พบบ่อย (FAQ)
Push Notification ใน .NET MAUI ต่างจาก Local Notification อย่างไร?
Push Notification ถูกส่งจากเซิร์ฟเวอร์ภายนอก (เช่น Firebase) ไปยังอุปกรณ์ผ่านบริการของ platform (FCM สำหรับ Android, APNs สำหรับ iOS) ส่วน Local Notification ถูกสร้างและแสดงโดยแอปเองบนอุปกรณ์ โดยไม่ต้องพึ่ง server ตัวอย่างง่ายๆ — การตั้งเตือนเวลากินยาเป็น local notification ส่วนการแจ้งเตือนออเดอร์ใหม่จากร้านค้าเป็น push notification
ทำไม FCM Legacy API ถึงใช้ไม่ได้แล้ว ต้องทำอย่างไร?
Google ยกเลิก FCM Legacy HTTP API อย่างเป็นทางการเมื่อ 20 มิถุนายน 2024 ทุกแอปต้อง migrate มาใช้ FCM v1 API ซึ่งใช้ OAuth 2.0 สำหรับ authentication แทน server key เดิม ต้องอัปเดต endpoint เป็น https://fcm.googleapis.com/v1/projects/{project-id}/messages:send และใช้ Service Account Key สำหรับสร้าง Bearer token ถ้ายังไม่ได้ migrate ควรรีบทำเลยครับ
สามารถทดสอบ Push Notification บน Emulator ได้หรือไม่?
สำหรับ Android ทดสอบได้บน emulator ที่มี Google Play Services ติดตั้งอยู่ สำหรับ iOS ต้องใช้ physical device เท่านั้น เพราะ iOS Simulator ไม่รองรับ APNs นอกจากนี้ FCM บน iOS ยังทำงานได้เฉพาะ Release mode อีกด้วย ดังนั้นการทดสอบ iOS จะยุ่งยากกว่า Android พอสมควร
จะจัดการ Token Refresh อย่างไรเมื่อ Token เปลี่ยน?
FCM token สามารถเปลี่ยนได้ทุกเมื่อ เช่น เมื่อผู้ใช้ลบและติดตั้งแอปใหม่ ล้าง app data หรือ restore ไปยังอุปกรณ์ใหม่ แอปต้องฟัง event OnTokenRefresh หรือ override method OnNewToken แล้วส่ง token ใหม่ไปอัปเดตที่เซิร์ฟเวอร์ทันที มิฉะนั้นเซิร์ฟเวอร์จะส่ง notification ด้วย token เก่าที่ใช้ไม่ได้แล้ว อันนี้สำคัญมากครับ พลาดตรงนี้ notification จะหายไปเงียบๆ โดยไม่มี error
Plugin.FirebasePushNotifications กับ Plugin.Firebase.CloudMessaging ต่างกันอย่างไร เลือกใช้ตัวไหนดี?
Plugin.FirebasePushNotifications เน้นเฉพาะ push notification มีขนาดเล็กและ API ที่เรียบง่าย เหมาะสำหรับโปรเจกต์ที่ต้องการแค่ push notification ส่วน Plugin.Firebase.CloudMessaging เป็นส่วนหนึ่งของ Plugin.Firebase ที่ใหญ่กว่า รองรับ Firebase services อื่นๆ ด้วย เช่น Analytics, Crashlytics ถ้าคุณใช้ Firebase หลายบริการ ให้เลือก Plugin.Firebase แต่ถ้าต้องการแค่ push notification อย่างเดียว ให้เลือก Plugin.FirebasePushNotifications เพื่อลดขนาดแอป ส่วนตัวผมมักเริ่มด้วยตัวเล็กก่อนแล้วค่อยเปลี่ยนทีหลังถ้าจำเป็น