localytics_flutter_sdk

Flutter plugin that wraps the official Localytics native SDKs:

  • Androidcom.localytics.androidx:library:7.1.0 from the Localytics public Maven repo.
  • iOSLocalytics-swiftpm 7.1.1 (pinned) via Swift Package Manager.

Published on pub.dev as localytics_flutter_sdk (the localytics_flutter name is held by a legacy package we do not control).

Requirements

Minimum
Flutter 3.22.0 (with SPM enabled)
Dart 3.5.0
Android minSdk 21
iOS 12.0

Enabling Swift Package Manager

The plugin ships iOS support via SPM only — there is no .podspec. SPM has been available in Flutter since 3.22 but is not always on by default. Enable it once per machine:

flutter config --enable-swift-package-manager

You can verify it's on with flutter config (look for enable-swift-package-manager: true).

Installation

Add the plugin to your app's pubspec.yaml:

dependencies:
  localytics_flutter_sdk: ^1.0.3

Then run:

flutter pub get

iOS — Swift Package Manager

The plugin declares its dependency on the official Localytics SPM package directly. Make sure SPM is enabled in your Flutter install (see Enabling Swift Package Manager above).

The first iOS build will fetch and compile Localytics-swiftpm from GitHub — expect a couple of extra minutes on the first run; subsequent builds are cached.

No extra Podfile entries are required — the plugin's ios/localytics_flutter_sdk/Package.swift pins Localytics-swiftpm for you.

iOS — App key

Unlike Android (which reads ll_app_key from localytics.xml), the iOS SDK expects the app key when you build the Localytics instance in AppDelegate. Use one of these patterns:

Option A — Info.plist + xcconfig (keeps keys out of Swift source)

  1. Add to ios/Runner/Info.plist:
<key>LocalyticsAppKey</key>
<string>$(LOCALYTICS_APP_KEY)</string>
  1. Create a gitignored ios/Flutter/LocalyticsEnv.xcconfig in your app:
LOCALYTICS_APP_KEY=your-ios-app-key-here
  1. Include it from ios/Flutter/Debug.xcconfig and Release.xcconfig (before Generated.xcconfig):
#include? "LocalyticsEnv.xcconfig"
#include "Generated.xcconfig"
  1. In AppDelegate, read the key from the bundle (see iOS initialization below).

Option B — Inline in AppDelegate (simplest)

Pass the key directly when building the SDK:

.settingAppKey("your-ios-app-key-here")

For push, location / Places, background modes, and permission delegates, follow the official Localytics iOS integration guide.

Android — Maven repository

The plugin's build.gradle already adds the Localytics Maven repository. However, your app-level android/build.gradle (or the consuming app's settings.gradle dependencyResolutionManagement block) must also expose it because Gradle resolves dependencies from the consuming app's repositories, not the plugin's:

// android/build.gradle (project level) OR settings.gradle
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://maven.localytics.com/public' }
    }
}

You also need to add a localytics.xml file to your app at android/app/src/main/res/values/localytics.xml containing your Localytics app key. See the official Localytics Android docs for the full template.

Google Play Services Ads Identifier

The Localytics SDK reflectively resolves AdvertisingIdClient at runtime to attach the device ADID to analytics payloads. Without that class on the classpath the SDK crashes at the first session open with:

java.lang.NoClassDefFoundError: Failed resolution of:
  Lcom/google/android/gms/ads/identifier/AdvertisingIdClient;

This plugin already pulls in com.google.android.gms:play-services-ads-identifier as an api dependency, so it's transitively available in your app — no extra action needed for typical Play-store builds.

If you target stores that forbid Google Play Services (Huawei AppGallery, Amazon Appstore, F-Droid, etc.), exclude it explicitly in your app's android/app/build.gradle:

dependencies {
    implementation('com.localytics.flutter') {
        exclude group: 'com.google.android.gms',
                module: 'play-services-ads-identifier'
    }
}

The SDK swallows the resulting ClassNotFoundException and proceeds without an ADID attached.

Initialization

The native SDK is initialized via the standard Localytics Builder / autoIntegrate flow on each platform. This plugin does not wrap the initialization step — you should keep that in your platform-native code (Application.kt for Android, AppDelegate.swift for iOS) so you get the exact behavior described in the Localytics docs.

Android: MainActivity must extend FlutterFragmentActivity

The Localytics Android SDK requires a FragmentActivity subclass to host the dialog fragment that renders in-app messages. Flutter's default FlutterActivity extends androidx.core.app.ComponentActivity, not FragmentActivity — so triggering an in-app message with the default scaffold fails with:

In-app messages can only appear in Activities subclassing
FragmentActivity, which includes ActionBarActivity and AppCompatActivity.

Switch your MainActivity to Flutter's first-class FlutterFragmentActivity instead. It's a drop-in replacement — same lifecycle, same plugin behavior, same manifest setup:

package com.example.app

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

No AndroidManifest.xml changes are needed (the activity entry stays the same).

