“This project is based on code originally developed by Denisr under the BSD 3-Clause License.”
A comprehensive Flutter wrapper for Klaviyo's Android SDK and iOS SDK. Track events, manage user profiles, and handle push notifications with ease.
- Event Tracking: Log custom events with optional metadata
- Profile Management: Create and update user profiles with identifiers and attributes
- Push Notifications: Full support for Firebase Cloud Messaging (Android) and APNs (iOS)
- Type Safety: Strongly-typed API with null safety support
- Cross-Platform: Unified API for both iOS and Android
- Android: Minimum SDK version 23+, uses Klaviyo Android SDK v3.3.1
- iOS: iOS 13.0+, uses Klaviyo Swift SDK v4.2.1
- Dart: SDK 3.2.0 or higher
- Flutter: 3.0.0 or higher
- Firebase: This plugin requires
firebase_messagingfor push notifications on both platforms
Add klaviyo_flutter to your pubspec.yaml:
dependencies:
klaviyo_flutter: ^0.3.0Then run:
flutter pub getimport 'package:flutter/material.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize with your Klaviyo public API key
await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');
runApp(MyApp());
}Get your public API key from Klaviyo Account Settings.
Initialize Klaviyo before using any other methods, typically in your main() function:
await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');
// Check if already initialized
if (Klaviyo.instance.isInitialized) {
print('Klaviyo is ready!');
}Track user actions and behaviors in your app:
await Klaviyo.instance.logEvent('Viewed Product');await Klaviyo.instance.logEvent(
'Added to Cart',
{
'product_id': '12345',
'product_name': 'Blue T-Shirt',
'price': 29.99,
'quantity': 2,
},
);Klaviyo supports special e-commerce events with the $ prefix:
// Track a purchase
await Klaviyo.instance.logEvent(
'\$successful_payment',
{
'\$value': 149.99,
'order_id': 'ORD-789',
'items': ['item1', 'item2'],
},
);
// Track product views
await Klaviyo.instance.logEvent(
'Viewed Product',
{
'\$product_id': 'SKU-123',
'\$product_name': 'Wireless Headphones',
'\$value': 79.99,
},
);Use KlaviyoProfile for bulk updates with all fields optional:
await Klaviyo.instance.updateProfile(
KlaviyoProfile(
id: 'customer-42', // External ID
email: 'user@example.com',
phoneNumber: '+15555551212',
firstName: 'Jane',
lastName: 'Doe',
organization: 'Acme Corp',
title: 'Product Manager',
image: 'https://example.com/avatar.jpg',
address1: '123 Main St',
address2: 'Apt 4B',
city: 'San Francisco',
country: 'United States',
region: 'CA',
zip: '94102',
timezone: 'America/Los_Angeles',
latitude: 37.7749,
longitude: -122.4194,
properties: {
'app_version': '2.0.5',
'premium_member': true,
'loyalty_points': 1250,
},
),
);Set profile attributes one at a time:
// Set identifiers
await Klaviyo.instance.setExternalId('customer-123');
await Klaviyo.instance.setEmail('user@example.com');
await Klaviyo.instance.setPhoneNumber('+15555551212');
// Set name fields
await Klaviyo.instance.setFirstName('Jane');
await Klaviyo.instance.setLastName('Doe');
// Set organization info
await Klaviyo.instance.setOrganization('Acme Corp');
await Klaviyo.instance.setTitle('Product Manager');
await Klaviyo.instance.setImage('https://example.com/avatar.jpg');
// Set address fields
await Klaviyo.instance.setAddress1('123 Main St');
await Klaviyo.instance.setAddress2('Apt 4B');
await Klaviyo.instance.setCity('San Francisco');
await Klaviyo.instance.setCountry('United States');
await Klaviyo.instance.setRegion('CA');
await Klaviyo.instance.setZip('94102');
await Klaviyo.instance.setTimezone('America/Los_Angeles');
// Set location coordinates
await Klaviyo.instance.setLatitude(37.7749);
await Klaviyo.instance.setLongitude(-122.4194);
// Set custom attributes
await Klaviyo.instance.setCustomAttribute('favorite_color', 'blue');
await Klaviyo.instance.setCustomAttribute('membership_level', 'gold');Retrieve current profile identifiers:
String? externalId = await Klaviyo.instance.getExternalId();
String? email = await Klaviyo.instance.getEmail();
String? phoneNumber = await Klaviyo.instance.getPhoneNumber();
print('Current user: $email (ID: $externalId)');Clear all profile data (e.g., on user logout):
await Klaviyo.instance.resetProfile();Note: After resetting, you'll need to call sendTokenToKlaviyo() again to associate the device with a new profile.
The KlaviyoProfile class provides convenient methods for profile manipulation:
final profile = KlaviyoProfile.fromJson({
'external_id': 'user-123',
'email': 'user@example.com',
'first_name': 'Jane',
'latitude': 37.7749,
'properties': {'app_version': '1.0.0'},
});Create a modified copy while preserving other fields:
final updatedProfile = profile.copyWith(
city: 'New York',
zip: '10001',
properties: {'app_version': '1.1.0'},
);Map<String, dynamic> json = profile.toJson();This plugin provides a unified push notification stream for both iOS and Android:
- iOS: Push events are handled natively via
UNUserNotificationCenterDelegateand streamed to Flutter - Android: The plugin automatically bridges
firebase_messagingstreams internally, transformingRemoteMessageobjects intoKlaviyoPushEvent
This means you only need to listen to Klaviyo.instance.onPushEvent - the plugin handles the platform differences for you.
- Configure Firebase Cloud Messaging for both platforms
- Initialize Firebase in your app (required for both platforms)
- Initialize the Klaviyo push bridge
- Listen to unified push events (received/opened/dismissed/action)
- Handle deep links and analytics in your app
- Create a Firebase project and add your Android/iOS apps
- Download
google-services.json(Android) andGoogleService-Info.plist(iOS) - Enable Firebase Cloud Messaging
- Copy the FCM server key to Klaviyo Dashboard (Account > Settings > Push)
- Add
firebase_coreandfirebase_messagingto your app's dependencies:
dependencies:
firebase_core: ^3.0.0
firebase_messaging: ^15.0.0
klaviyo_flutter: ^0.3.0-
Add Google Services Plugin
Place
google-services.jsoninandroid/app/and updateandroid/app/build.gradle:dependencies { classpath 'com.google.gms:google-services:4.3.15' } // At the bottom of the file apply plugin: 'com.google.gms.google-services'
-
Initialize Firebase in your app
The plugin relies on
firebase_messagingfor Android push handling. Initialize Firebase before using Klaviyo:import 'package:firebase_core/firebase_core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); // Then initialize Klaviyo... }
-
Add Permissions
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
Optional: Custom Notification Icon
<meta-data android:name="com.klaviyo.push.default_notification_icon" android:resource="@drawable/ic_notification" />
-
Enable AndroidX
In
android/gradle.properties:android.useAndroidX=true android.enableJetifier=true
Note: On Android, push notifications are handled by
firebase_messaging. The plugin automatically listens to Firebase Messaging streams and converts them to unifiedKlaviyoPushEventobjects. You do not need to set up your own Firebase Messaging listeners for push events - just useKlaviyo.instance.onPushEvent.
-
Add GoogleService-Info.plist
Drag the file into the Runner target in Xcode.
-
Enable Capabilities
In Xcode, enable:
- Push Notifications
- Background Modes > Remote notifications
-
Update Info.plist
<key>NSPhotoLibraryUsageDescription</key> <string>We need access to your photo library</string>
-
Set Minimum Deployment Target
In
ios/Runner.xcodeproj/project.pbxproj:IPHONEOS_DEPLOYMENT_TARGET = 13.0;
import 'package:firebase_messaging/firebase_messaging.dart';
// Request permission (iOS prompts user, Android auto-grants on API 33+)
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
print('User granted permission');
// Tokens are automatically registered with Klaviyo when received.
// You can optionally get the token manually if needed:
// String? token = await FirebaseMessaging.instance.getToken();
}import 'package:firebase_core/firebase_core.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase first (required for Android push)
await Firebase.initializeApp();
// Initialize Klaviyo with your public API key
// This also initializes the push notification bridge:
// - On iOS: sets up native event channel
// - On Android: automatically subscribes to Firebase Messaging streams
await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');
// Check if app was launched from a notification
final initial = await Klaviyo.instance.getInitialNotification();
if (initial != null) {
// Handle deep links or analytics from a terminated launch
print('Launched from notification: ${initial.title}');
}
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_setupPushListeners();
}
void _setupPushListeners() {
// Listen to all push events
Klaviyo.instance.onPushEvent.listen((event) {
switch (event.type) {
case KlaviyoPushEventType.received:
print('Push received: ${event.title}');
break;
case KlaviyoPushEventType.opened:
print('Push opened: ${event.title}');
// Handle deep link
if (event.deepLink != null) {
// Navigate to deep link
}
break;
case KlaviyoPushEventType.dismissed:
print('Push dismissed');
break;
case KlaviyoPushEventType.actionTapped:
print('Action tapped: ${event.actionId}');
break;
}
});
// Or use specific streams
Klaviyo.instance.onPushOpened.listen((event) {
print('Push opened with appState: ${event.appState}');
});
// Listen for token refreshes (auto-registered with Klaviyo)
Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
print('Token refreshed: ${tokenEvent.token}');
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomeScreen());
}
}// Check rawData for Klaviyo marker
if (event.rawData.containsKey('_k')) {
// This is a Klaviyo push
}// Set badge count on app icon
await Klaviyo.instance.setBadgeCount(5);
// Clear badge
await Klaviyo.instance.setBadgeCount(0);Note: setBadgeCount() only works on iOS. On Android, it's a no-op because badge behavior varies by manufacturer.
Tokens are automatically registered with Klaviyo when refreshed. You can also listen for token events in Flutter:
Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
print('Token refreshed and auto-registered: ${tokenEvent.token}');
});Here's a full example demonstrating all major features:
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:klaviyo_flutter/klaviyo_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase first
await Firebase.initializeApp();
// Initialize Klaviyo (also sets up push notification bridge)
await Klaviyo.instance.initialize('YOUR_PUBLIC_API_KEY');
// Check for launch notification
final initialPush = await Klaviyo.instance.getInitialNotification();
runApp(MyApp(initialPush: initialPush));
}
class MyApp extends StatelessWidget {
final KlaviyoPushEvent? initialPush;
const MyApp({this.initialPush});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: KlaviyoDemo(initialPush: initialPush),
);
}
}
class KlaviyoDemo extends StatefulWidget {
final KlaviyoPushEvent? initialPush;
const KlaviyoDemo({this.initialPush});
@override
State<KlaviyoDemo> createState() => _KlaviyoDemoState();
}
class _KlaviyoDemoState extends State<KlaviyoDemo> {
String _status = 'Ready';
@override
void initState() {
super.initState();
_initializeKlaviyo();
_setupPushListeners();
// Handle initial push if app was launched from notification
if (widget.initialPush != null) {
_handlePushEvent(widget.initialPush!);
}
}
void _setupPushListeners() {
// Listen for all push events
Klaviyo.instance.onPushEvent.listen(_handlePushEvent);
// Token refreshes are auto-registered with Klaviyo
Klaviyo.instance.onTokenRefresh.listen((tokenEvent) {
setState(() => _status = 'Token registered');
});
}
void _handlePushEvent(KlaviyoPushEvent event) {
setState(() => _status = 'Push ${event.type.name}: ${event.title ?? ""}');
// Handle deep links
if (event.type == KlaviyoPushEventType.opened && event.deepLink != null) {
// Navigate to deep link
}
}
Future<void> _identifyUser() async {
try {
await Klaviyo.instance.updateProfile(
KlaviyoProfile(
id: 'user-12345',
email: 'demo@example.com',
firstName: 'Demo',
lastName: 'User',
phoneNumber: '+15555551212',
city: 'San Francisco',
country: 'United States',
properties: {
'app_version': '1.0.0',
'platform': 'flutter',
},
),
);
setState(() => _status = 'User identified');
} catch (e) {
setState(() => _status = 'Error: $e');
}
}
Future<void> _trackEvent() async {
try {
await Klaviyo.instance.logEvent(
'Button Clicked',
{
'button_name': 'Track Event',
'timestamp': DateTime.now().toIso8601String(),
},
);
setState(() => _status = 'Event tracked');
} catch (e) {
setState(() => _status = 'Error: $e');
}
}
Future<void> _trackPurchase() async {
try {
await Klaviyo.instance.logEvent(
'\$successful_payment',
{
'\$value': 99.99,
'order_id': 'ORD-${DateTime.now().millisecondsSinceEpoch}',
'item_count': 3,
},
);
setState(() => _status = 'Purchase tracked');
} catch (e) {
setState(() => _status = 'Error: $e');
}
}
Future<void> _logout() async {
try {
await Klaviyo.instance.resetProfile();
setState(() => _status = 'Logged out');
} catch (e) {
setState(() => _status = 'Error: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Klaviyo Flutter Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Status: $_status',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 40),
ElevatedButton(
onPressed: _identifyUser,
child: Text('Identify User'),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _trackEvent,
child: Text('Track Event'),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _trackPurchase,
child: Text('Track Purchase'),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _logout,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Logout'),
),
],
),
),
);
}
}| Method | Parameters | Returns | Description |
|---|---|---|---|
initialize() |
String apiKey |
Future<void> |
Initialize the SDK with your public API key |
logEvent() |
String name, [Map<String, dynamic>? metaData] |
Future<String> |
Log a custom event with optional metadata |
updateProfile() |
KlaviyoProfile profile |
Future<String> |
Update profile with bulk attributes |
setExternalId() |
String id |
Future<void> |
Set external identifier |
getExternalId() |
- | Future<String?> |
Get current external ID |
setEmail() |
String email |
Future<void> |
Set email address |
getEmail() |
- | Future<String?> |
Get current email |
setPhoneNumber() |
String phoneNumber |
Future<void> |
Set phone number |
getPhoneNumber() |
- | Future<String?> |
Get current phone number |
setFirstName() |
String firstName |
Future<void> |
Set first name |
setLastName() |
String lastName |
Future<void> |
Set last name |
setOrganization() |
String organization |
Future<void> |
Set organization |
setTitle() |
String title |
Future<void> |
Set job title |
setImage() |
String image |
Future<void> |
Set profile image URL |
setAddress1() |
String address |
Future<void> |
Set address line 1 |
setAddress2() |
String address |
Future<void> |
Set address line 2 |
setCity() |
String city |
Future<void> |
Set city |
setCountry() |
String country |
Future<void> |
Set country |
setRegion() |
String region |
Future<void> |
Set region/state |
setZip() |
String zip |
Future<void> |
Set postal code |
setTimezone() |
String timezone |
Future<void> |
Set timezone |
setLatitude() |
double latitude |
Future<void> |
Set latitude coordinate |
setLongitude() |
double longitude |
Future<void> |
Set longitude coordinate |
setCustomAttribute() |
String key, String value |
Future<void> |
Set custom profile attribute |
resetProfile() |
- | Future<void> |
Clear all profile data |
sendTokenToKlaviyo() |
String token |
Future<void> |
Register device token for push |
handlePush() |
Map<String, dynamic> message |
Future<bool> |
Handle incoming push notification |
isKlaviyoPush() |
Map<String, dynamic> message |
bool |
Check if push is from Klaviyo |
setBadgeCount() |
int count |
Future<void> |
Set app badge count (iOS only) |
isInitialized |
- | bool |
Check if SDK is initialized |
| Property | Type | Description |
|---|---|---|
id |
String? |
External user identifier |
email |
String? |
Email address |
phoneNumber |
String? |
Phone number (format guide) |
firstName |
String? |
First name |
lastName |
String? |
Last name |
organization |
String? |
Company/organization name |
title |
String? |
Job title |
image |
String? |
Profile image URL |
address1 |
String? |
Address line 1 |
address2 |
String? |
Address line 2 |
city |
String? |
City |
country |
String? |
Country |
region |
String? |
State/province/region |
zip |
String? |
Postal/ZIP code |
timezone |
String? |
Timezone (e.g., 'America/New_York') |
latitude |
double? |
Latitude coordinate |
longitude |
double? |
Longitude coordinate |
properties |
Map<String, dynamic>? |
Custom properties |
This plugin does not support clearing individual profile fields (e.g., setting a field to null). This is due to the underlying Klaviyo API behavior where omitting a field leaves it unchanged, and null values are not processed.
To clear profile fields: Use Klaviyo.resetProfile() to clear all profile data, then call updateProfile() with only the fields you want to keep:
// Clear all profile data
await Klaviyo.instance.resetProfile();
// Set only the fields you want to keep
await Klaviyo.instance.updateProfile(
KlaviyoProfile(
email: 'user@example.com',
firstName: 'Jane',
// All other fields are now cleared
),
);Build fails with "Duplicate class" errors
Enable Jetifier in android/gradle.properties:
android.enableJetifier=truePush notifications not received
- Verify
google-services.jsonis inandroid/app/ - Ensure Firebase is initialized before
Klaviyo.instance.initialize() - Confirm FCM server key is added to Klaviyo dashboard
- Check that
firebase_messagingis properly configured in your app
Build fails with deployment target error
Set minimum deployment target in ios/Runner.xcodeproj/project.pbxproj:
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
Push notifications not received
- Verify Push Notifications capability is enabled
- Check that APNs certificates are configured in Apple Developer Portal
- Ensure device token is being sent to Klaviyo
"API key must not be empty" error
Make sure you're using your public API key, not the private key.
Events not appearing in Klaviyo
- Check that
initialize()was called before logging events - Verify API key is correct
- Events may take a few minutes to appear in the dashboard
- Klaviyo Help Center
- Android SDK Documentation
- iOS SDK Documentation
- Push Notification Setup Guide
- Sending Push Campaigns
- Adding Push to Flows
Contributions are welcome! Please feel free to submit a Pull Request.
See the LICENSE file for details.
See CHANGELOG.md for version history.