Uninstall Tracking
Track app uninstalls to measure user retention and optimize re-engagement campaigns by integrating push notification services with the Singular SDK.
Important: Google deprecated GCM APIs in April 2018. Use Firebase Cloud Messaging (FCM) for all Android uninstall tracking implementations.
Android Uninstall Tracking
Prerequisites
Before implementing uninstall tracking in your Flutter app, configure your app in the Singular platform following the guide Setting Up Android Uninstall Tracking.
System Requirements
Uninstall tracking requires Firebase Cloud Messaging and specific device configurations.
FCM Requirements (source):
- Android Version: Devices must run Android 4.1 (API 16) or higher
- Google Play Services: Devices must have the Google Play Store app installed
- Emulator Support: Android 4.1+ emulators with Google APIs are supported
- Distribution: Apps can be distributed outside the Google Play Store while still supporting uninstall tracking
Note: Users on unsupported Android versions or devices without Google Play Services will not be tracked for uninstalls.
Implementation Steps
Step 1: Install Firebase Packages
Add Firebase dependencies to your pubspec.yaml file for
core functionality and messaging support.
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2
firebase_messaging: ^14.7.10
singular_flutter_sdk: ^1.8.0
After adding dependencies, run flutter pub get to install
the packages.
Step 2: Configure Firebase
Add Firebase configuration files to your Flutter project for Android.
- Register your Android app in your Firebase Console project
- Download
google-services.jsonand place it inandroid/app/ - Verify Firebase messaging dependencies are added to your project
For detailed setup instructions, see Add Firebase to your Flutter app.
Step 3: Initialize Firebase
Initialize Firebase before running your Flutter app to enable messaging services.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
runApp(MyApp());
}
Step 4: Request Notification Permission
Request notification permissions from the user (required for Android 13+) before retrieving the FCM token.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
Future<bool> requestNotificationPermission() async {
if (Platform.isAndroid) {
// Android 13+ requires explicit permission request
// Note: firebase_messaging handles this automatically via requestPermission
final FirebaseMessaging messaging = FirebaseMessaging.instance;
final NotificationSettings settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
return settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
}
// iOS permission handled separately
return true;
}
Step 5: Retrieve and Register FCM Token
Get the FCM device token and register it with Singular using
registerDeviceTokenForUninstall() after requesting permissions.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
if (Platform.isAndroid) {
initializeAndroidUninstallTracking();
}
}
Future<void> initializeAndroidUninstallTracking() async {
try {
// Request notification permission
final hasPermission = await requestNotificationPermission();
if (!hasPermission) {
print('Notification permission denied - uninstall tracking unavailable');
return;
}
// Get FCM token
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
// Register token with Singular for uninstall tracking
Singular.registerDeviceTokenForUninstall(token);
print('FCM token registered with Singular: $token');
} else {
print('No FCM token available');
}
} catch (error) {
print('Error setting up uninstall tracking: $error');
}
}
Future<bool> requestNotificationPermission() async {
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
return settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
Method Signature:
static void registerDeviceTokenForUninstall(String token)
For complete method documentation, see registerDeviceTokenForUninstall reference.
Step 6: Handle Token Refresh
Update the FCM token with Singular whenever it refreshes to maintain accurate uninstall tracking.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
if (Platform.isAndroid) {
setupTokenRefreshListener();
}
}
void setupTokenRefreshListener() {
// Listen for token refresh events
FirebaseMessaging.instance.onTokenRefresh.listen((String token) {
print('FCM token refreshed: $token');
// Update Singular with new token
Singular.registerDeviceTokenForUninstall(token);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
Best Practice: FCM tokens can refresh at any time
(app updates, device restore, etc.). Always subscribe to the
onTokenRefresh stream to keep Singular updated with
the latest token.
iOS Uninstall Tracking
Prerequisites
Configure your iOS app in the Singular platform following the guide Setting Up iOS Uninstall Tracking.
Uninstall tracking on iOS is based on Apple Push Notification service (APNs) technology. If your app doesn't support push notifications, see Apple's guide to Registering Your App with APNs.
Implementation Steps
Step 1: Configure iOS Project
Add Firebase configuration and enable push notification capabilities in your iOS project.
- Register your iOS app in your Firebase Console project
- Download
GoogleService-Info.plistand add it to the Xcode Runner folder - In Xcode project settings, enable Push Notifications capability
- Enable Background Modes and check Remote notifications
Step 2: Request iOS Notification Authorization
Request notification permissions from the user and retrieve the APNS device token.
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
Future<bool> requestIOSNotificationPermission() async {
if (!Platform.isIOS) {
return false;
}
try {
final FirebaseMessaging messaging = FirebaseMessaging.instance;
final NotificationSettings settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
final authorized =
settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
if (authorized) {
print('iOS notification authorization status: ${settings.authorizationStatus}');
return true;
}
return false;
} catch (error) {
print('Error requesting iOS notification permission: $error');
return false;
}
}
Step 3: Retrieve and Register APNS Token
Get the APNS device token and register it with Singular using
registerDeviceTokenForUninstall() after authorization is
granted.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
if (Platform.isIOS) {
initializeIOSUninstallTracking();
}
}
Future<void> initializeIOSUninstallTracking() async {
try {
// Request notification authorization
final hasPermission = await requestIOSNotificationPermission();
if (!hasPermission) {
print('Notification permission denied - uninstall tracking unavailable');
return;
}
// Get APNS token
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken != null) {
// Register token with Singular for uninstall tracking
Singular.registerDeviceTokenForUninstall(apnsToken);
print('APNS token registered with Singular: $apnsToken');
} else {
print('No APNS token available');
}
} catch (error) {
print('Error setting up iOS uninstall tracking: $error');
}
}
Future<bool> requestIOSNotificationPermission() async {
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
return settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
Token Format: The APNS token retrieved from
getAPNSToken() is already formatted as a hexadecimal
string, which is the correct format for Singular.
Step 4: Handle Token Refresh (iOS)
Update the APNS token with Singular if it changes during the app lifecycle.
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'dart:io';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
if (Platform.isIOS) {
setupIOSTokenRefreshListener();
}
}
void setupIOSTokenRefreshListener() {
// Listen for token refresh events
FirebaseMessaging.instance.onTokenRefresh.listen((String token) {
print('APNS token refreshed: $token');
// Update Singular with new token
Singular.registerDeviceTokenForUninstall(token);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
Complete Cross-Platform Implementation
Unified Uninstall Tracking Setup
Implement uninstall tracking for both Android and iOS platforms with proper error handling and token refresh logic.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'dart:io';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// Initialize Singular SDK
initializeSingularSDK();
// Setup uninstall tracking
initializeUninstallTracking();
// Setup token refresh listener
setupTokenRefreshListener();
}
void initializeSingularSDK() {
final config = SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
);
Singular.start(config);
}
Future<void> initializeUninstallTracking() async {
try {
if (Platform.isAndroid) {
await setupAndroidUninstallTracking();
} else if (Platform.isIOS) {
await setupIOSUninstallTracking();
}
} catch (error) {
print('Error initializing uninstall tracking: $error');
}
}
Future<void> setupAndroidUninstallTracking() async {
print('Setting up Android uninstall tracking');
// Request notification permission
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus != AuthorizationStatus.authorized &&
settings.authorizationStatus != AuthorizationStatus.provisional) {
print('Android notification permission denied');
return;
}
// Get and register FCM token
final token = await messaging.getToken();
if (token != null) {
Singular.registerDeviceTokenForUninstall(token);
print('Android FCM token registered: $token');
} else {
print('Failed to retrieve Android FCM token');
}
}
Future<void> setupIOSUninstallTracking() async {
print('Setting up iOS uninstall tracking');
// Request iOS notification authorization
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
final authorized =
settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional;
if (!authorized) {
print('iOS notification permission denied');
return;
}
// Get and register APNS token
final apnsToken = await messaging.getAPNSToken();
if (apnsToken != null) {
Singular.registerDeviceTokenForUninstall(apnsToken);
print('iOS APNS token registered: $apnsToken');
} else {
print('Failed to retrieve iOS APNS token');
}
}
void setupTokenRefreshListener() {
// Listen for token refresh events (works for both platforms)
FirebaseMessaging.instance.onTokenRefresh.listen((String token) {
print('${Platform.operatingSystem.toUpperCase()} token refreshed: $token');
Singular.registerDeviceTokenForUninstall(token);
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Uninstall Tracking Demo'),
),
body: Center(
child: Text('Uninstall tracking initialized'),
),
),
);
}
}
Platform-Specific Notes
- iOS: Ensure your app has the necessary push notification entitlements and APNs is properly configured in your Apple Developer account
-
Android: Verify that FCM is set up in your Firebase console and the
google-services.jsonfile is included in your project atandroid/app/
Verification and Troubleshooting
Verify Implementation
Confirm uninstall tracking is working correctly before deploying to production.
- Check Logs: Verify token registration appears in your console logs with the correct format
- Test Token Generation: Ensure tokens are generated on first app launch after granting permissions
- Monitor Dashboard: Check Singular dashboard for uninstall tracking data after 24-48 hours
- Test Token Refresh: Clear app data and verify token updates correctly when app relaunches
Common Issues
-
Token Not Generated: Verify Firebase dependencies are correctly installed and Firebase is configured in your Flutter project. Run
flutter pub getafter adding dependencies - Permission Denied: Check that users have granted notification permissions. For Android 13+, explicit permission request is required. For iOS, users must authorize notifications
-
Token Not Updating: Ensure you've subscribed to the
onTokenRefreshstream for both platforms. The listener should be set up in your app's initialization - Missing Data: Confirm devices meet platform requirements (Android 4.1+ with Google Play Services, iOS with APNs support). Devices without these services cannot be tracked
- Configuration Error: Verify uninstall tracking is enabled in Singular platform settings for your app. Follow the platform-specific setup guides linked in Prerequisites
-
Firebase Setup: For Android, ensure
google-services.jsonis inandroid/app/. For iOS, ensureGoogleService-Info.plistis added to Xcode project Runner folder -
SDK Initialization: Confirm the Singular SDK is initialized before calling
registerDeviceTokenForUninstall(). The token registration should happen after SDK startup -
Platform Detection: Use
Platform.isAndroidandPlatform.isIOSfromdart:ioto ensure platform-specific code runs on the correct platform
Additional Resources: For detailed troubleshooting, see the Android Uninstall Tracking Setup Guide, iOS Uninstall Tracking Setup Guide, and Flutter Firebase Setup Documentation.
Best Practices
Token Management
- Early Registration: Request permissions and register tokens as early as possible in the app lifecycle, ideally during first app launch
- Error Handling: Implement robust error handling around token retrieval and registration to gracefully handle failures
- Token Refresh: Always implement token refresh listeners to keep Singular updated when tokens change
- User Experience: Request notification permissions in context, explaining why the app needs them to improve permission grant rates
Testing Strategy
- Development Testing: Test on both physical Android and iOS devices, as emulators may have limited push notification support
- Permission Flows: Test scenarios where users deny permissions and verify the app handles this gracefully
- Token Persistence: Verify tokens persist across app restarts and are correctly updated on refresh
- Logging: Enable verbose logging during development to track token generation and registration