flutter

Hello AnakInformatika! Ever wondered how your favorite apps can suddenly send notifications like "Flash Sale Discount!" or "New message!" even when the app isn't open? Well, that's all thanks to a magical feature called Push Notification. This feature is crucial for maintaining user engagement and ensuring important information reaches users on time.

In this deep-dive tutorial, we will thoroughly break down how to Build Push Notifications in Flutter Using Firebase and Go Backend. Why this specific stack? Flutter is the champion for beautiful, cross-platform UI/UX; Firebase (specifically Firebase Cloud Messaging or FCM) is the most reliable "postal courier" for delivering notifications; and Go is a super-efficient, blazing-fast backend choice to trigger those notifications. Perfect combination, right?

Think of it this way: your Flutter app is a house, and the notification is an important letter. Firebase Cloud Messaging (FCM) is the post office holding the address directory for all houses (devices). Meanwhile, our Go backend is the "assistant" who writes the letter (notification payload) and hands it over to the post office (FCM) to be delivered to the correct house. Pretty neat, right?

💡 Tip: Push notifications aren't just for promotions! You can also use them for transaction alerts, new chats, status updates, or important reminders that significantly improve the user experience.

Prerequisites: What Do You Need?

Before jumping into the code, make sure you have the following gear ready:

  • Flutter SDK: Ensure it is installed and on the latest stable version.

  • Go: Version 1.18 or newer installed on your system.

  • Google Account & Firebase Project: Set up a new Firebase project or use an existing one.

  • IDE: Visual Studio Code (VS Code) is highly recommended for both Flutter and Go.

  • Basic Understanding of Flutter & Go: Familiarity with the basic syntax and concepts of both platforms.

  • Emulator or Physical Device: For testing notifications on Android or iOS.

Step-by-Step Implementation

Grab your coffee and let's begin our coding journey!

1. Firebase & Flutter Setup

The first step is building the foundation on Firebase and connecting it to our Flutter application.

1.1. Create a New Firebase Project

  1. Open the Firebase Console.

  2. Click "Add project".

  3. Follow the steps to create a new project. Name it something recognizable, such as AnakInformatikaPush.

1.2. Add Flutter App to Firebase Project

You need to register both Android and iOS apps inside your Firebase project so Firebase can identify the devices.

For Android:
  1. In your project overview, click the Android icon.

  2. Enter your Flutter app's package name (found in android/app/src/main/AndroidManifest.xml under the package attribute). Example: com.anakinformatika.pushnotification.

  3. Register the app.

  4. Download the google-services.json file and place it in the android/app/ directory.

  5. Add the Firebase configuration to your android/build.gradle and android/app/build.gradle files as instructed by Firebase.

For iOS:
  1. Click the iOS icon in the project overview.

  2. Enter your Bundle ID (visible in Xcode under General > Identity).

  3. Register the app.

  4. Download GoogleService-Info.plist and place it inside ios/Runner/ via Xcode. Ensure it is added to the app target.

  5. Enable Push Notifications and Background Modes (Remote notifications) in Xcode under "Signing & Capabilities".

⚠️ Important Note: Ensure you have activated the Cloud Messaging API (Legacy) or set up proper service account credentials in Google Cloud Console. In the Firebase Console, navigate to Project Settings > Cloud Messaging to retrieve your Server Key (Legacy). Store this key securely!

2. Configuring Flutter to Receive Notifications

Now, let's prepare our Flutter app to receive and process incoming notifications.

2.1. Add Dependencies

Open pubspec.yaml in your Flutter project and add the following dependencies:

YAML
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2
  firebase_messaging: ^14.7.12

Run flutter pub get in your terminal.

2.2. Initialize Firebase and Handle Notifications

Open main.dart and add the initialization logic along with handlers for different notification states:

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

