localytics_flutter_sdk
Flutter plugin that wraps the official Localytics native SDKs:
- Android —
com.localytics.androidx:library:7.1.0from the Localytics public Maven repo. - iOS —
Localytics-swiftpm7.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)
- Add to
ios/Runner/Info.plist:
<key>LocalyticsAppKey</key>
<string>$(LOCALYTICS_APP_KEY)</string>
- Create a gitignored
ios/Flutter/LocalyticsEnv.xcconfigin your app:
LOCALYTICS_APP_KEY=your-ios-app-key-here
- Include it from
ios/Flutter/Debug.xcconfigandRelease.xcconfig(beforeGenerated.xcconfig):
#include? "LocalyticsEnv.xcconfig"
#include "Generated.xcconfig"
- 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.plistcontains aUIApplicationSceneManifestentry withUISceneDelegateClassName, andios/Runner/includes aSceneDelegate.swiftthat subclassesFlutterSceneDelegate. Use Sample A. - Legacy scaffold (apps created on Flutter 3.28 or earlier that have
not migrated) — no
UIApplicationSceneManifest, noSceneDelegate.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 withtagCustomerRegisteredandtagCustomerLoggedIn.InboxCampaign— read-only descriptor returned by inbox queries.ProfileScope—application(default) ororganization.InAppDismissButtonLocation—leftorright.
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) — requiresEventChanneland richer native delegate wiring. LocationListener/CallToActionListenercallbacks — requireEventChannel.
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).