AlgorithmX Docs Logo
DocsFlutter Integration

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 Application and iOS AppDelegate, 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 appWhat you addSection
pubspec.yamlThe plugin.2
Android Application and iOS AppDelegateOne line to start the SDK.3
Dart main()Set the SDK handlers and bind Dart to the SDK.3
Login, restored session, logoutTell the SDK who the customer is.4
Business eventsForward the agreed events.5
Your push setup (Firebase on Android, APNs on iOS)Pass tokens and AlgorithmX notifications to the SDK.6
Your navigationOpen 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_flutter plugin 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:

PlatformWhat to sendDetails
AndroidApplication ID, your Firebase project, and access to send messages through it.Android guide, section 1
iOSBundle 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_flutter

Then check two minimum versions:

  • Android: minSdk 24 or higher in android/app/build.gradle.
  • iOS: iOS 15.0 or higher, for example platform :ios, '15.0' in ios/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());
}
  • initialize connects 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 when initialize runs.
  • To see each SDK request while testing, call AlgorithmX.setLoggingEnabled(true) (Android) or AlgorithmX.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)
}
  • AlgorithmX here is the native iOS SDK class inside the plugin, available through import 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.swift file (in its ios folder, under NativeSDK/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:

HandlerWhenWhat it does
onDeepLinkThe campaign action is "open screen".Open the link with your router. The SDK does not navigate.
onCustomActionThe campaign action is a custom action.Run the named action with its parameters.
onActionButtonClickedThe customer taps a notification button.Handle the button and return true.
onNotificationClickEvery 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:

  1. AlgorithmX sends a background message to the device, and your push code passes it to the SDK.
  2. The SDK saves the campaign.
  3. 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.

CheckExpected result
Update from your current store version while signed inThe customer is identified without logging in again, and the token is registered.
Login, restart, logout, second accountEvents use the right customer each time, and no campaign from the first account appears for the second.
Your existing notifications and other push pluginsOther notifications behave exactly as before.
AlgorithmX notification with the app open, in the background, and closedA tap opens the right screen once, including when the tap launches the app.
Each deep link, custom action, and button your app supportsThe right screen opens; unknown links and actions follow your app's normal fallback.
In-app campaignIt appears the next time the app becomes active, over the current screen, and closes with its close button.
Notification permission deniedIn-app campaigns still arrive; visible notifications do not.
Business eventsThe agreed events appear for the customer in AlgorithmX.

API reference

CallUse
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, onCampaignInteractionYour 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

SymptomWhat to check
initialize fails with a different URLDart and native code use different URLs. Use the same value in both.
No events in AlgorithmXNative initialization runs at startup with the right HTTPS URL, and the device is online.
No AlgorithmX notifications arriveSee the troubleshooting section of the Android or iOS guide: token registration, sending access, and forwarding.
Android messages stop reaching firebase_messagingA second FirebaseMessagingService was added; keep only one and forward from it.
Tap on a notification does not reach DartHandlers are set before initialize, and the native Application or AppDelegate starts the SDK.
Tap that launches the app opens nothingYour router keeps the destination until the navigator is ready.
A button does nothing, or opens the main action on iOSYour button handler has no case for that action text and returned false.
In-app campaign never appearsPush 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 appearsCall 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.