Flutter Integration
This guide shows how to add the AlgorithmX SDK to a Flutter app that is already in production, with its own login, analytics, push notifications, and navigation. You do not rebuild any of these. You add a few calls in the places where your app already handles startup, sign-in, business events, notifications, and navigation. The SDK does the rest: it shows campaign notifications and in-app campaigns, tracks interactions, and applies frequency rules.
The plugin algorithmx_flutter contains the native Android and iOS SDKs, so there is nothing else to install. The work splits in two:
- Native code (a few lines): start the SDK in your Android
Applicationand iOSAppDelegate, and pass push tokens and notifications to it. This has to be native because a push can start your app before Dart runs. - Dart: identify customers, send events, and handle campaign navigation.
The plugin supports Android and iOS. Names such as appRouter in the examples stand for your own code; they are not SDK APIs.
How the SDK fits into your app
| Where in your existing app | What you add | Section |
|---|---|---|
| pubspec.yaml | The plugin. | 2 |
| Android Application and iOS AppDelegate | One line to start the SDK. | 3 |
| Dart main() | Set the SDK handlers and bind Dart to the SDK. | 3 |
| Login, restored session, logout | Tell the SDK who the customer is. | 4 |
| Business events | Forward the agreed events. | 5 |
| Your push setup (Firebase on Android, APNs on iOS) | Pass tokens and AlgorithmX notifications to the SDK. | 6 |
| Your navigation | Open your screens from campaign links, custom actions, and buttons. | 7 |
In-app campaigns need no widget or screen code from you: once these connections are in place, the SDK shows campaigns by itself (section 8).
1. Before you start
What AlgorithmX gives you
- The
algorithmx_flutterplugin with its version. - The API base URL for each environment, such as staging and production. All URLs use HTTPS.
What your team gives AlgorithmX
AlgorithmX sends notifications through Firebase on Android and through Apple's push service (APNs) on iOS, so it needs the same details as a native app:
| Platform | What to send | Details |
|---|---|---|
| Android | Application ID, your Firebase project, and access to send messages through it. | Android guide, section 1 |
| iOS | Bundle ID, Team ID, APNs authentication key and Key ID, and which build type each workspace serves. | iOS guide, section 1, including the Apple Developer account checklist |
Also agree with the AlgorithmX team on the customer ID you will use (section 4), the events you will send (section 5), and the deep links and custom actions campaigns may use (section 7).
2. Add the plugin
Add the plugin to pubspec.yaml from the location AlgorithmX gives you. The path below is an example.
dependencies:
algorithmx_flutter:
path: ../algorithmx_flutterThen check two minimum versions:
- Android:
minSdk24 or higher inandroid/app/build.gradle. - iOS: iOS 15.0 or higher, for example
platform :ios, '15.0'inios/Podfile.
The plugin needs Flutter 3.19 or later and was verified with Flutter 3.47. It adds no Firebase dependency; your app keeps its own push setup.
3. Initialize the SDK
In native code
Start the SDK in your existing native entry points. A push can start your app in the background before Flutter runs, and this is the code that runs then.
Android, in your Application class:
import algorithmx.engage.core.AlgorithmX
import algorithmx.engage.flutter.AlgorithmXFlutterPlugin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AlgorithmXFlutterPlugin.initializeForBackground(this, "https://api.example.com")
AlgorithmX.setSmallIcon(R.drawable.ic_stat_notification) // Optional: your notification icon.
// Keep your existing startup code.
}
}Flutter apps do not have their own Application class by default. If yours does not, create one like this and register it with android:name=".MyApplication" on the <application> element of AndroidManifest.xml.
iOS, in application(_:didFinishLaunchingWithOptions:) of your AppDelegate:
import algorithmx_flutter
// Inside application(_:didFinishLaunchingWithOptions:), before returning super:
AlgorithmXFlutterPlugin.initializeForBackground(apiBaseUrl: "https://api.example.com", appGroup: "group.com.example.shop")
UNUserNotificationCenter.current().delegate = self // Keep your existing delegate assignment.
application.registerForRemoteNotifications() // Every launch, independent of permission.The App Group is optional; it is only needed for iOS delivery reporting (section 6). Without it, call AlgorithmXFlutterPlugin.initializeForBackground(apiBaseUrl:).
In Dart
In main(), set your handlers (section 7) first, then bind Dart to the SDK with the same URL, before runApp:
import 'package:algorithmx_flutter/algorithmx_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sdk = AlgorithmX.instance;
// Set your handlers from section 7 here first.
// Then bind Dart to the SDK that native code already started.
await sdk.initialize(apiBaseUrl: 'https://api.example.com');
runApp(const MyApp());
}initializeconnects Dart to the SDK that native code already started. It fails if the URL differs from the native one.- Set handlers before
initialize. The SDK holds notification taps and other callbacks that arrive before Dart is ready, such as the tap that launched the app, and delivers them wheninitializeruns. - To see each SDK request while testing, call
AlgorithmX.setLoggingEnabled(true)(Android) orAlgorithmX.shared.setLoggingEnabled(true)(iOS) in the native startup code.
4. Identify your customers
Until you identify a customer, the SDK uses an anonymous device ID. Call identifyUser with the same customer ID your backend uses, in two places: after a successful login, and when your app restores a saved session at startup. The second one matters for customers who are already signed in when they update to the first release with AlgorithmX.
// In the existing restored-session or successful-login code.
await AlgorithmX.instance.identifyUser(
'customer_123',
attributes: {'first_name': 'Amina', 'language': 'ar', 'loyalty_tier': 'gold'},
);The SDK remembers the customer across app restarts and sends their previous anonymous ID, so AlgorithmX can link earlier activity to the profile. At logout, and when one account replaces another, clear waiting campaigns so the next person does not see them:
// Before completing logout:
await AlgorithmX.instance.clearWebViewQueue();
await AlgorithmX.instance.resetIdentity();
// Before replacing one signed-in account with another:
await AlgorithmX.instance.clearWebViewQueue();
await AlgorithmX.instance.identifyUser('customer_456', attributes: {'language': 'en'});resetIdentity() returns to the anonymous device ID. AlgorithmX identity follows your login; it does not replace your authentication.
5. Track business events
Forward the agreed events from the places where your app already knows an action succeeded, for example your order-confirmation handler, next to your existing analytics calls.
await AlgorithmX.instance.trackEvent('product_view', properties: {'product_id': 'SKU-123'});
await AlgorithmX.instance.trackEvent('add_to_cart', properties: {'product_id': 'SKU-123', 'quantity': 2});
await AlgorithmX.instance.trackEvent('purchase', properties: {
'order_id': 'ORDER-1001',
'total': 149.90,
'currency': 'SAR',
'items': [
{'product_id': 'SKU-123', 'quantity': 2},
],
});- Use plain values: strings, numbers, booleans, lists, and maps. Send dates as timestamps or ISO 8601 strings, and do not pass your own Dart objects.
- Use simple event names such as
add_to_cart; the name becomes part of the request URL. - Events are not retried. An event sent while the device is offline is lost, so keep your own backend as the source of truth for orders and payments.
6. Connect your existing push notifications
Your app keeps its current push setup. On each platform you pass the device token to AlgorithmX and pass AlgorithmX notifications to the SDK.
Android
If your app uses the firebase_messaging plugin, forward AlgorithmX messages from your existing Dart handlers. The native initialization in section 3 is what makes this work when the app starts in the background, so the background handler does not call initialize.
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
bool _isAlgorithmX(RemoteMessage message) =>
message.data['engage_action'] == 'algo_show_notification' ||
message.data['engage_action'] == 'algo_trigger_webview';
Future<void> _forwardToAlgorithmX(RemoteMessage message) async {
if (!_isAlgorithmX(message)) return;
await AlgorithmX.instance.handleFcmMessage(
message.data.map((key, value) => MapEntry(key, value.toString())),
notificationTitle: message.notification?.title,
notificationBody: message.notification?.body,
);
}
@pragma('vm:entry-point')
Future<void> firebaseBackgroundHandler(RemoteMessage message) async {
await _forwardToAlgorithmX(message);
// Keep your existing background handling here.
}
void wireFirebaseMessaging() {
FirebaseMessaging.onBackgroundMessage(firebaseBackgroundHandler);
FirebaseMessaging.onMessage.listen((message) {
_forwardToAlgorithmX(message);
// Keep your existing foreground handling here.
});
// Android only: on iOS, AlgorithmX needs the APNs token, registered natively.
if (Platform.isAndroid) {
FirebaseMessaging.instance.getToken().then((token) {
if (token != null) AlgorithmX.instance.registerDeviceToken(token);
});
FirebaseMessaging.instance.onTokenRefresh.listen(AlgorithmX.instance.registerDeviceToken);
}
}Do not add your own FirebaseMessagingService next to the one from firebase_messaging: Android delivers each message to only one of them.
If your app has its own native FirebaseMessagingService, add the AlgorithmX branch there, exactly as in section 6 of the Android guide:
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
if (AlgorithmX.isAlgorithmXPush(message.data)) {
AlgorithmX.handleFcmMessage(
applicationContext, message.data,
message.notification?.title, message.notification?.body
)
return
}
// Keep your current message handling here.
}
override fun onNewToken(token: String) {
super.onNewToken(token)
AlgorithmX.registerDeviceToken(token)
}In that case, also register the current token on every launch in your Application.onCreate:
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
AlgorithmX.registerDeviceToken(token)
}In both cases, keep your existing notification permission prompt; the SDK never asks for permission. The plugin already forwards new Intents and tracks the foreground for you.
iOS
iOS notifications go through your AppDelegate, which in a Flutter app extends FlutterAppDelegate. Add the AlgorithmX branches to these methods, overriding the ones you do not have yet, and pass everything else to super as your app does today:
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let apnsToken = deviceToken.map { String(format: "%02x", $0) }.joined()
AlgorithmX.shared.registerDeviceToken(apnsToken)
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
override func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotification(userInfo: userInfo) { completionHandler(.newData) }
return
}
super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotification(userInfo: userInfo) { completionHandler([.banner, .list, .sound]) }
return
}
super.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler)
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotificationResponse(
actionIdentifier: response.actionIdentifier,
userInfo: userInfo,
completionHandler: completionHandler
)
return
}
super.userNotificationCenter(center, didReceive: response, withCompletionHandler: completionHandler)
}AlgorithmXhere is the native iOS SDK class inside the plugin, available throughimport algorithmx_flutter.- Send the APNs token, even if your app also uses Firebase on iOS.
- Enable Background Modes, Remote notifications in Xcode; in-app campaigns need it.
- Standalone iOS installs need a release or profile build; Flutter debug builds only run with the debugger attached.
- Images and buttons (optional): add the call from section 10 of the iOS guide to your notification service extension. Copy the plugin's
EngageNotificationService.swiftfile (in itsiosfolder, underNativeSDK/Notifications) into the extension target instead of linking the plugin, and copy it again when you update the plugin.
7. Handle navigation in Dart
When a customer taps an AlgorithmX notification, the SDK calls your Dart handlers. They open your screens and return true when they handled the action:
| Handler | When | What it does |
|---|---|---|
onDeepLink | The campaign action is "open screen". | Open the link with your router. The SDK does not navigate. |
onCustomAction | The campaign action is a custom action. | Run the named action with its parameters. |
onActionButtonClicked | The customer taps a notification button. | Handle the button and return true. |
onNotificationClick | Every tap on the notification itself, before the campaign's action. | Optional. Return true only if you handled the tap yourself; return false, or leave it unset, to let the SDK run the campaign's action. |
"Open web page" and "open in-app campaign" actions are handled by the SDK and need no handler.
// In main(), before AlgorithmX.instance.initialize.
sdk.onDeepLink = (url) => appRouter.openUrl(url);
sdk.onCustomAction = (action, data) {
final params = data['parsedActionData'];
if (action == 'view_product' && params is Map && params['product_id'] is String) {
return appRouter.openProduct(params['product_id'] as String);
}
if (action == 'view_cart') return appRouter.openCart();
return false;
};
sdk.onActionButtonClicked = (event) {
if (event.actionText == 'view_cart') return appRouter.openCart();
return event.actionText == 'dismiss';
};- Answer quickly. The SDK waits up to 5 seconds for each answer. Start the navigation and return; do not wait for network calls first.
- Your router must wait for your navigation to be ready. On a tap that launches the app, the handlers run before your first frame. Keep the destination and open it once the navigator exists, for example in a post-frame callback.
- Handle every button you offer. When your button handler returns false, iOS runs the notification's main action instead, while Android does nothing more. Return true for each button your campaigns use.
- Reuse your existing links. Give the campaign team the list of links your app supports, with their required parameters, for example
myapp://open/product?id=SKU-123. You do not need new URL schemes or screens. - Agree on action names and parameter types with the campaign team, and only offer campaign actions that your released app version implements. Match buttons on their action text, not their translated title.
- A campaign parameter is only a navigation hint: keep your normal login and permission checks before sensitive actions.
8. In-app campaigns
In-app campaigns need no extra code. With sections 3 to 6 in place:
- AlgorithmX sends a background message to the device, and your push code passes it to the SDK.
- The SDK saves the campaign.
- The next time the customer opens or returns to the app, the SDK shows the campaign over the current screen, at most one per visit. It closes with the campaign's close button, or with Back on Android.
Points to check in your app:
- Logout and account switch: clear waiting campaigns as shown in section 4.
- Your own dialogs at launch: a campaign can appear at the moment the app opens, on top of whatever is on screen. Check the combination with any dialog your app shows at startup.
- Close button: iOS has no Back button, so every campaign template must include a close button. Remind your campaign team.
- Notification permission is not needed for in-app campaigns.
- Delivery is best effort when the customer force-stops the app (Android) or force-closes it from the app switcher (iOS): messages arrive only after the app is opened again.
To follow campaign activity while testing, set onCampaignInteraction or listen to AlgorithmX.instance.campaignInteractions; they report each impression, click, and close.
9. Test and ship
Test on real Android and iOS devices with a test customer, using release builds as well as debug builds. On iOS, repeat the main checks with a TestFlight build, because store builds use a different push environment.
| Check | Expected result |
|---|---|
| Update from your current store version while signed in | The customer is identified without logging in again, and the token is registered. |
| Login, restart, logout, second account | Events use the right customer each time, and no campaign from the first account appears for the second. |
| Your existing notifications and other push plugins | Other notifications behave exactly as before. |
| AlgorithmX notification with the app open, in the background, and closed | A tap opens the right screen once, including when the tap launches the app. |
| Each deep link, custom action, and button your app supports | The right screen opens; unknown links and actions follow your app's normal fallback. |
| In-app campaign | It appears the next time the app becomes active, over the current screen, and closes with its close button. |
| Notification permission denied | In-app campaigns still arrive; visible notifications do not. |
| Business events | The agreed events appear for the customer in AlgorithmX. |
API reference
| Call | Use |
|---|---|
AlgorithmXFlutterPlugin.initializeForBackground(this, apiBaseUrl) (Android) | Start the SDK in Application.onCreate. |
AlgorithmXFlutterPlugin.initializeForBackground(apiBaseUrl:appGroup:) (iOS) | Start the SDK in the AppDelegate. The App Group is optional. |
AlgorithmX.instance.initialize(apiBaseUrl:) | Bind Dart to the SDK after setting handlers; use the same URL as native code. |
identifyUser(userId, attributes:) | Identify the customer after login and when a session is restored. |
resetIdentity() | Return to the anonymous device ID at logout. |
trackEvent(name, properties:) | Send a business event. |
registerDeviceToken(token) | Pass a token from Dart, when your token handling lives there. |
handleFcmMessage(data, notificationTitle:, notificationBody:) | Android: forward an AlgorithmX message received in Dart. |
onDeepLink, onCustomAction, onActionButtonClicked, onNotificationClick, onCampaignInteraction | Your handlers. Set them before initialize. |
clearWebViewQueue() | Remove waiting campaigns at logout or account switch. |
getDeviceFingerprint(), getWebViewQueueSize() | Inspect the current customer ID and waiting campaigns while testing. |
The native calls in section 6 (AlgorithmX.handleFcmMessage, AlgorithmX.shared.handleNotification, and so on) are the same as in the Android and iOS guides.
Troubleshooting
| Symptom | What to check |
|---|---|
| initialize fails with a different URL | Dart and native code use different URLs. Use the same value in both. |
| No events in AlgorithmX | Native initialization runs at startup with the right HTTPS URL, and the device is online. |
| No AlgorithmX notifications arrive | See the troubleshooting section of the Android or iOS guide: token registration, sending access, and forwarding. |
| Android messages stop reaching firebase_messaging | A second FirebaseMessagingService was added; keep only one and forward from it. |
| Tap on a notification does not reach Dart | Handlers are set before initialize, and the native Application or AppDelegate starts the SDK. |
| Tap that launches the app opens nothing | Your router keeps the destination until the navigator is ready. |
| A button does nothing, or opens the main action on iOS | Your button handler has no case for that action text and returned false. |
| In-app campaign never appears | Push forwarding is in place, iOS Background Modes is on, the app was not force-closed, and you reopened the app after the campaign was sent. |
| A previous account's campaign appears | Call clearWebViewQueue before logout and account switch. |
For creating campaigns, continue with In-App Push Campaigns and In-App View Campaigns. Return to Integrations for the other platforms.