// Top-level function to handle background/terminated messages
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print("Handling a background message: ${message.messageId}");
  // You can also perform local database storage here if needed
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Register background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Push Notification',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  String? _token;
  String _message = 'No notifications yet';

  @override
  void initState() {
    super.initState();
    _initFirebaseMessaging();
  }

  Future<void> _initFirebaseMessaging() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;

    // 1. Request Permission (Required for iOS & Web)
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');

    // 2. Get Device FCM Token
    String? token = await messaging.getToken();
    setState(() {
      _token = token;
    });
    print("FCM Token: $_token");

    // 3. Listener for Token Refresh
    messaging.onTokenRefresh.listen((newToken) {
      setState(() {
        _token = newToken;
      });
      print("New FCM Token: $newToken");
      // Send this updated token to your backend server!
    }).onError((err) {
      print("Error refreshing token: $err");
    });

    // 4. Handle messages when the app is in Foreground
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground!');
      print('Message data: ${message.data}');

      if (message.notification != null) {
        print('Message notification: ${message.notification?.title} - ${message.notification?.body}');
        setState(() {
          _message = 'Foreground Notification: ${message.notification?.title ?? ''} - ${message.notification?.body ?? ''}';
        });
      }
    });

    // 5. Handle message when app is opened from Terminated state
    RemoteMessage? initialMessage = await messaging.getInitialMessage();
    if (initialMessage != null) {
      print('App opened from terminated state by notification!');
      setState(() {
        _message = 'Opened from terminated: ${initialMessage.notification?.title ?? ''} - ${initialMessage.notification?.body ?? ''}';
      });
    }

    // 6. Handle message when app is opened from Background state
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('App opened from background by notification!');
      setState(() {
        _message = 'Opened from background: ${message.notification?.title ?? ''} - ${message.notification?.body ?? ''}';
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Push Notification'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'Your FCM Token:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              SelectableText(
                _token ?? 'Token unavailable',
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 16),
              ),
              const SizedBox(height: 30),
              const Text(
                'Last Notification Status:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              Text(
                _message,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 16, color: Colors.green),
              ),
              const SizedBox(height: 20),
              const Text(
                'Copy the FCM Token above to send notifications from your Go backend!',
                textAlign: TextAlign.center,
                style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

💡 Tip: Make sure you understand the difference between onMessage (foreground), onBackgroundMessage (background/terminated), and onMessageOpenedApp / getInitialMessage (when the app is launched via notification click). This distinction is key to building a smooth user experience.

3. Building the Go Backend to Send Notifications

Now that Flutter is configured, let's write our Go backend service that acts as the "post office client" triggering messages to FCM.

3.1. Initialize Go Project

Create a folder for your backend project and initialize a Go module:

Bash
mkdir go-fcm-backend
cd go-fcm-backend
go mod init go-fcm-backend

3.2. Implement Notification Logic

Create a main.go file with the following HTTP handler implementation:

Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

// NotificationRequest defines the structure sent from client to our Go backend
type NotificationRequest struct {
	Token string            `json:"token"` // Target device FCM token
	Title string            `json:"title"`
	Body  string            `json:"body"`
	Data  map[string]string `json:"data,omitempty"` // Optional custom data payload
}

func main() {
	// Retrieve Server Key from Environment Variables for security
	// Usage: export FCM_SERVER_KEY="YOUR_FIREBASE_SERVER_KEY_HERE"
	fcmServerKey := os.Getenv("FCM_SERVER_KEY")
	if fcmServerKey == "" {
		log.Fatal("FCM_SERVER_KEY environment variable not set. Please set it.")
	}

	http.HandleFunc("/send-notification", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		var req NotificationRequest
		decoder := json.NewDecoder(r.Body)
		if err := decoder.Decode(&req); err != nil {
			http.Error(w, "Invalid request body", http.StatusBadRequest)
			return
		}

		if req.Token == "" {
			http.Error(w, "FCM Token is required", http.StatusBadRequest)
			return
		}

		// Prepare the payload for FCM Legacy Endpoint
		fcmPayload := map[string]interface{}{
			"to": req.Token,
			"notification": map[string]string{
				"title": req.Title,
				"body":  req.Body,
			},
			"data": req.Data,
		}

		payloadBytes, err := json.Marshal(fcmPayload)
		if err != nil {
			http.Error(w, "Failed to marshal FCM payload", http.StatusInternalServerError)
			return
		}

		// Construct HTTP Request to FCM API
		fcmURL := "https://fcm.googleapis.com/fcm/send"
		httpClient := &http.Client{}
		fcmReq, err := http.NewRequest("POST", fcmURL, bytes.NewBuffer(payloadBytes))
		if err != nil {
			http.Error(w, "Failed to create FCM request", http.StatusInternalServerError)
			return
		}

		// Set required headers
		fcmReq.Header.Set("Content-Type", "application/json")
		fcmReq.Header.Set("Authorization", "key="+fcmServerKey)

		// Send request to FCM
		resp, err := httpClient.Do(fcmReq)
		if err != nil {
			http.Error(w, "Failed to send request to FCM", http.StatusInternalServerError)
			return
		}
		defer resp.Body.Close()

		// Read response from FCM
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			log.Printf("Error reading FCM response: %v", err)
			http.Error(w, "Failed to read FCM response", http.StatusInternalServerError)
			return
		}

		log.Printf("FCM Response Status: %s", resp.Status)
		log.Printf("FCM Response Body: %s", string(body))

		if resp.StatusCode != http.StatusOK {
			http.Error(w, fmt.Sprintf("FCM returned error: %s", string(body)), resp.StatusCode)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Notification sent successfully!"))
	})

	port := ":8080"
	log.Printf("Go FCM Backend running on port %s", port)
	log.Fatal(http.ListenAndServe(port, nil))
}

