Skip to content

Built, not yet published · pub

Retainza Flutter SDK

Identify users, track events, register push and receive in-app messages from a Flutter app on iOS and Android.

View as Markdownllms.txt

Build this with AI

Integrate the Retainza Flutter SDK into this Flutter app: add the package the way the guide describes (it is not on pub.dev yet), initialize it with my public key, identify the user on login, track the key events, register push and handle in-app messages.

Overview

retainza_flutter is the Dart client for Retainza. It identifies users, tracks events, keeps collections, registers push tokens and receives in-app messages from a Flutter app on iOS and Android.

  • Dart 3.4+ / Flutter 3.22+
  • iOS 15+ and Android 7.0 (API 24)+

Attribute rules, the offline queue, sessions and consent behave exactly as in the other client SDKs. The concepts section covers them at length; this page covers what is different in Flutter.

1. Install

Once the package is published, the dependency will be:

pubspec.yaml
dependencies:
  retainza_flutter: ^1.0.0

then flutter pub get.

pubspec.yaml
dependencies:
  retainza_flutter:
    path: ../sdk/flutter/retainza_flutter

It brings in http, shared_preferences, package_info_plus, device_info_plus and flutter_timezone. Firebase is deliberately not a dependency — an app using Retainza only for analytics should not inherit it. Add it yourself if you use push (Push).

The SDK is pure Dart with no platform channels of its own: nothing to link, and no pod install steps beyond what the plugins above need.

2. Start the SDK

lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:retainza_flutter/retainza_flutter.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  Retainza.init(
    publicKey: const String.fromEnvironment('RETAINZA_PK'), // pk_test_… or pk_live_…
    debug: kDebugMode,
  );
  runApp(const MyApp());
}

Then use Retainza.instance anywhere. Call init once; calling it again replaces the client. Retainza.instance throws a StateError if init has not run.

Only a public key (pk_…) is accepted — a secret key throws ArgumentError. The key decides the environment, read back as Retainza.instance.environment (RetainzaEnvironment.test for pk_test_, .live for pk_live_, .unknown for any other pk_ key). There is no runtime switch.

Platform facts (timezone, device, app version, install date) resolve asynchronously. Await them before the very first identify if it must carry them — every later call carries them regardless:

Dart
await Retainza.ready();

Configuration worth knowing about

All of these are named parameters of Retainza.init:

ParameterDefaultWhat it does
baseUrl--dart-define=ENGAGE_BASE_URL, else https://api.retainza.com/v1Ingest API
userProvider—() => currentUserId; authoritative when non-null
debugfalseLogs requests and state. Off = no console output at all
logger—Receives lines instead of the console; gets warn/error even with debug off
defaultAttributes—Extra attributes on every identify. App values win
storageSharedPreferencesStorage()Queue, install id, session, base-URL override
autoSessiontrueSessions from the app lifecycle (Sessions)
requireConsentfalseSend nothing until analytics consent (Consent)
allowRuntimeBaseUrlvalue of debugLets setBaseUrl work. Keep off in release
sessionTimeout30 minBackground time before a new session
maxQueuedEvents100Offline queue capacity
devicedetectedOverride detected DeviceInfo fields
httpClient—Override the HTTP client (tests, proxies)

A dev screen can repoint a debug build without rebuilding:

  • setBaseUrl(url) — remembers an override; throws StateError unless allowRuntimeBaseUrl (or debug) is on, and the URL must start with http:// or https://.
  • clearBaseUrl() — forgets the override.
  • getBaseUrl() — the URL requests actually go to (stored override > config > dart-define > default).
  • testConnection() — hits /health (one level above /v1) and returns (ok, status, error).

3. Identify & track

Dart
final r = Retainza.instance;

await r.identify(
  'user_42',
  attributes: {'plan': 'pro', 'trial_days_left': 3},
  profile: const RetainzaProfile(firstName: 'Asha', email: 'asha@example.com', locale: 'en-IN'),
);

await r.setAttributes({'plan': 'team'});          // patch — sends only this
await r.track('checkout_completed', properties: {'amount': 499, 'currency': 'INR'});

After identify, later calls use that user id; pass userId: to override, or configure userProvider. A call with no resolvable user throws StateError.

Attribute values are scalars: String, num, bool or null. null values in SDK/default layers are dropped rather than clearing server-side values.

RetainzaProfile fields become the reserved attributes first_name, last_name, email, phone, gender, birthdate; email and locale are also sent as top-level columns. Empty strings are ignored.

Collections

One-to-many data such as challenges, badges or saved searches. The item id is the merge key, so an offline replay never duplicates an item:

Dart
await r.addToCollection('challenges', CollectionItem('lagos_10k', {'status': 'joined'}));
await r.removeFromCollection('challenges', 'lagos_10k');
await r.setCollection('badges', [CollectionItem('early_bird'), CollectionItem('streak_7')]); // [] clears

A CollectionItem id may not be empty (the constructor throws ArgumentError).

Signed identity

Optional, and in the SDK's unreleased changes after 1.0.0. With identity verification on for an environment, your server signs each user id (hex(HMAC-SHA256(identitySecret, userId))) and returns it with the session. Pass it on every launch:

Dart
await Retainza.instance.identify(user.id, userHash: userHash);
// or, without re-identifying:
Retainza.instance.setUserHash(userHash);

The hash is sent with every call for that user id and no other, is kept in memory only, and is cleared by reset(). Never put the identity secret in the app. See Identity.

Logout

Dart
await Retainza.reset();

Unregisters this device's push token from the user being left, then clears the cached user id, the offline queue, session counters and in-app polling. The install id and install_date survive — they describe the device. If you use userProvider, make it return null after logout.