Android: Application and localytics.xml

Register a custom Application subclass in AndroidManifest.xml (android:name=".YourApplication") and call Localytics.Builder()…autoIntegrate(this) from onCreate(). The app key is supplied via android/app/src/main/res/values/localytics.xml (ll_app_key) as described in Android — Maven repository. See example/android/ for a working setup.

iOS: initialize in AppDelegate

Call Localytics.builder()…autoIntegrate() in application(_:didFinishLaunchingWithOptions:) before returning from super. The plugin does not perform this step for you.

Flutter ships two iOS application scaffolds and the AppDelegate boilerplate differs between them. Identify which one your project uses by opening ios/Runner/Info.plist:

  • Scene-based scaffold (default for apps created with Flutter 3.29+) — Info.plist contains a UIApplicationSceneManifest entry with UISceneDelegateClassName, and ios/Runner/ includes a SceneDelegate.swift that subclasses FlutterSceneDelegate. Use Sample A.
  • Legacy scaffold (apps created on Flutter 3.28 or earlier that have not migrated) — no UIApplicationSceneManifest, no SceneDelegate.swift. Use Sample B.

If you copy the wrong sample your build succeeds but every Dart call into the plugin throws MissingPluginException at runtime, because plugin registration happens against the wrong engine instance.

Sample A — scene-based scaffold (default on Flutter 3.29+)

The implicit FlutterEngine is created by SceneDelegate later in the launch sequence. Register plugins through FlutterImplicitEngineDelegate.didInitializeImplicitFlutterEngine(_:)not in didFinishLaunchingWithOptions. Registering in both places double-attaches the plugin to the engine and trips the runtime warning This FlutterEngine was already invoked., which blocks the VM service from coming up.

import Flutter
import Localytics
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let appKey = (Bundle.main.object(forInfoDictionaryKey: "LocalyticsAppKey") as? String)
      ?? "YOUR_IOS_APP_KEY"

    var builder = Localytics.builder()
      .settingAppKey(appKey)
      .settingLaunchOptions(launchOptions)
    #if DEBUG
    builder = builder.settingLoggingEnabled(true)
    #endif
    builder.build().autoIntegrate()

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
  }
}

A working reference is example/ios/Runner/AppDelegate.swift.

Sample B — legacy scaffold (no SceneDelegate)

FlutterAppDelegate.application(_:didFinishLaunchingWithOptions:) calls GeneratedPluginRegistrant.register(with: self) for you inside super, so no manual registration is needed.