⚠️ Important Note: Never hardcode your Firebase Server Key inside production source code. Always use environment variables or secret managers. For local testing, export the variable in your terminal before running: export FCM_SERVER_KEY="YOUR_SERVER_KEY_HERE".

3.3. Run the Go Backend

Open your terminal in the go-fcm-backend directory and execute:

Bash
# Ensure FCM_SERVER_KEY environment variable is exported
go run main.go

You should see output similar to:

Plaintext
2023/11/27 10:00:00 Go FCM Backend running on port :8080

4. Testing the Push Notification Pipeline

Now let's test our complete end-to-end setup!

  1. Run Flutter Application: Start your app on an emulator or physical device.

  2. Copy FCM Token: Once booted up, copy the token printed in the logs or rendered on the UI screen.

  3. Send Request via cURL or Postman: Make a POST request to your running Go server endpoint.

Bash
curl -X POST \
  http://localhost:8080/send-notification \
  -H 'Content-Type: application/json' \
  -d '{
    "token": "PASTE_YOUR_FCM_TOKEN_HERE",
    "title": "Hello from Go Backend!",
    "body": "This is our first push notification via Go!",
    "data": {
      "page": "home",
      "id": "123"
    }
  }'

Expected Results:

  • Foreground: If your app is active on screen, the onMessage log fires, updating the UI string.

  • Background / Terminated: If the app is minimized or closed, a system notification banner will appear in your device's notification tray.

  • Go Terminal: You will see a successful response payload logged from FCM.

Best Practices & Pro Tips

While the basic implementation works, keep these production-grade practices in mind:

  • Token Database Management: Store device FCM tokens mapped to User IDs in your backend database. Update database records on token refresh (onTokenRefresh), and prune invalid or unregistered tokens (NotRegistered errors).

  • notification vs data Payloads:

    • notification: Triggers system UI banners directly when the app is in background/terminated states.

    • data: Sends custom key-value pairs directly to application logic, useful for silent pushes, background data syncs, or custom navigation routing.

  • Android Notification Channels: For Android 8.0 (API 26) and above, push notifications must belong to a channel. Integrate packages like flutter_local_notifications for granular local management.

  • API Security: Secure your backend /send-notification endpoint with authentication/authorization layers so unauthorized third parties cannot trigger system notifications.

Conclusion

Congratulations! You have successfully built a push notification workflow using Flutter, Firebase Cloud Messaging, and Go. Your app is now equipped with instant communication capabilities to boost user retention and deliver timely updates.