Entwickler · API v1.0.0
Flutter-Integration
Dieser Leitfaden richtet sich an Android- und iOS-Apps, die mit Flutter entwickelt werden. Die allgemeine API-Referenz finden Sie in der Überblick.
Unterstützte Versionen
| Komponente | Minimum | Empfohlen |
|---|---|---|
| Flutter | 3.22 | aktuelles Stable |
| Dart | 3.4 | aktuell |
firebase_messaging |
15.x | aktuell |
| Android | 6.0 (API 23) | targetSdk 35+ |
| iOS | 13.0 | 15.0+ |
Wie funktioniert es?
NotiPilot 1.0.0 stellt Benachrichtigungen über den Expo Push Service zu. Ihre Flutter-App:
- ruft mit
firebase_messagingden nativen Push-Token ab (Android: FCM-Token, iOS: APNs-Token), - konvertiert diesen Token über den Expo-Token-Service in einen Expo Push Token,
- registriert den Expo Push Token mit
register-devicebei NotiPilot.
Ihre App muss nicht mit Expo entwickelt sein; Expo dient lediglich als Zustellungsinfrastruktur.
1. Vorbereitung (einmalig)
- Verbinden Sie das Firebase-Projekt mit Ihrer Flutter-App:
dart pub global activate flutterfire_cli flutterfire configure - Legen Sie auf expo.dev ein Projekt an und notieren Sie die Projekt-ID (UUID).
- Laden Sie die Zugangsdaten in das Expo-Projekt hoch (expo.dev → Projekt → Credentials):
- Android: Firebase → Projekteinstellungen → Dienstkonten → über Neuen privaten Schlüssel generieren heruntergeladenes JSON → FCM V1 service account key
- iOS: Im Apple Developer Portal erstellter APNs-Schlüssel (
.p8) → Push Key (Schritte im iOS-Leitfaden)
- Legen Sie Ihre App im NotiPilot-Dashboard an: Expo Project ID, Expo Access Token, Android Package, iOS Bundle ID.
2. Abhängigkeiten
flutter pub add firebase_core firebase_messaging http flutter_secure_storage uuid package_info_plus
iOS: Fügen Sie in Xcode unter ios/Runner.xcworkspace → Runner-Target → Signing & Capabilities die Capabilities Push Notifications und Background Modes → Remote notifications hinzu.
Android: In android/app/build.gradle muss minSdk 23 gesetzt sein. Die Benachrichtigungsberechtigung unter Android 13+ wird von firebase_messaging angefragt.
3. lib/notipilot.dart
import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:ui' show PlatformDispatcher;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:uuid/uuid.dart';
class NotiPilot {
NotiPilot._();
static final NotiPilot instance = NotiPilot._();
static const _baseUrl = 'https://app.notipilot.com/api/v1';
static const _appId = 'IHRE-EXPO-PROJEKT-ID'; // "Expo Project ID" aus dem Dashboard
static const _uidKey = 'notipilot_device_uid';
final _storage = const FlutterSecureStorage();
final _messaging = FirebaseMessaging.instance;
Future<String> deviceUid() async {
var uid = await _storage.read(key: _uidKey);
if (uid == null) {
uid = const Uuid().v4();
await _storage.write(key: _uidKey, value: uid);
}
return uid;
}
/// Beim App-Start aufrufen. Fragt die Berechtigung an, ruft den Token ab und registriert ihn bei NotiPilot.
Future<Map<String, dynamic>?> registerDevice({Map<String, Object?> extraAttributes = const {}}) async {
final settings = await _messaging.requestPermission(alert: true, badge: true, sound: true);
if (settings.authorizationStatus == AuthorizationStatus.denied) return null;
final uid = await deviceUid();
final expoToken = await _getExpoPushToken(uid);
if (expoToken == null) return null;
final info = await PackageInfo.fromPlatform();
final locale = PlatformDispatcher.instance.locale;
return _post('/register-device', {
'app_id': _appId,
'device_uid': uid,
'token': expoToken,
'platform': Platform.isIOS ? 'ios' : 'android',
'provider': 'expo',
'attributes': {
'locale': locale.languageCode, // "tr"
'country': locale.countryCode, // "TR"
'app_version': info.version,
...extraAttributes, // z. B. {'city': 'Istanbul'}
},
});
}
/// Aufrufen, wenn sich der Nutzer anmeldet.
Future<Map<String, dynamic>> identify(String externalId, {Map<String, Object?>? attributes}) async {
return _post('/identify-device', {
'app_id': _appId,
'device_uid': await deviceUid(),
'external_id': externalId,
if (attributes != null) 'attributes': attributes,
});
}
/// Hält NotiPilot bei Token-Erneuerung aktuell. Einmal beim App-Start aufrufen.
void listenForTokenRefresh() {
_messaging.onTokenRefresh.listen((_) => registerDevice());
}
Future<String?> _getExpoPushToken(String uid) async {
final isIOS = Platform.isIOS;
String? deviceToken;
if (isIOS) {
// Der APNs-Token steht kurz nach dem App-Start zur Verfügung
for (var i = 0; i < 5 && deviceToken == null; i++) {
deviceToken = await _messaging.getAPNSToken();
if (deviceToken == null) await Future.delayed(const Duration(seconds: 1));
}
} else {
deviceToken = await _messaging.getToken();
}
if (deviceToken == null) return null;
final info = await PackageInfo.fromPlatform();
final res = await http.post(
Uri.parse('https://exp.host/--/api/v2/push/getExpoPushToken'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'type': isIOS ? 'apns' : 'fcm',
'deviceId': uid.toLowerCase(),
'development': isIOS && kDebugMode, // iOS-Debug-Builds nutzen die APNs-Sandbox
'appId': info.packageName, // Android package / iOS bundle id
'deviceToken': deviceToken,
'projectId': _appId,
}),
);
if (res.statusCode != 200) {
debugPrint('[NotiPilot] Expo token exchange failed: ${res.statusCode} ${res.body}');
return null;
}
return (jsonDecode(res.body)['data'] as Map<String, dynamic>)['expoPushToken'] as String;
}
Future<Map<String, dynamic>> _post(String path, Map<String, Object?> body, [int attempt = 0]) async {
final res = await http.post(
Uri.parse('$_baseUrl$path'),
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: jsonEncode(body),
);
final json = res.body.isEmpty ? <String, dynamic>{} : jsonDecode(res.body) as Map<String, dynamic>;
if ((res.statusCode == 429 || res.statusCode >= 500) && attempt < 3) {
final wait = (json['retry_after'] as num?)?.toInt() ?? (1 << attempt);
await Future.delayed(Duration(seconds: wait));
return _post(path, body, attempt + 1);
}
if (res.statusCode >= 400) {
debugPrint('[NotiPilot] ${res.statusCode} ${json['error']} ${json['errors'] ?? json['message']}');
}
return json;
}
/// Gibt das vom Dashboard gesendete benutzerdefinierte Feld `data` zurück.
static Map<String, dynamic> customData(RemoteMessage message) {
final raw = message.data['body'];
if (raw is String) {
try {
return jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {}
}
return Map<String, dynamic>.from(message.data);
}
}
4. main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'notipilot.dart';
@pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
// iOS: Benachrichtigung auch anzeigen, wenn die App im Vordergrund ist
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
alert: true, badge: true, sound: true,
);
NotiPilot.instance.registerDevice(extraAttributes: {'city': 'Istanbul'});
NotiPilot.instance.listenForTokenRefresh();
// Beim Tippen auf die Benachrichtigung
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final data = NotiPilot.customData(message);
// z. B. data['screen'] == 'product' → mit dem Navigator weiterleiten
});
runApp(const MyApp());
}
Wenn sich der Nutzer anmeldet: await NotiPilot.instance.identify(user.id);
Vordergrund-Benachrichtigungen unter Android: Ist die App geöffnet, zeigt Android die Benachrichtigung nicht automatisch an. Wenn Benachrichtigungen auch im Vordergrund erscheinen sollen, erstellen Sie sie mit dem Paket
flutter_local_notificationsinFirebaseMessaging.onMessageüber einen Kanal mit der IDdefault. Verwenden Sie für den Titelmessage.notification?.title ?? message.data['title']und für den Textmessage.notification?.body ?? message.data['message'].
Checkliste
-
flutterfire configurewurde ausgeführt - FCM-V1-Schlüssel und APNs-Schlüssel (
.p8) in das Expo-Projekt hochgeladen - Expo Project ID im Dashboard =
_appId, Paketname / Bundle ID im Dashboard korrekt - Unter iOS sind die Capabilities Push Notifications + Remote notifications aktiviert
-
listenForTokenRefresh()wird beim Start aufgerufen -
countryund nach Möglichkeitcitywerden als Attributes gesendet
Häufige Probleme
| Symptom | Lösung |
|---|---|
getAPNSToken() liefert unter iOS null |
Verwenden Sie ein physisches Gerät und prüfen Sie die Capabilities sowie die Benachrichtigungsberechtigung. |
Token-Konvertierung liefert 4xx |
Prüfen Sie die Werte von projectId und appId (Paketname / Bundle ID). |
| Registrierung erfolgreich, aber keine Benachrichtigung | Im Expo-Projekt fehlen die Zugangsdaten für die betreffende Plattform. |
404 unknown_app |
app_id ist im Dashboard nicht registriert. |