import Flutter
import Localytics
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let appKey = (Bundle.main.object(forInfoDictionaryKey: "LocalyticsAppKey") as? String)
      ?? "YOUR_IOS_APP_KEY"

    var builder = Localytics.builder()
      .settingAppKey(appKey)
      .settingLaunchOptions(launchOptions)
    #if DEBUG
    builder = builder.settingLoggingEnabled(true)
    #endif
    builder.build().autoIntegrate()

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Turn off verbose logging in release builds (settingLoggingEnabled only in #if DEBUG, as shown).

iOS: push notifications

After APNs registration, forward notification callbacks from your AppDelegate (or UNUserNotificationCenter delegate) into the Dart plugin:

  • Localytics.didReceiveNotificationResponse(…)
  • Localytics.handleNotification(…) / handleNotificationReceived(…)
  • didRequestUserNotificationAuthorization / didRegisterNotificationSettings

Enable the Push Notifications capability in Xcode and complete any entitlements / Info.plist entries required by your push provider. Details are in the Localytics iOS docs.

Library version tagging

The plugin automatically tags itself with the native SDK during onAttachedToEngine / register(with:), so every uploaded event reports a library identifier like:

androida_7.1.0:Flutter_1.0.3     # Android
iosa_7.1.1:Flutter_1.0.3         # iOS

This matches the convention used by the Localytics Xamarin/MAUI and React Native wrappers, and lets Localytics' analytics dashboards break down traffic by wrapper SDK. The tagging is best-effort — if the call fails for any reason the SDK simply continues with its default identifier.

The version string is defined once in pubspec.yaml and propagated to Dart, Kotlin, Swift, and android/build.gradle by:

dart run tool/sync_plugin_version.dart

Run that command after every version: bump. For native SDK bumps (Android Maven / iOS SPM) and coordinated release checklists, see VERSIONING.md.

After native initialization, all Dart code interacts with the SDK through the Localytics class:

import 'package:localytics_flutter_sdk/localytics_flutter_sdk.dart';

await Localytics.setLoggingEnabled(true);
await Localytics.tagEvent('Onboarding Completed');
await Localytics.setCustomerId('3neRKTxbNWYKM4NJ');
await Localytics.upload();

Public API

The plugin exposes the following surface. Each row maps to one or more methods on the Localytics facade.

Area Methods
Privacy / logging setLoggingEnabled, setOptedOut, isOptedOut, setPrivacyOptedOut, isPrivacyOptedOut
Session / upload openSession, closeSession, upload, pauseDataUploading, setSessionTimeoutInterval (no-op; configure at init)
Test mode / theme setTestModeEnabled, isTestModeEnabled, enableDarkMode
Diagnostics getCustomerId, getIdentifier, getPushRegistrationId, getInstallId, getAppKey, getLibraryVersion, Localytics.pluginVersion
Events tagEvent, tagScreen, setCustomDimension, getCustomDimension, setIdentifier
Ecommerce tagPurchased, tagAddedToCart, tagStartedCheckout, tagCompletedCheckout
Content tagContentViewed, tagSearched, tagShared, tagContentRated
Customer identity setCustomerId, setCustomerIdWithPrivacyOptedOut, setCustomerEmail, setCustomerFirstName, setCustomerLastName, setCustomerFullName, tagCustomerRegistered, tagCustomerLoggedIn, tagCustomerLoggedOut, tagInvited
Profile setProfileAttribute, deleteProfileAttribute, addProfileAttributesToSet, removeProfileAttributesFromSet, incrementProfileAttribute, decrementProfileAttribute
In-app triggerInAppMessage, dismissCurrentInAppMessage, setInAppMessageDismissButtonLocation, triggerSessionStartInAppMessages
Location / Places setLocationMonitoringEnabled, persistLocationMonitoring
Push (cross-platform) setPushRegistrationId, handleTestModeURL
Push (Android-only) registerPush, handleFirebaseMessage, tagPushReceivedEvent
Push (iOS-only) didReceiveNotificationResponse, handleNotification, handleNotificationReceived, didRequestUserNotificationAuthorization, didRegisterNotificationSettings
Inbox getInboxCampaigns, refreshInboxCampaigns, getAllInboxCampaigns, refreshAllInboxCampaigns, getInboxCampaignsUnreadCount, setInboxCampaignRead, deleteInboxCampaign, inboxListItemTapped

Models / enums

  • Customer — used with tagCustomerRegistered and tagCustomerLoggedIn.
  • InboxCampaign — read-only descriptor returned by inbox queries.
  • ProfileScopeapplication (default) or organization.
  • InAppDismissButtonLocationleft or right.

Platform-only methods

The Android-only and iOS-only push entries are still callable from any platform — the opposite platform implements them as a graceful no-op so your cross-platform Dart code never throws MissingPluginException. This lets you keep a single push pipeline in shared Dart code:

if (Platform.isAndroid) {
  final handled = await Localytics.handleFirebaseMessage(message.data);
} else {
  await Localytics.didReceiveNotificationResponse(
    userInfo: message.data,
    actionIdentifier: response.actionIdentifier,
  );
}

Location / Places

setLocationMonitoringEnabled and persistLocationMonitoring tell the native SDK to use location for geofence / Places campaigns. They do not show the system permission dialog — request location access in your Flutter app first (for example with permission_handler or geolocator), then call these once the user has granted the authorization level you need.

You still need the usual platform setup in the host app (AndroidManifest location permissions, iOS Info.plist usage strings, background modes if required). On iOS, some flows also require a native LLLocationMonitoringDelegate for permission prompts — see the Localytics iOS integration guide.

// Example: after your app has obtained location permission
await Localytics.setLocationMonitoringEnabled(true);
await Localytics.persistLocationMonitoring(true);

Not covered (yet)

  • Advanced geofencing / Places (setLocation, triggerRegion, geofencesToMonitor, region enter/exit listeners) — requires EventChannel and richer native delegate wiring.
  • LocationListener / CallToActionListener callbacks — require EventChannel.

Architecture

The plugin uses a plain MethodChannel (com.localytics.flutter/methods). A future revision may migrate to Pigeon for type-safe codegen — the existing API is structured to make that swap mechanical.

Releasing & testing the published artifact

The bundled example/ app exercises the plugin via a same- repo path: dependency, which is great for day-to-day iteration but won't catch every "ships broken" issue. Before tagging a release, walk through the validation workflow documented in RELEASING.md — it covers pub publish --dry-run, pana scoring, consumer-app testing via git ref, release-mode builds, and the version-sync checklist that keeps the four pluginVersion constants and the two native SDK pins in lockstep.

Secrets and the example app

Real Localytics app keys must never be committed or published.

Location What ships publicly
pub.dev package Plugin only — example/ is excluded via .pubignore
Git .env and LocalyticsEnv.xcconfig are gitignored; only .env.example with placeholders may be committed
Your machine Copy .env.example.env locally and add your keys (see example/README.md)

The pub.dev tarball does not include your .env, generated xcconfig files, or the tool/sync_sample_env.dart helper. Clone the repository to run the example app.

License

This plugin’s source code is licensed under the Apache License, Version 2.0.

Use of the Localytics platform and native Android/iOS SDKs remains subject to your Localytics customer agreement and the separate terms that govern those SDKs.

Libraries

localytics_flutter_sdk
Flutter bindings for the Localytics native SDKs (Android + iOS).