guidexr_optimize_flutter_sdk 0.1.0
guidexr_optimize_flutter_sdk: ^0.1.0 copied to clipboard
Official Flutter SDK for the GuideXR Optimize platform — event tracking, user and device registration, and attribution capture.
GuideXR Optimize Flutter SDK #
The official Flutter SDK for GuideXR Optimize — a patient engagement and revenue intelligence platform for healthcare providers, built by Spatial Guide.
The SDK is a single, versioned client for everything an app needs to talk to GuideXR Optimize — event tracking, user/device registration, attribution capture, notification lifecycle tracking, a notification inbox, and in-app messages — so your app doesn't hand-roll and separately maintain this plumbing.
Contents #
- Prerequisites
- Install
- Quick start
- Configuration reference
- Event tracking
- User & device registration
- Attribution capture
- Consent
- Destinations: Firebase, Meta, and other forwarders
- Notification lifecycle tracking
- Notification inbox
- In-app messages
- Offline delivery
- Adopting the SDK in an app with existing users
- Example app
Prerequisites #
You need a GuideXR Optimize account with an API key and a channel ID. See spatial.guide/guidexr-optimize for account setup.
Install #
dependencies:
guidexr_optimize_flutter_sdk: ^0.1.0
Quick start #
Call init once, as early as possible in main():
import 'package:flutter/material.dart';
import 'package:guidexr_optimize_flutter_sdk/guidexr_optimize_flutter_sdk.dart';
final navigatorKey = GlobalKey<NavigatorState>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await GuidexrOptimize.init(OptimizeConfig(
baseUrl: 'https://your-optimize-endpoint',
apiKey: 'YOUR_API_KEY',
channelId: 'YOUR_CHANNEL_ID',
source: 'my_app',
navigatorKey: navigatorKey, // required for in-app overlays
onDeepLink: (link) => router.go(link),
));
runApp(MyApp());
}
Every other API on this page assumes init has already been called and
is accessed via the singleton:
final sdk = GuidexrOptimize.instance;
Configuration reference #
Everything the SDK needs is one OptimizeConfig, passed once to init.
| Field | Required | Purpose |
|---|---|---|
baseUrl |
✅ | GuideXR Optimize backend endpoint. Resolve your own dev/staging/prod URL before passing it in — the SDK doesn't know about environments. |
apiKey |
✅ | Sent as the x-api-key header on every request. |
channelId |
✅ | Your channel identifier. |
source |
✅ | Default context.source value (e.g. 'my_app'). Overridden automatically once attribution is captured. |
enableLogging |
Verbose diagnostic logging via onLog. |
|
onLog / onError |
Observability callbacks — pipe into your own logger/crash reporter. The SDK never throws into your app; every internal failure surfaces here instead. | |
destinations |
Intent flags per forwarder (see Destinations). | |
locationProvider |
Optional. Returns a location snapshot for event/registration payloads. The SDK never bundles location plugins itself — permission handling stays yours. | |
deviceIdProvider |
Optional. Returns a stable device id (e.g. android_id / identifierForVendor). |
|
installationIdProvider |
Optional. Returns an installation id (e.g. Firebase Installations). No provider → sent as "", never fabricated. |
|
appInstanceIdProvider |
Optional. Firebase Analytics app-instance id, used by the in-app evaluate call. | |
legacyAnonymousIdProvider |
Only relevant if you have existing users — see below. | |
navigatorKey |
for in-app messages | Your app's root navigator key, so the SDK can present in-app overlays. |
onDeepLink |
for in-app messages | Called when an in-app overlay tap carries a deeplink. The SDK never navigates itself. |
Event tracking #
sdk.trackPageView('home_page');
sdk.trackClick('add_to_cart_click', attributes: {'sku': 'X1'});
sdk.trackBackendOnly('server_only_event'); // skips destination forwarders
sdk.trackCustom('custom_event', attributes: {...});
sdk.trackAppOpen(
openSource: OptimizeOpenSource.push, // .push / .deepLink / .direct
notificationId: id,
campaignId: campaignId,
);
trackPageView de-duplicates consecutive identical calls automatically
— you don't need your own "don't fire the same screen view twice" logic.
app_install (fired exactly once per real device) and
app_version_changed are handled automatically by init — you never
call these yourself.
Attributes that should ride along on every event (not just one
call site) — e.g. a field your backend expects on every event, not just
in identifiers — can be set once instead of passed everywhere:
sdk.setDefaultEventAttributes({'user_phone': profile.phone ?? ''});
// cleared automatically on reset()
User & device registration #
await sdk.registerUser(
externalId: userId,
profile: OptimizeProfile(
email: email,
firstName: firstName,
lastName: lastName,
phone: phone,
),
channelPreferences: {'push_opt_in': pushPermissionGranted},
);
await sdk.registerDevice(fcmToken: token, pushEnabled: pushPermissionGranted);
// On logout:
await sdk.reset();
Channel preferences default to opted-in for every channel except
push_opt_in, which reflects the OS notification permission and
defaults to false if you don't pass it explicitly. An explicit
false you do pass is always respected.
Attribution capture #
// A deep link (e.g. from your router/deep-link package):
await sdk.captureAttributionFromUri(uri);
// Play Install Referrer:
await sdk.captureAttributionFromInstallReferrer(referrerString);
// A resolved Meta deferred deep link (you fetch it — the SDK has no
// Meta SDK dependency):
await sdk.captureAttributionFromUri(
resolvedUri,
attributionSource: 'meta_deferred',
priority: AttributionSourcePriority.metaDeferred,
);
Capture is priority-based (installReferrer < metaDeferred < manualDeepLink) and expires after 30 days. A single attribution_capture
event fires only when the capture actually changes something — not on
every call.
Consent #
await sdk.setConsent(OptimizeConsent(
analytics: true,
marketing: true,
location: false,
version: 'v1',
source: 'onboarding',
));
Attached automatically to every event and user-registration payload from then on.
Destinations: Firebase, Meta, and other forwarders #
The SDK never depends on firebase_analytics or facebook_app_events
— apps that don't use them shouldn't have to pull them in. Instead,
implement OptimizeForwarder using whatever destinations you've
already set up:
class MyFirebaseForwarder implements OptimizeForwarder {
@override
bool get isReady => Firebase.apps.isNotEmpty;
@override
Future<void> onEvent(OptimizeEvent event) =>
FirebaseAnalytics.instance.logEvent(
name: FirebaseNaming.normalizeName(event.name),
parameters: FirebaseNaming.normalizeParams(event.attributes),
);
@override
Future<void> onIdentify(OptimizeIdentity identity) =>
FirebaseAnalytics.instance.setUserId(id: identity.externalId);
@override
Future<void> onReset() => FirebaseAnalytics.instance.setUserId(id: null);
}
sdk.addForwarder('firebase', MyFirebaseForwarder());
FirebaseNaming.normalizeName/normalizeParams (also exported by the
SDK) handle Firebase's own naming rules — lowercase, [a-z0-9_], a
40-character cap, and a 25-parameter cap — so you don't reimplement
them.
A few things worth knowing:
- Every registered, enabled, ready forwarder receives
onEventin parallel with the backend call — one broken forwarder never blocks the backend or another forwarder. trackBackendOnlyevents skip forwarders entirely.sdk.setForwarderEnabled('firebase', false)is a runtime kill switch.- Optionally declare intent up front so misconfiguration is caught
early:
If a destination is flaggedOptimizeConfig( destinations: {Destination.firebase: true, Destination.meta: true}, ... )truebut never registered (or itsisReadyisfalse), the SDK logs a clear warning viaonLogon your first tracked event — instead of silently dropping data. - Session-replay/screen-tracking tools (e.g. Microsoft Clarity) don't
need the full forwarder interface — call their screen-name API
directly at your own
trackPageViewcall site instead.
Notification lifecycle tracking #
sdk.notifications.trackReceived(notificationId);
sdk.notifications.trackImpression(notificationId);
sdk.notifications.trackClicked(notificationId);
sdk.notifications.trackButtonAction(notificationId, buttonId);
sdk.notifications.trackDismissed(notificationId);
Wire these into your own FCM listeners:
FirebaseMessaging.onMessage.listen((message) async {
if (await sdk.inApp.handleForegroundFcm(message.data)) return; // handled as in-app
final id = OptimizeNotificationPayload.extractNotificationId(message.data);
sdk.notifications.trackReceived(id);
// ... render your own tray notification ...
});
FCM's background isolate doesn't have the GuidexrOptimize
singleton — construct a standalone tracker there instead:
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final id = OptimizeNotificationPayload.extractNotificationId(message.data);
OptimizeNotificationTracker(yourConfig).trackReceived(id);
}
Parsing helpers for the data payload:
OptimizeNotificationPayload.extractNotificationId(message.data);
OptimizeNotificationPayload.extractDeeplink(message.data);
OptimizeNotificationButton.parseAll(message.data); // action buttons
Notification inbox #
Data layer only — build your own list UI on top:
final page = await sdk.inbox.list(page: 1, pageSize: 20);
await sdk.inbox.setRead(notificationId, isRead: true);
await sdk.inbox.setReadBulk(notificationIds, isRead: true); // auto-chunked at 20
await sdk.inbox.clear(notificationId);
await sdk.inbox.clearAll();
In-app messages #
// On launch and on resume:
await sdk.inApp.evaluatePending();
// In your foreground FCM listener (see above) — returns true if the
// payload was an in-app message, whether or not it was displayed:
await sdk.inApp.handleForegroundFcm(message.data);
Given navigatorKey/onDeepLink in your config, matching messages are
presented automatically as one of four overlay types (full-screen,
modal, top banner, bottom banner), with impression/clicked/
button_action/dismissed tracking built in. You don't build any of this
UI yourself.
Offline delivery #
Failed events (transport error or a 5xx response) are queued and retried automatically — you don't need to do anything for this to work. 4xx responses are never retried, since the payload itself was rejected.
await sdk.flushQueue(); // optional manual trigger, e.g. from your own
// connectivity listener or app-lifecycle hook
There's no hard guarantee of delivery before the OS suspends your app —
that would need native background-task APIs, which this package
deliberately doesn't add. flushQueue() narrows the window; it doesn't
close it.
Adopting the SDK in an app with existing users #
If your app already tracks an anonymous id under its own storage,
wire legacyAnonymousIdProvider before shipping to your existing user
base — skipping this fires a spurious app_install for every
existing user on their first launch of the updated app, corrupting
install-count and retention data.
OptimizeConfig(
...
legacyAnonymousIdProvider: () => yourExistingAnonymousIdStorage.peek(),
)
This provider must be strictly read-only. Return null when
nothing exists yet; never create a value as a side effect. A
"get-or-create" style method (mints a value the first time it's called)
will return non-empty even for a genuinely new install, which the SDK
cannot distinguish from a real pre-existing value — silently suppressing
app_install forever, not just for your existing users. If your
existing service only exposes a get-or-create method, add a true
read-only variant and wire that instead.
Consulted exactly once — the very first init() call where the SDK's
own storage is still empty. Every launch after that reads the SDK's own
storage directly and never calls this again.
Example app #
See example/ for a runnable app covering initialization,
event tracking, and registration.