booleanmaths_flutter_sdk
Flutter plugin for the BooleanMaths SDK — user event tracking and attribution.
The plugin is a thin bridge: Dart calls travel over a single MethodChannel
(com.booleanmaths/sdk_channel) to the native SDK, which owns all buffering,
persistence and syncing.
| Dart package | booleanmaths_flutter_sdk |
| Android package | com.booleanmaths.flutter |
| Android dependency | com.booleanmaths:bm-sdk:1.0.13 (Maven Central) |
| iOS dependency | BooleanMathsSDK ~> 1.2 (CocoaPods Trunk) |
Platform support
| Platform | Minimum | Notes |
|---|---|---|
| Android | minSdk 24 | Full support, including deep-link and notification attribution. |
| iOS | 15.0 | Full event tracking. No attribution data — see Attribution. |
Calls are safe no-ops on any other platform, so you can instrument shared UI
code once and run it everywhere. Gate on BooleanMaths.isSupported when the
surrounding work is itself worth skipping.
Install
flutter pub add booleanmaths_flutter_sdk
or add it by hand:
dependencies:
booleanmaths_flutter_sdk: ^0.2.1
Android needs no extra Gradle configuration; the native SDK comes from Maven Central automatically.
That includes release builds with isMinifyEnabled — you need no ProGuard
keep rules of your own. Two sets are applied automatically: bm-sdk 1.0.13's
own consumer rules, and this plugin's (android/consumer-rules.pro), which
keeps androidx.work.InputMerger constructors that R8 full mode would otherwise
strip. Both were release-only failures that produced no error in a release
build, for different reasons:
| Missing rule | Symptom |
|---|---|
bm-sdk ≤ 1.0.12 |
Events uploaded and returned 200, but field names were obfuscated into the JSON keys, so nothing was readable server side. |
InputMerger constructor |
EventWorker failed to start, so events were persisted and never dispatched at all. |
If you added keep rules for com.booleanmaths.sdk or androidx.work as a
workaround, they are now redundant but harmless.
iOS requires CocoaPods. BooleanMathsSDK is distributed as a CocoaPods-only
vendored XCFramework, so this plugin ships a podspec and no Package.swift.
Apps with Swift Package Manager enabled still build — Flutter falls back to
CocoaPods for this plugin — but they cannot drop CocoaPods entirely. Your
Podfile must declare 15.0 or higher:
platform :ios, '15.0'
Usage
import 'package:booleanmaths_flutter_sdk/booleanmaths_flutter_sdk.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
Future<void> main() async {
// Required when initializing before runApp(): the plugin talks over a
// MethodChannel, which needs the bindings in place first.
WidgetsFlutterBinding.ensureInitialized();
// Initialize once, before tracking anything.
await BooleanMaths.initialize(
apiKey: 'your-api-key',
pixelId: 'your-pixel-id',
// Keeps your own testing out of production reporting. `isDebug` defaults
// to false, so pass this explicitly — see Debug vs production below.
isDebug: kDebugMode,
);
runApp(const MyApp());
}
// Record behaviour anywhere in the app.
await BooleanMaths.trackEvent('AddToCart', properties: {
'sku': 'ABC-1',
'value': 499.0,
'quantity': 2,
'currency': 'INR',
'in_stock': true,
});
initialize can throw — see Errors. Do not await it unguarded in
main() unless you are willing for a bad key to take start-up down with it.
API
| Dart | Android | iOS |
|---|---|---|
initialize(apiKey:, pixelId:, isDebug:) |
BooleanMathsSDK.initialize(context, …) |
BooleanMaths.shared.initialize(…) |
trackEvent(name, properties:) |
BooleanMathsSDK.trackEvent(…) |
BooleanMaths.shared.track(…) |
handleIntent() |
BooleanMathsSDK.handleIntent(intent) |
no-op |
flush({timeout}) |
returns false — no flush exists |
BooleanMaths.shared.flush(…) |
getHelloMessage() |
BooleanMathsSDK.getHelloMessage() |
BooleanMaths.getHelloMessage() |
isSupported |
— | — |
Automatic events
initialize starts the native SDK's own tracking. These arrive without any
trackEvent call of your own, so avoid hand-rolling duplicates:
| Event | When | Android | iOS |
|---|---|---|---|
FirstOpen |
Once per install | ✅ with Play Install Referrer | ✅ |
app_opened |
Every launch | ✅ | ✅ |
DeepLinkClick |
ACTION_VIEW intent handled |
✅ | ❌ |
NotificationClick |
Other intent carrying campaign data | ✅ | ❌ |
FirstOpen is PascalCase on the wire deliberately — backend install reporting
keys off that exact string.
Do not depend on the order of FirstOpen relative to app_opened. On Android
FirstOpen waits for the Play Install Referrer callback, so it is usually
emitted a few hundred milliseconds after app_opened.
Attribution
Android works automatically and needs nothing from your app. The plugin is
ActivityAware: it forwards the launch intent during initialize and registers
a NewIntentListener for every intent that arrives afterwards. Deep links and
notification taps are attributed on both cold and warm start.
You do not need a MainActivity override — Flutter's
FlutterActivity.onNewIntent already calls setIntent before dispatching to
plugins. Either launchMode of singleTop or singleTask works.
Campaign data from a handled intent is persisted natively and attached to every
subsequent event as data.attribution.
BooleanMaths.handleIntent() exists for the rare case where you want to force a
re-read. It is safe to call at any time: the SDK de-duplicates intents it has
already seen, and ignores a plain launcher tap rather than attributing it.
iOS emits no attribution data. The native iOS SDK has no intent concept;
handleIntent() is a no-op there and logs a notice in debug builds.
A spurious NotificationClick while developing
The native SDK ignores a launch intent only when all of these hold: the action
is ACTION_MAIN, the categories contain CATEGORY_LAUNCHER, there are no
extras, and data is null. A real launcher tap satisfies all four and is
correctly ignored.
flutter run and the integration_test harness both add extras to the launch
intent (enable-dart-profiling, enable-checked-mode, …), which breaks the
"no extras" condition — so a debug launch is attributed and emits a
NotificationClick whose campaign data is just those Flutter flags. This does
not happen for a normal launcher tap, on debug or release builds. Verify
attribution behaviour with a plain launch rather than one started by the
tooling:
adb shell am start -n your.package/.MainActivity \
-a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Event properties
properties accepts any value the platform message codec supports — String,
num, bool, List, Map. The native side normalizes them before handing
them to the SDK:
- Null values and non-string keys are dropped, so one bad property never costs the whole event. Nulls inside a list are preserved, because dropping one would shift the index of everything after it.
- Non-finite doubles (
double.nan,double.infinity) are dropped. On iOS these failJSONSerializationand would silently discard the event — and the batch it ships in. - Whole-valued doubles that convert exactly are sent as integers, so a
quantityof2.0is handed to the SDK as2, not2.0.
Known limitation (Android). The last point currently has no visible effect on the wire.
bm-sdkpersists each event to storage and re-reads it before dispatch, and that round-trip widens every JSON number back to a double — so a Dartintof2still arrives at the backend as2.0. This happens downstream of the plugin (anintthat the plugin never touches is widened too), so it needs a fix inbm-sdkrather than here. The normalization is kept because it is correct and will take effect once that round-trip preserves integers.
Debug vs production
isDebug marks events as development rather than production traffic so test
data stays out of your reporting.
It defaults to false — matching the native Android and iOS SDKs. An app
that says nothing reports production, including in debug builds. Opt in
explicitly:
await BooleanMaths.initialize(
apiKey: apiKey,
pixelId: pixelId,
isDebug: true, // this traffic is development
);
The default is deliberately not tied to the build mode, but following the build mode is what most apps want, so pass it explicitly — this is what the quick-start and the example app both do:
isDebug: kDebugMode, // debug build = someone testing; release = a real user
Written at the call site, the choice stays visible instead of hiding in a
default. That matters because kDebugMode does not describe every case: a
staging flavour that ships as a release build is release-mode but should
still report as development. Use a flavour constant there rather than
kDebugMode.
⚠️ Because the default is false, an app that passes nothing sends local test
traffic to production reporting. Set this before you start instrumenting.
The flag is recorded per event at queue time, not at send time, so an event
queued with isDebug: true still reports development if a later build
delivers it.
Flushing
flush forces a dispatch attempt before a known interruption, such as a
checkout hand-off where the user may not come back.
final bool dispatched = await BooleanMaths.flush(
timeout: const Duration(seconds: 10),
);
iOS only. On Android it returns false immediately without doing anything,
because bm-sdk persists to Room and syncs through WorkManager, which owns the
timing. A false result means "no flush was performed" — never that an event
was lost. Queued events are durable and retried on the next launch on both
platforms, so it is safe to proceed regardless of the answer.
Errors
Failures surface as PlatformException:
| Code | Meaning |
|---|---|
invalid_argument |
A blank/missing apiKey, pixelId or eventName. |
sdk_error |
The native SDK threw; message carries its reason. |
try {
await BooleanMaths.initialize(apiKey: apiKey, pixelId: pixelId);
} on PlatformException catch (e) {
debugPrint('BooleanMaths init failed: ${e.code} ${e.message}');
}
A platform with no native implementation answers with MissingPluginException,
which the Dart layer swallows as a no-op rather than taking the host app down.
Verifying the bridge
getHelloMessage() answers from the native SDK itself, so a non-null result
proves the native artifact linked — not merely that the plugin registered.
Useful when events silently go nowhere.
debugPrint(await BooleanMaths.getHelloMessage() ?? 'native SDK not linked');
Example app
cd example
flutter run --dart-define=BM_API_KEY=your-key --dart-define=BM_PIXEL_ID=your-pixel
Initializes the SDK, sends events with typed properties, exercises flush and
handleIntent, and logs each result on screen.
Releasing
wrapper_version reaches the backend from lib/src/version.dart, which is
generated from pubspec.yaml. After a version bump:
dart run tool/sync_version.dart
test/version_test.dart fails if the two drift, so a forgotten regeneration
breaks the suite rather than shipping a wrong version. Keep s.version in
ios/booleanmaths_flutter_sdk.podspec in step as well.
Tests
flutter test # Dart unit tests
cd example/android && ./gradlew :booleanmaths_flutter_sdk:testDebugUnitTest
cd example && flutter test integration_test # against a device/emulator