Flutter Integration
Add AlgorithmX to your Flutter app step by step: install the plugin, start it, identify customers, send events, connect push notifications, and open the right screen from campaigns.
This guide adds AlgorithmX to your Flutter app in 8 steps. You keep your app's login, push setup, and navigation. You only add a few lines in places your app already has.
The plugin algorithmx_flutter already contains the Android and iOS SDKs, so it is the only thing you install. The work has two parts:
- A few native lines in Android and iOS. They start the SDK and pass push notifications to it. This must be native, because a notification can start your app before Dart runs.
- Everything else in Dart: customers, events, and navigation.
In the examples, names such as appRouter are your own code, not part of the plugin.
Before you start
Check that you have these:
- Flutter 3.19 or later.
- Android:
minSdk24 or higher, and Firebase Cloud Messaging set up. - iOS: iOS 15 or later, and push notifications set up.
- The API base URL from AlgorithmX, for example
https://api.example.com. - The list of customer ID, events, deep links, and custom actions you agreed on with AlgorithmX. See the overview.
AlgorithmX needs the same push details as for native apps. Follow "Before you start" in the Android guide (Firebase project) and the iOS guide (Apple Developer account and APNs key).
The steps
| Step | What you do | Where |
|---|---|---|
| 1 | Install the plugin | pubspec.yaml |
| 2 | Start the SDK | Android Application, iOS AppDelegate, Dart main() |
| 3 | Tell AlgorithmX who the customer is | Login, app start, logout |
| 4 | Send events | Where actions happen, such as checkout |
| 5 | Connect push notifications | Your Firebase code and iOS AppDelegate |
| 6 | Open the right screen | Dart main() |
| 7 | Check in-app campaigns | No code |
| 8 | Test | Real Android and iOS phones |
Step 1: Install the plugin
From your app's folder, run:
flutter pub add algorithmx_flutter
This adds the plugin to your pubspec.yaml:
dependencies:
algorithmx_flutter: ^1.0.0
The plugin does not add Firebase; your app keeps its own push setup. To update later, raise the version and run flutter pub get. Release notes are on pub.dev.
Check: flutter pub get finishes, and the app still builds for Android and iOS.
Step 2: Start the SDK
You start the SDK in three places: Android, iOS, and Dart. Use the same URL in all three.
2a. 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 have no Application class by default. If yours has none, create this file next to MainActivity, and register it in android/app/src/main/AndroidManifest.xml:
<application
android:name=".MyApplication"
...>
2b. iOS
In application(_:didFinishLaunchingWithOptions:) of your AppDelegate, before return super...:
import algorithmx_flutter
AlgorithmXFlutterPlugin.initializeForBackground(apiBaseUrl: "https://api.example.com")
UNUserNotificationCenter.current().delegate = self // Keep yours if you already set it.
application.registerForRemoteNotifications() // On every launch.
For delivery reports (step 5, optional part), use initializeForBackground(apiBaseUrl: "https://api.example.com", appGroup: "group.com.example.shop") instead.
2c. Dart
In main(), set your handlers from step 6 first, then connect Dart to the SDK, before runApp:
import 'package:algorithmx_flutter/algorithmx_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sdk = AlgorithmX.instance;
// 1. Set your handlers from step 6 here.
// 2. Connect Dart to the SDK that native code already started.
await sdk.initialize(apiBaseUrl: 'https://api.example.com');
runApp(const MyApp());
}
initializefails if its URL is different from the native one.- Handlers before
initialize: taps that happen before Dart is ready, such as the tap that started the app, are kept and delivered wheninitializeruns. If your handlers are not set yet, they miss them. - To see each SDK request while testing, call
AlgorithmX.setLoggingEnabled(true)(Android) orAlgorithmX.shared.setLoggingEnabled(true)(iOS) in the native code.
Check: the app starts on both platforms without an error from initialize.
Step 3: Tell AlgorithmX who the customer is
Until you do this, the SDK uses an anonymous ID for the phone.
At login, and when your app starts with a customer already logged in, call identifyUser with your customer ID:
await AlgorithmX.instance.identifyUser(
'customer_123',
attributes: {'first_name': 'Amina', 'language': 'ar', 'loyalty_tier': 'gold'},
);
Do both places. The second one covers customers who are already logged in when they update to your first app version with AlgorithmX.
At logout, clear waiting campaigns first, so the next person does not see them:
await AlgorithmX.instance.clearWebViewQueue();
await AlgorithmX.instance.resetIdentity();
When one account replaces another without a logout:
await AlgorithmX.instance.clearWebViewQueue();
await AlgorithmX.instance.identifyUser('customer_456', attributes: {'language': 'en'});
The SDK remembers the customer after the app restarts. This does not replace your own login. It only tells AlgorithmX who is using the app.
Check: after login, the customer appears in AlgorithmX with the ID you sent.
Step 4: Send events
Send an event right after an action succeeds, for example after an order is confirmed. Put it 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},
],
});
Rules for events:
- Values: use text, numbers, true/false, lists, and maps. Send dates as a timestamp or an ISO 8601 string. Do not send your own Dart objects.
- Names: use simple names with letters and underscores, such as
add_to_cart. - Offline: an event sent without internet is lost. It is not sent again later. Keep your own backend as the record of orders and payments.
Check: the event appears for the customer in AlgorithmX.
Step 5: Connect push notifications
You keep your push setup. On each platform you send the push token to AlgorithmX and pass AlgorithmX notifications to the SDK.
5a. Android
Choose the case that matches your app.
Case 1: your app uses the firebase_messaging plugin (most Flutter apps). Pass AlgorithmX messages on from your existing Dart handlers:
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:algorithmx_flutter/algorithmx_flutter.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);
// Your existing background code.
}
void wireFirebaseMessaging() {
FirebaseMessaging.onBackgroundMessage(firebaseBackgroundHandler);
FirebaseMessaging.onMessage.listen((message) {
_forwardToAlgorithmX(message);
// Your existing foreground code.
});
// Android only. On iOS, AlgorithmX needs the Apple token from step 5b.
if (Platform.isAndroid) {
FirebaseMessaging.instance.getToken().then((token) {
if (token != null) AlgorithmX.instance.registerDeviceToken(token);
});
FirebaseMessaging.instance.onTokenRefresh.listen(AlgorithmX.instance.registerDeviceToken);
}
}
- The background handler does not need
initialize; step 2a already started the SDK. - Do not add your own
FirebaseMessagingServicenext tofirebase_messaging. Android gives each message to only one of them.
Case 2: your app has its own native FirebaseMessagingService. Add the AlgorithmX check at the top, exactly as in step 5 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
}
// Your existing code for other messages.
}
override fun onNewToken(token: String) {
super.onNewToken(token)
AlgorithmX.registerDeviceToken(token)
}
Then also send the token on every app start, in Application.onCreate:
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
AlgorithmX.registerDeviceToken(token)
}
In both cases, keep your own notification permission prompt; the SDK never asks for permission.
5b. iOS
In a Flutter app, your AppDelegate extends FlutterAppDelegate. Add these methods (or add the AlgorithmX lines if you already have them). Everything else still goes to super.
// Send the Apple push token.
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)
}
// Hidden notifications. In-app campaigns arrive here.
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)
}
// A notification arrives while the app is open.
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)
}
// The customer taps a notification or a button.
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 iOS SDK inside the plugin.import algorithmx_fluttergives you access to it.- Send the Apple (APNs) token, even if your app also uses Firebase on iOS.
- Turn on Background Modes, Remote notifications in Xcode. In-app campaigns need it.
- To install on an iPhone without Xcode attached, use a release or profile build. Flutter debug builds only run with the debugger.
- Images and buttons (optional): follow step 9 of the iOS guide. Instead of adding the SDK to the extension, copy the plugin's
EngageNotificationService.swiftfile (in itsiosfolder, underNativeSDK/Notifications) into the extension target. Copy it again when you update the plugin.
Check: ask your campaign team to send a test notification to each platform. It appears once.
Step 6: Open the right screen when a customer taps
When a customer taps an AlgorithmX notification, the plugin calls your Dart handlers. They open your screens and return true when they handled the tap.
| Handler | When it runs | What to do |
|---|---|---|
onDeepLink | The campaign opens a screen with a link. | Open the link with your router. |
onCustomAction | The campaign runs a custom action. | Open the screen for that action. |
onActionButtonClicked | The customer taps a notification button. | Handle the button and return true. |
onNotificationClick | Every tap, before the campaign's action. | Optional. Leave it unset so the SDK runs the campaign's action. |
"Open a web page" and "open an in-app campaign" need no handler; the SDK does them.
Set the handlers in main(), before initialize (step 2c):
sdk.onDeepLink = (url) => appRouter.openUrl(url);
sdk.onCustomAction = (action, data) {
final details = data['parsedActionData'];
if (action == 'view_product' && details is Map && details['product_id'] is String) {
return appRouter.openProduct(details['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 fast. The SDK waits up to 5 seconds for each answer. Start the navigation and return; do not wait for network calls.
- Wait for your navigator. When a tap starts the app, the handlers run before your first frame. Keep the destination and open it when your navigator is ready, for example in a post-frame callback.
- Handle every button you offer. If the button handler returns
false, iOS runs the notification's main action instead, and Android does nothing. Returntruefor each button your campaigns use. - Match buttons on their action text, not their title, because the title can be translated.
- You do not need new links or screens. Give your campaign team the links and action names your released app supports, for example
myapp://open/product?id=SKU-123. - Keep your normal login and permission checks before sensitive screens: a campaign only suggests where to go.
Check: send a test notification for each link, action, and button your app supports. The right screen opens once.
Step 7: Check in-app campaigns
In-app campaigns need no code. After steps 2 to 5:
- AlgorithmX sends a hidden message to the phone, and your push code passes it to the SDK.
- The SDK saves the campaign.
- The next time the customer opens 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.
Check these in your app:
- Logout: clear waiting campaigns, as in step 3.
- Your own dialogs at start: a campaign can appear when the app opens, on top of a dialog your app shows at the same time. Try them together.
- Close button: iPhones have no Back button, so every campaign template needs a close button. Tell your campaign team.
- Notification permission is not needed for in-app campaigns.
- Delivery is not guaranteed if the customer force-stops the app (Android) or swipes it away (iOS). Messages then arrive only after the app is opened again.
To see impressions, clicks, and closes while testing, set onCampaignInteraction or listen to AlgorithmX.instance.campaignInteractions.
Step 8: Test before you release
Use real Android and iOS phones and a test customer. Test release builds, not only debug builds. On iOS, repeat the main checks with a TestFlight build, because store builds use a different Apple push server.
- Update while logged in: install your current store version, log in, then update to the new build. The customer is recognized without logging in again.
- Login, restart, logout, second account: events use the right customer each time. No campaign from the first account appears for the second.
- Your existing notifications and other push plugins still work exactly as before.
- AlgorithmX notification with the app open, in the background, and closed: a tap opens the right screen once, also when the tap starts the app.
- Each deep link, custom action, and button opens the right screen.
- In-app campaign: it appears the next time the app opens and closes with its close button.
- Notifications turned off: in-app campaigns still arrive.
- Events appear for the customer in AlgorithmX.
Quick reference
| Call | What it does |
|---|---|
AlgorithmXFlutterPlugin.initializeForBackground(this, apiBaseUrl) (Android) | Starts the SDK in Application.onCreate. |
AlgorithmXFlutterPlugin.initializeForBackground(apiBaseUrl:appGroup:) (iOS) | Starts the SDK in the AppDelegate. The App Group is optional. |
AlgorithmX.instance.initialize(apiBaseUrl:) | Connects Dart to the SDK. Call it after setting handlers, with the same URL. |
identifyUser(userId, attributes:) | Sets the customer. Call at login and app start. |
resetIdentity() | Goes back to an anonymous customer. Call at logout. |
trackEvent(name, properties:) | Sends an event. |
registerDeviceToken(token) | Sends a push token from Dart (Android). |
handleFcmMessage(data, notificationTitle:, notificationBody:) | Android: gives an AlgorithmX message received in Dart to the SDK. |
onDeepLink, onCustomAction, onActionButtonClicked, onNotificationClick, onCampaignInteraction | Your handlers. Set them before initialize. |
clearWebViewQueue() | Removes waiting in-app campaigns. Call at logout. |
getDeviceFingerprint(), getWebViewQueueSize() | Show the current customer ID and waiting campaigns, for testing. |
The native calls in step 5 are the same as in the Android and iOS guides.
Something not working?
| Problem | What to check |
|---|---|
initialize fails | Dart and native code use different URLs. Use the same URL in all three places of step 2. |
| No events in AlgorithmX | The native code in step 2 runs with the right HTTPS URL, and the phone is online. |
| No AlgorithmX notifications | See "Something not working?" in the Android or iOS guide: token, sending permission, and passing messages to the SDK. |
Android messages stop reaching firebase_messaging | A second FirebaseMessagingService was added. Keep only one. |
| A tap does not reach Dart | Handlers are set before initialize, and the native code in step 2 starts the SDK. |
| A tap that starts 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 | Step 5 is in place, iOS Background Modes is on, the app was not force-closed, and you opened it again after the campaign was sent. |
| A previous account's campaign appears | Call clearWebViewQueue() at logout and when switching accounts. |
Next steps
Create your first campaigns with In-App Push Campaigns and In-App View Campaigns. Go back to the overview for other platforms.
