AlgorithmX
Integrations14 min read

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: minSdk 24 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

StepWhat you doWhere
1Install the pluginpubspec.yaml
2Start the SDKAndroid Application, iOS AppDelegate, Dart main()
3Tell AlgorithmX who the customer isLogin, app start, logout
4Send eventsWhere actions happen, such as checkout
5Connect push notificationsYour Firebase code and iOS AppDelegate
6Open the right screenDart main()
7Check in-app campaignsNo code
8TestReal Android and iOS phones

Step 1: Install the plugin

From your app's folder, run:

bash
flutter pub add algorithmx_flutter

This adds the plugin to your pubspec.yaml:

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:

kotlin
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:

xml
<application
    android:name=".MyApplication"
    ...>

2b. iOS

In application(_:didFinishLaunchingWithOptions:) of your AppDelegate, before return super...:

swift
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:

dart
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());
}
  • initialize fails 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 when initialize runs. If your handlers are not set yet, they miss them.
  • To see each SDK request while testing, call AlgorithmX.setLoggingEnabled(true) (Android) or AlgorithmX.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:

dart
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:

dart
await AlgorithmX.instance.clearWebViewQueue();
await AlgorithmX.instance.resetIdentity();

When one account replaces another without a logout:

dart
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.

dart
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:

dart
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 FirebaseMessagingService next to firebase_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:

kotlin
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:

kotlin
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.

swift
// 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)
}
  • AlgorithmX here is the iOS SDK inside the plugin. import algorithmx_flutter gives 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.swift file (in its ios folder, under NativeSDK/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.

HandlerWhen it runsWhat to do
onDeepLinkThe campaign opens a screen with a link.Open the link with your router.
onCustomActionThe campaign runs a custom action.Open the screen for that action.
onActionButtonClickedThe customer taps a notification button.Handle the button and return true.
onNotificationClickEvery 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):

dart
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. Return true for 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:

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

CallWhat 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, onCampaignInteractionYour 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?

ProblemWhat to check
initialize failsDart and native code use different URLs. Use the same URL in all three places of step 2.
No events in AlgorithmXThe native code in step 2 runs with the right HTTPS URL, and the phone is online.
No AlgorithmX notificationsSee "Something not working?" in the Android or iOS guide: token, sending permission, and passing messages to the SDK.
Android messages stop reaching firebase_messagingA second FirebaseMessagingService was added. Keep only one.
A tap does not reach DartHandlers are set before initialize, and the native code in step 2 starts the SDK.
A tap that starts 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 appearsStep 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 appearsCall 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.