bearound_flutter_sdk 3.4.5
bearound_flutter_sdk: ^3.4.5 copied to clipboard
Bearound Flutter SDK - Beacon detection and proximity tracking
π» Bearound Flutter SDK #
Official Flutter plugin for the Bearound native SDKs β Android 3.4.5 Β· iOS 3.4.5.
Features #
- Native beacon scanning on Android and iOS
- Real-time beacon stream with metadata (battery, firmware, temperature)
- Sync lifecycle events (sync started/completed callbacks)
- Background detection events (beacons detected in background)
- Optional Android foreground service (
connectedDevice) for resilient background scanning - Scanning state and error streams
- User properties support for enriched beacon events
- Native permission handling - iOS uses
requestAlwaysAuthorization()directly (no blue GPS indicator) - Business token authentication with automatic app ID detection
- Automatic Bluetooth metadata collection and periodic scanning
Installation #
Add to your pubspec.yaml:
dependencies:
bearound_flutter_sdk: ^3.4.5
Run:
flutter pub get
Platform Setup #
Android #
Add JitPack to your root settings.gradle or build.gradle:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
You normally don't need to declare any permissions β the SDK declares them all and the Android manifest merger injects them into your app automatically. The SDK uses the connectedDevice foreground-service model (reading data from Bluetooth devices), not location.
β οΈ If you redeclare
BLUETOOTH_SCANin your own manifest, you must keep theneverForLocationflag. If any declaration omits it, the flag is dropped from the merged manifest and Google treats the app as deriving location:<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
For reference, the manifest merge injects the following into your app (this is the
plugin's actual manifest β android/src/main/AndroidManifest.xml):
<!-- Bluetooth (Android 11 and below) -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- Bluetooth (Android 12+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Location: legacy only β required for a BLE scan on API <= 30; not requested on API 31+ -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
<!-- Foreground service: connectedDevice (BLE) on Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<!-- Misc -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Advertising ID -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
What the manifest merge means for your Play Store listing
Know these before submitting to Google Play β they are discovered at review time otherwise:
AD_IDβ your Play Console Data Safety form must declare that the app collects the Advertising ID. Apps in the Families / Designed for Families program are not allowed to carry this permission at all β if that's your case, talk to Bearound before integrating.uses-feature android.hardware.bluetooth_le required="true"β the Play Store filters your app out of devices without BLE. Usually desirable for a beacon product, but be aware your listing's device reach shrinks.FOREGROUND_SERVICE_CONNECTED_DEVICEβ its presence in the merged manifest triggers a Play Console foreground-service declaration + demo video for apps targeting SDK 34+. The service itself only runs if you opt in viaforegroundScanConfig/enableForegroundScanning(); if you stay on the opportunistic mode, remove the permission withtools:node="remove"(snippet in Scan modes) and no declaration/video is needed.POST_NOTIFICATIONSβ declared by the SDK, but on Android 13+ you still need the runtime grant for the foreground-service notification to be visible (requestPermissions()already asks for it).
Minimum Android SDK: 23.
iOS #
Add the following to ios/Runner/Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>processing</string>
<string>bluetooth-central</string>
<string>remote-notification</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.bearound.sdk.sync</string>
<string>io.bearound.sdk.processing</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to detect nearby beacons.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to detect nearby beacons.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to detect nearby beacons in background.</string>
Important for terminated app detection:
fetchmode allows iOS to wake the app when beacons are detected- User must grant "Always" location permission (not "When In Use")
- User must enable "Background App Refresh" in device Settings
βΉοΈ The iOS SDK uses a two-eyes model (CoreLocation + CoreBluetooth), so location is used on iOS by design. The
neverForLocationstrategy applies to Android only.
Background tasks (BGTaskScheduler) β required
Without this wiring the SDK still works in foreground, but it silently degrades in background: the SDK's BGTasks (periodic sync + processing) never run and terminated-state uploads never finalize.
The SDK schedules two BGTasks β io.bearound.sdk.sync (app refresh) and
io.bearound.sdk.processing (longer execution). Both identifiers must be listed
in BGTaskSchedulerPermittedIdentifiers (snippet above), and the app must call
registerBackgroundTasks() before the app finishes launching β i.e. in your
AppDelegate, not from Dart. Wire it in ios/Runner/AppDelegate.swift (the
example/ app is the working reference):
import BearoundSDK
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
// Register the SDK's BGTasks BEFORE the app finishes launching.
BeAroundSDK.shared.registerBackgroundTasks()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Background fetch β iOS periodically wakes the app for a refresh.
override func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
BeAroundSDK.shared.performBackgroundFetch { success in
completionHandler(success ? .newData : .noData)
}
}
// Background URLSession β iOS relaunches the app to deliver completed
// beacon-upload transfers. Forward to the SDK so terminated-state uploads
// are finalized (without this they complete in nsurlsessiond but the app
// never processes the result).
override func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
BeAroundSDK.shared.handleBackgroundURLSessionEvents(
identifier: identifier,
completionHandler: completionHandler
)
}
}
Without the
BGTaskSchedulerPermittedIdentifiersentries,registerBackgroundTasks()cannot register the SDK's BGTasks and iOS will never grant them execution time.
Push notifications & background wakeup
The backend can trigger a background BLE scan via silent push, and iOS relaunches a closed
app when it enters a beacon region. On Flutter 3.41+ this requires three extra steps in
your app target β the example/ app is the working reference; copy from example/ios/Runner/.
1. Push entitlement β create ios/Runner/Runner.entitlements:
<key>aps-environment</key>
<string>development</string> <!-- or "production" -->
Reference it in the Runner build settings (all configs): CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
2. Disable the UIScene β in ios/Runner/Info.plist, rename UIApplicationSceneManifest
to _UIApplicationSceneManifest (keep the _ prefix β do not delete the block, Flutter's
migrator re-injects it every build). This drops the app back to the legacy AppDelegate life cycle,
which is what makes silent-push and beacon wakeup work. Keep UIMainStoryboardFile and
UILaunchStoryboardName as-is (removing them causes a black screen).
3. AppDelegate β legacy mode + forward the silent push to the SDK. Full file:
example/ios/Runner/AppDelegate.swift. Key points:
class AppDelegate: FlutterAppDelegate(noFlutterImplicitEngineDelegate) +GeneratedPluginRegistrant.register(with: self)- override the deprecated
application(_:didReceiveRemoteNotification:)(withoutfetchCompletionHandler) and callBeAroundSDK.shared.performBackgroundBLERefreshAndSync(...)β the modern variant is not delivered on Flutter (flutter#155479) requestAuthorization(...)+ setUNUserNotificationCenter.current().delegate = self
iOS rules that are NOT bugs:
- Silent push never wakes a force-quit app (Apple rule) β a closed app is relaunched by beacon/region wakeup, and only then processes the backend's silent pushes.
- Environment must match:
developmententitlement β APNs sandbox;productionβ APNs production. Wrong environment β APNs returnssentbut the device never gets it (BadDeviceToken).- Requires Background App Refresh ON on the device.
Minimum iOS version: 13.0.
Scan modes (Android) #
On iOS scanning is always system-managed (region monitoring +
BGTaskScheduler). These modes are Android-only.
The SDK ships two background-scan strategies, and you pick per app β right at startScanning(), with no native changes. Both already exist in the native SDK; you're just choosing which one to turn on.
At a glance β what you gain #
| πͺΆ Opportunistic (default) | π‘οΈ Foreground service | |
|---|---|---|
| Best for | casual presence, battery-first apps | real-time footfall, mission-critical presence |
| You gain | zero setup Β· no Play video Β· lowest battery | reliable detection that survives app-kill & aggressive OEMs |
| You accept | unpredictable latency Β· misses in deep background | persistent notification + Play demo video |
Quick pick #
- Can't (or won't) submit a Play demo video, and occasional detection is fine β πͺΆ Opportunistic
- Need real-time presence that survives the app being killed on Xiaomi/Huawei/Samsung β π‘οΈ Foreground service
- In between β Foreground service if you can submit the video; otherwise Opportunistic
Mode 1 β Opportunistic (no foreground service) Β· default #
What you gain: no FOREGROUND_SERVICE_CONNECTED_DEVICE permission, no Play demonstration video, and the lowest battery footprint.
await BearoundFlutterSdk.startScanning();
The OS delivers beacons through a PendingIntent scan (BluetoothScanReceiver), re-armed by AlarmManager (ScanWatchdogReceiver) β it keeps working with the app killed, but the system decides when (opportunistic, throttled).
To fully drop the Play video, also remove the FGS permission the native SDK injects via manifest merge:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"
tools:node="remove" />
Mode 2 β Foreground service (connectedDevice) #
What you gain: continuous, low-latency detection that survives app-kill and aggressive OEMs (Xiaomi/Huawei/Samsung) β the reliable path for footfall/presence analytics.
// By default the notification shows the host app's own name (localized by the
// device) + a generic, localized subtitle ("Atualizando conteΓΊdo" / "Updating
// content") β nothing about Bluetooth or reading data. Just enable it:
await BearoundFlutterSdk.startScanning(
foregroundScanConfig: const ForegroundScanConfig(),
);
// Want a custom title/subtitle instead? Pass them explicitly:
// await BearoundFlutterSdk.startScanning(
// foregroundScanConfig: const ForegroundScanConfig(
// notificationTitle: 'My App', notificationText: 'Bluetooth active',
// ),
// );
await BearoundFlutterSdk.disableForegroundScanning(); // stop the service
β οΈ Google Play: the
connectedDeviceforeground service requires a Play Console declaration + demonstration video. In the video/declaration, frame the feature as reading data from external Bluetooth devices β never as location or proximity (stays consistent withneverForLocation). The persistent notification itself just shows the app name, which is enough to satisfy the perceptibility requirement.
Trade-off #
| πͺΆ Opportunistic | π‘οΈ Foreground service | |
|---|---|---|
| App in foreground | continuous | continuous |
| App in background | opportunistic, throttled | continuous |
| App killed / swiped away | relaunched by OS (PendingIntent) | process kept alive |
| Aggressive OEM (Xiaomi/Huawei) | β killed | β survives |
| Detection latency | unpredictable (s β min) | low (per ScanPrecision) |
| Presence accuracy | low / medium | high |
| Battery | lower | higher |
| Persistent notification | none | yes (mandatory) |
| Extra permission | none | FOREGROUND_SERVICE_CONNECTED_DEVICE |
| Google Play video | β not required | β required |
Scan windows (detection cadence) #
ScanPrecision governs the duty cycle while the scan is active (foreground or FGS):
| Strategy | Typical detection window | Predictable? | Battery |
|---|---|---|---|
FGS + ScanPrecision.high |
~1β5 s, continuous | yes | high |
FGS + ScanPrecision.medium |
~10β20 s (10 s scan + 10 s pause Γ3 / 60 s) | yes | medium |
FGS + ScanPrecision.low |
~60 s (10 s scan + 50 s pause / 60 s) | yes | low |
| Opportunistic (no FGS) | seconds β several minutes (OS-decided) | no | low |
| WorkManager (client-side, external) | β₯ 15 min (WorkManager minimum) | yes | very low |
Windows are approximate, derived from the
ScanPrecisionduty cycle. In opportunistic modeScanPrecisiononly governs cadence while the app is in the foreground; in the background the OS throttles thePendingIntentscan.
Advanced: WorkManager (client-side) #
The SDK does not bundle WorkManager. For a predictable low-frequency sweep without a foreground service, schedule your own PeriodicWorkRequest (minimum interval 15 min) that calls startScanning() for a short window and then stopScanning(). This trades latency for battery and avoids the Play video β presence lags by up to the chosen period.
Background reliability (Android) #
Doze and OEM battery managers (Xiaomi/Huawei/Oppo/Vivo/OnePlusβ¦) are the #1 cause of "it stopped detecting in background" on Android β they kill or freeze the process regardless of scan mode. Since 3.4.5 the plugin exposes the native helpers that mitigate both:
| Method | What it does |
|---|---|
isIgnoringBatteryOptimizations() |
true if the app is already exempt from battery optimization (Doze). |
openBatteryOptimizationSettings() |
Opens the battery-optimization Settings screen so the user can exempt the app. Uses the Settings screen β not the restricted REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission β so it has no Google Play review impact. Returns true if the screen opened. |
isAutostartManageable() |
true if the device is from an OEM with a known autostart/protected-apps screen (Xiaomi/Huawei/Oppo/Vivo/OnePlus/Letv). false on stock Android (Pixel). |
openManufacturerAutostartSettings() |
Opens the manufacturer's autostart/protected-apps screen when one exists. Returns true if the screen opened. |
Recommended flow β check β explain β open Settings. Never deep-link the user to a Settings screen without telling them why first:
Future<void> ensureBackgroundReliability() async {
// 1. Doze exemption.
if (!await BearoundFlutterSdk.isIgnoringBatteryOptimizations()) {
// Show your own rationale dialog first ("we need this to keep detecting
// nearby devices in background"), then:
await BearoundFlutterSdk.openBatteryOptimizationSettings();
}
// 2. OEM autostart / protected apps (Xiaomi, Huawei, ...).
if (await BearoundFlutterSdk.isAutostartManageable()) {
// Rationale dialog, then:
await BearoundFlutterSdk.openManufacturerAutostartSettings();
}
}
iOS: all four methods are safe to call unconditionally β there is no equivalent restriction, so
isIgnoringBatteryOptimizations()resolvestrueand the other three are no-ops resolvingfalse.
Quick Start #
import 'package:bearound_flutter_sdk/bearound_flutter_sdk.dart';
Future<void> setupSdk() async {
final permissionsOk = await BearoundFlutterSdk.requestPermissions();
if (!permissionsOk) {
return;
}
await BearoundFlutterSdk.configure(
businessToken: 'your-business-token-here',
scanPrecision: ScanPrecision.high, // high | medium | low (default: high)
maxQueuedPayloads: MaxQueuedPayloads.medium, // small | medium | large | xlarge
);
// Note: appId is automatically extracted from the app's package/bundle identifier
// Bluetooth metadata and periodic scanning are automatic
// Listen to beacons
BearoundFlutterSdk.beaconsStream.listen((beacons) {
for (final beacon in beacons) {
print('${beacon.major}.${beacon.minor} - RSSI ${beacon.rssi}');
}
});
// Listen to sync lifecycle
BearoundFlutterSdk.syncLifecycleStream.listen((event) {
if (event.isStarted) {
print('Sync started with ${event.beaconCount} beacons');
} else if (event.isCompleted) {
// `success` is a bool? (null on 'started' events) β compare explicitly.
print('Sync completed: ${event.success == true ? "success" : "failed"}');
}
});
// Listen to background detections
BearoundFlutterSdk.backgroundDetectionStream.listen((event) {
print('Background: ${event.beaconCount} beacons detected');
});
// Other streams
BearoundFlutterSdk.scanningStream.listen((isScanning) {
print('Scanning: $isScanning');
});
BearoundFlutterSdk.errorStream.listen((error) {
print('SDK error: ${error.message}');
});
await BearoundFlutterSdk.startScanning();
}
User Properties #
await BearoundFlutterSdk.setUserProperties(
const UserProperties(
internalId: 'user-123',
email: 'user@example.com',
name: 'John Doe',
customProperties: {'plan': 'premium'},
),
);
Push Token #
Forward the device push token to the native SDK so the backend can target the
device (silent-push wake-up). The native SDK associates it with the stable
deviceId and registers it with the backend β immediately if scanning is
already active, otherwise on the next sync.
Call setPushToken explicitly on both platforms:
- Android (plugin β₯ 3.4.1): pass the FCM token. The bridge forwards it
to the native SDK (
BeAroundSDK.setPushToken, available since native Android 3.4.0). - iOS: pass the raw APNs token β the backend delivers pushes through APNs, so the FCM token does not work here. The SDK does try to capture the APNs token automatically via an AppDelegate swizzle, but that capture fails when Firebase is present (Firebase intercepts the swizzle β the most common setup), and the token silently ends up NULL in the backend. Forwarding the raw token explicitly works in every setup, and the call is idempotent.
import 'dart:io' show Platform;
if (Platform.isAndroid) {
// Android: FCM token (e.g. via firebase_messaging).
final token = await FirebaseMessaging.instance.getToken();
if (token != null) await BearoundFlutterSdk.setPushToken(token);
} else if (Platform.isIOS) {
// iOS: RAW APNs token β NOT the FCM token.
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken != null) await BearoundFlutterSdk.setPushToken(apnsToken);
}
If you opted out of the SDK's AppDelegate swizzle (
BearoundAppDelegateProxyEnabled = NOinInfo.plist), callingsetPushTokenon iOS is not just recommended β it's the only way the token reaches the backend.
API Summary #
Full cross-platform event/field parity matrix: EVENT-PARITY.md.
Methods #
| Method | Platform | Notes |
|---|---|---|
configure({businessToken, scanPrecision, maxQueuedPayloads}) |
Android + iOS | Required before startScanning(). Defaults: ScanPrecision.high, MaxQueuedPayloads.medium. |
startScanning({foregroundScanConfig}) |
Android + iOS | foregroundScanConfig is Android-only (ignored on iOS). |
stopScanning() |
Android + iOS | |
isScanning() |
Android + iOS | |
requestPermissions() |
Android + iOS | iOS: native requestAlwaysAuthorization(); Android: runtime permissions via permission_handler (incl. notifications on 13+). |
checkPermissions() |
Android + iOS | |
requestLocationAuthorization({level}) |
iOS | Unlocks the Location eye (terminated-app wake-up requires always). No-op on Android. |
isIgnoringBatteryOptimizations() |
Android | iOS always resolves true (no equivalent restriction). |
openBatteryOptimizationSettings() |
Android | iOS: no-op, resolves false. |
isAutostartManageable() |
Android | false on stock Android and iOS. |
openManufacturerAutostartSettings() |
Android | iOS/stock: no-op, resolves false. |
setUserProperties(UserProperties) |
Android + iOS | |
clearUserProperties() |
Android + iOS | |
setPushToken(String) |
Android + iOS | FCM token on Android, raw APNs token on iOS β see Push Token. |
getSdkVersion() |
Android + iOS | Android returns BuildConfig.SDK_VERSION (injected at build time); '' only if the native layer is unavailable. |
getCurrentScanPrecision() |
Android + iOS | '' before configure(). |
getBleDiagnosticInfo() |
iOS | Android returns ''. |
getPendingBatchCount() |
Android + iOS | Batches queued for retry after API failures. |
isConfigured() |
Android + iOS | iOS: tracked by the bridge β may read false after an auto-restored background relaunch even while isScanning() is true. |
isLocationAvailable() |
Android + iOS | Device location services on/off. |
getAuthorizationStatus() |
Android + iOS | iOS: mirrors CLAuthorizationStatus. Android: derived from location permissions only β it does not reflect the BLE-scan gate on 12+; prefer getBluetoothState(). |
getBluetoothState() |
Android + iOS | poweredOn/poweredOff/unauthorized/... β on Android 12+, unauthorized means BLUETOOTH_SCAN (Nearby devices) is missing. |
getPersistedLog() / getPersistedLogRaw() |
iOS | Persisted detection log. Android returns [] (native SDK has no persisted log). |
clearPersistedLog() |
iOS | No-op on Android. |
enableForegroundScanning([ForegroundScanConfig]) |
Android | iOS: no-op. |
disableForegroundScanning() |
Android | iOS: no-op. |
isForegroundScanningEnabled() |
Android | iOS always resolves false. |
setForegroundNotificationContent(NotificationContent) |
Android | iOS: no-op. |
setErrorReportingEnabled(bool) |
Android + iOS | Opt out of the Dart-layer error telemetry (default: on). Native crash capture is independent. |
setDebugNotificationsEnabled(bool) |
iOS | QA aid: visible local notification per silent-push scan (default: off β keep off in production). Silent no-op on Android. Also settable via configure(debugNotifications:). |
Streams #
| Stream | Platform | Emits |
|---|---|---|
beaconsStream |
Android + iOS | List<Beacon> β detected beacons with metadata (battery, firmware, temperature). |
scanningStream |
Android + iOS | bool β scanning state changes. |
errorStream |
Android + iOS | BearoundError β SDK errors. Replays up to 16 errors emitted before the first listener (buffer arms on first errorStream access); subscribing before startScanning() is still recommended. |
syncLifecycleStream |
Android + iOS | SyncLifecycleEvent β sync started/completed. |
backgroundDetectionStream |
Android + iOS | BackgroundDetectionEvent β beacon count detected in background. |
beaconRegionStream |
Android + iOS | BeaconRegionEvent β beacon-region enter/exit. |
activeScanStream |
Android + iOS | ActiveScanEvent β active scan (ranging + BLE) on/off. |
bluetoothZoneStream |
iOS | BluetoothZoneEvent β Bluetooth-eye zone enter/exit (two-eyes model). Never emits on Android. |
bluetoothScanModeStream |
iOS | BluetoothScanModeEvent β BLE scanner duty cycle (idle/active). Never emits on Android. |
bluetoothStateStream |
Android + iOS | BluetoothState β Bluetooth adapter state (emits current state on subscribe). |
Configuration Enums #
- ScanPrecision:
high(continuous scan, sync every 15s β plugin default),medium(10s scan + 10s pause per 60s),low(10s scan + 50s pause per 60s) - MaxQueuedPayloads:
small(50),medium(100 β default),large(200),xlarge(500)
Troubleshooting #
| Symptom | Check | Fix |
|---|---|---|
| No beacons detected (any platform) | Is it a Bearound beacon? The SDK only detects beacons advertising the proprietary 0xBEAD service data β a generic iBeacon, or an iPhone simulating an iBeacon, is filtered out by design (Android scan filter accepts only 0xBEAD). |
Test with a real Bearound beacon. |
| No beacons detected | await BearoundFlutterSdk.isConfigured() returns false. |
Call configure() (and await it) before startScanning(). |
| No beacons on Android 12+ | getBluetoothState() returns unauthorized β "Nearby devices" (BLUETOOTH_SCAN) was denied. Granting location does NOT unlock BLE scan on 12+. |
Request the Nearby devices permission (requestPermissions() does it) or send the user to app settings. |
| No beacons on iOS | getBluetoothState() β poweredOn, or getAuthorizationStatus() is denied/notDetermined. |
Turn Bluetooth on; call requestPermissions() (Location "Always" is required for terminated-app wake-up). |
| Detection stops in background (Android) | Battery optimization active or OEM battery killer (Xiaomi/Huawei/β¦): isIgnoringBatteryOptimizations() / isAutostartManageable(). |
Follow the Background reliability flow; for mission-critical presence use the foreground-service mode. |
| Detection stops in background (iOS) | Background App Refresh disabled, or BGTaskSchedulerPermittedIdentifiers missing from Info.plist. |
Enable Background App Refresh in device Settings; add both BGTask identifiers + registerBackgroundTasks() (iOS setup). |
| Errors never show up | errorStream accessed only after the error was emitted β the replay buffer (16 entries) only arms on the first access to the getter. |
Access/subscribe to errorStream before calling startScanning(); errors emitted after the buffer armed are replayed to the first listener. |
| Push token NULL in the backend / silent push never arrives (iOS) | Firebase present β the SDK's automatic APNs capture fails (swizzle intercepted). | Always call setPushToken with the raw APNs token (Push Token). |
| App not woken when terminated (iOS) | Force-quit apps are never woken by silent push (Apple rule); relaunch happens via beacon-region wakeup and requires Location "Always" + the legacy AppDelegate setup. | Follow Push notifications & background wakeup; verify the aps-environment matches the APNs environment (sandbox vs production). |
Error telemetry #
The SDK ships lightweight, isolated crash telemetry so we can spot and fix integration issues quickly. It works on two layers:
- Native (Android/iOS): the embedded native SDKs already capture native
crashes via their own
ErrorReporters. Nothing to configure. - Dart (this plugin): installed automatically on
configure(), it chainsFlutterError.onErrorandPlatformDispatcher.onErrorand reports only uncaught errors whose first application frame is inpackage:bearound_flutter_sdkβ i.e. errors that originate in the plugin. Errors from your app or third-party packages are never touched, including errors thrown inside your own callbacks that merely pass through the SDK.
It is designed to be invisible and safe:
- Never breaks your app. The global handlers are always chained β the
previous
FlutterError.onErroris kept and still invoked, andPlatformDispatcher.onErrorreturnsfalseso the platform still sees the error. Nothing is swallowed or hijacked. - Fire-and-forget delivery to
POST https://ingest.bearound.io/sdk-errorswith short (5 s) timeouts, an in-memory rate limit (20/h) and 5-minute dedupe. Usesdart:io HttpClientβ no extra dependency. - Minimal payload: error type/message/stack (capped at 8000 chars), a permission snapshot (Bluetooth/location/notifications, read without prompting), the platform, and a UTC timestamp. No PII.
Opt out at any time (default is on):
BearoundFlutterSdk.setErrorReportingEnabled(false);
Migration Guide #
From 2.4.x to 3.3.x #
foregroundScanInterval/backgroundScanIntervalwere replaced by a singlescanPrecision(ScanPrecision.high/medium/low).- Native SDKs bumped to 3.3.1 (Android + iOS).
- Android now uses the
connectedDeviceforeground-service model withBLUETOOTH_SCAN(neverForLocation) β no background-location permission.
Before:
await BearoundFlutterSdk.configure(
businessToken: 'token',
foregroundScanInterval: ForegroundScanInterval.seconds30, // β removed
backgroundScanInterval: BackgroundScanInterval.seconds90, // β removed
);
After:
await BearoundFlutterSdk.configure(
businessToken: 'token',
scanPrecision: ScanPrecision.high,
maxQueuedPayloads: MaxQueuedPayloads.medium,
);
From 2.1.0 to 2.2.0 #
Simplified API: Bluetooth metadata and periodic scanning are now automatic.
await BearoundFlutterSdk.configure(
businessToken: 'token',
// β
Bluetooth metadata: always enabled
// β
Periodic scanning: automatic (FG: enabled, BG: continuous)
);
// Listen to sync lifecycle events
BearoundFlutterSdk.syncLifecycleStream.listen((event) {
if (event.isStarted) print('Sync started');
if (event.isCompleted) print('Sync completed: ${event.success}');
});
// Listen to background detections
BearoundFlutterSdk.backgroundDetectionStream.listen((event) {
print('${event.beaconCount} beacons detected in background');
});
From 1.x to 2.x #
startScan(clientToken)was replaced byconfigure()+startScanning().- Backup size, region events, and legacy sync success/error events were removed.
- Use
syncLifecycleStreamfor sync events anderrorStreamfor failures.