Attributes the SDK sets for you

platform, app_version, sdk_version, install_date, language, country, tz (IANA, e.g. Asia/Kolkata), device_model, device_manufacturer, os_name, os_version, the session counters (Sessions) and push_permission once you report it. country is the locale region, not GPS — no permission prompt is involved.

4. Push

Retainza sends push through Firebase Cloud Messaging on both platforms, so register the FCM token — on iOS too, not the raw APNs device token.

Step 1: Set up Firebase

Set up Firebase for your app (flutterfire configure) and add firebase_core and firebase_messaging. On iOS, enable the Push Notifications capability and the Remote notifications background mode, and upload your APNs key in the Firebase console.

Step 2: Report permission and register the token
Dart
import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> setUpPush() async {
  final r = Retainza.instance;
  final fm = FirebaseMessaging.instance;

  // The SDK never prompts — asking is your product decision.
  final settings = await fm.requestPermission();
  await r.setPushPermission(switch (settings.authorizationStatus) {
    AuthorizationStatus.authorized => PushPermission.authorized,
    AuthorizationStatus.provisional => PushPermission.provisional,
    AuthorizationStatus.denied => PushPermission.denied,
    AuthorizationStatus.notDetermined => PushPermission.undetermined,
  });

  final token = await fm.getToken();
  if (token != null) await r.registerPush(token, Retainza.pushPlatform);
  fm.onTokenRefresh.listen((t) => r.registerPush(t, Retainza.pushPlatform));
}

Call it after identify. registerPush is safe on every launch: an unchanged token is re-sent at most once a day (force: true overrides). It is never queued — a late token may already be stale — so it throws on network errors.

Every Retainza push carries engage_* keys in its data block. parseRetainzaPush returns null for pushes that belong to another SDK:

Dart
void handleOpen(RemoteMessage m) {
  final push = parseRetainzaPush(m.data);
  if (push == null) return; // not ours
  Retainza.instance.ackPush(push.msgId, PushAction.opened).ignore();
  if (push.route != null) Retainza.instance.resolveRoute(push.route!);
}

FirebaseMessaging.onMessageOpenedApp.listen(handleOpen);
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) handleOpen(initial);

resolveRoute hands the route to the handler you register with onNavigate (In-app messages). Report PushAction.delivered from FirebaseMessaging.onMessage (foreground) if you want delivery counts — FCM has no delivery receipt of its own.

5. In-app messages

Poll for messages and hand each to your renderer:

Dart
final r = Retainza.instance;

r.onInApp((msg) {
  // msg.template: banner | modal | bottomsheet | fullscreen | html
  // msg.content: the full payload (title, body, buttons, media, style …)
  showMyDialog(title: msg.content['title'] as String?, body: msg.content['body'] as String?);
});

final stop = r.startInAppPolling(); // every 15 s; returns a stop function

shown is acked automatically when a message is delivered to onInApp. Report the rest yourself:

Dart
await r.ackInApp(msg.msgId, InAppAction.clicked);
await r.ackInApp(msg.msgId, InAppAction.dismissed);

fetchPendingInApp() does a single poll if you prefer to drive timing. The polling interval can be changed with startInAppPolling(interval: …).

Dart
r.onNavigate((route) => navigatorKey.currentState?.pushNamed(route.screen, arguments: route.kv));

Push opens and in-app buttons both resolve through this handler. A route is a screen name plus a kv map of string params.

Rendering it yourself (selfhandled)

Campaigns built with the self-handled template are never drawn by the SDK and never auto-acked. Your handler owns the acks — skipping them empties that campaign's funnel:

Dart
r.onSelfHandledInApp((h) {
  showMyCard(h.message.content, onVisible: h.markShown, onTap: () => h.markClicked(), onClose: h.markDismissed);
});

markClicked([route]) acks clicked and then navigates (to the given route, else the message's own). A self-handled message that arrives with no handler registered stays queued and is logged as a warning.

6. Offline queue

identify, setAttributes, track and the collection calls survive network failures. Network errors and 5xx responses are queued (100 items by default, oldest dropped first), persisted, and flushed in order with their original timestamps — backoff doubles from 1 s up to 5 min. A 4xx is thrown and never queued. Push registration and acks are never queued.

Dart
Retainza.instance.queueSize;          // pending calls
await Retainza.instance.flushQueue(); // drain now

8. Sessions

With autoSession on (default), the SDK starts a session at init and on each return to the foreground, and banks foreground time when the app goes to the background. Returning within 30 minutes (sessionTimeout) resumes the same session. A new session emits a session_start event and updates session_count, first_session_at, last_session_at and session_duration_total.

Turn it off (autoSession: false) and call startSession() / endSession() yourself if you need custom rules.

9. Troubleshooting

SymptomCheck
Nothing reaches the dashboarddebug: true and read the log; testConnection(); the key belongs to the environment you are looking at
StateError: no userIdCall identify first, pass userId:, or set userProvider
Events arrive lateDevice was offline — they were queued; queueSize shows the backlog
Nothing sent at all with requireConsentsetConsent(analytics: true) has not been called
Push token registered, no pushes on iOSYou registered the APNs token instead of FirebaseMessaging.instance.getToken(), or no APNs key in Firebase
Self-handled campaign shows 0 impressionsYour handler never calls markShown()
First identify lacks device/timezoneawait Retainza.ready() before it

The SDK's example app exercises every call. With the SDK source checked out, run it from its example/ directory:

Terminal
flutter run --dart-define=RETAINZA_PK=pk_test_xxx

Other platforms: React Native, iOS, Android, server. The HTTP API is in the reference.

Developer docsDashboard

Popular