ChangelogEdit pageSTAFFOpen dashboard

Platform differences

One API on four platforms. Where names, calling style or behaviour differ between Android, iOS, Flutter and React Native, and why.

Android and iOS have the same functions, names and behaviour wherever the platform allows, written the way each language expects. Flutter and React Native are thin bridges over the two native SDKs: geofences, notifications, the event queue and consent all run natively, and keep working with the app closed. This page lists every place where they differ.

Install#

  • Android: Maven Central, implementation("tech.bubbl.sdk:bubbl-sdk:5.0.0").
  • iOS: Swift Package Manager, https://github.com/bubbl-public/bubbl-ios-sdk, product BubblSDK (and BubblNotificationService for a notification service extension). Not CocoaPods trunk, which goes read-only on 2 December 2026.
  • Flutter: pub.dev, bubbl_flutter_sdk: ^5.0.0. The plugin carries both native SDKs.
  • React Native: npm, @bubblsdk/react-native-sdk@5.0.0. It needs the New Architecture, and carries the iOS SDK in its pod.

The SDK installs on older versions than it runs on: it installs down to Android minSdk 23 and iOS 13.0, but runs only on Android 8.1 (API 27) and iOS 17 and later. Below that, start logs one line and does nothing else, every other call does nothing, and listeners never fire. See Requirements.

Calling style#

  • Android: suspend functions, each with a Bubbl.Callback<T> variant delivered on the main thread (for Java).
  • iOS: async functions, each with a completion variant that runs on the main thread.
  • Flutter: every call returns a Future, except addEventListener (a StreamSubscription) and isBubblMessage (a bool).
  • React Native: calls that only tell Bubbl something return nothing; calls that answer return a Promise. isSupported and isBubblMessage are synchronous.
  • permissions.status() is synchronous on Android and iOS, and asynchronous on Flutter and React Native.
  • isSupported is a synchronous property on Android, iOS and React Native, and a Future<bool> getter on Flutter.

Names that differ#

Bubbl.start(context, credential, options)Android only
Start with a device credential: an overload of start.
Bubbl.start(credential:options:)iOS only
Start with a device credential: an overload of start.
Bubbl.startWithCredential(credential, options: options)Flutter only
Start with a device credential. A separate name, because Dart has no overloads.
Bubbl.startWithCredential(credential, options)React Native only
Start with a device credential, named as on Flutter.
Bubbl.removeEventListener(listener)Android only
Removes a listener by reference.
Bubbl.removeEventListener(token)iOS only
Removes a listener by the BubblEventToken addEventListener returned: Swift closures have no identity.
Bubbl.removeEventListener(subscription)Flutter only
Cancels the StreamSubscription addEventListener returned.
subscription.remove()React Native only
There's no removeEventListener: call remove() on the Subscription addEventListener returned.
Bubbl.eventsFlutter only
A Stream<BubblEvent> of every event, besides addEventListener.
Bubbl.permissions.requestLocation(always = true)Android only
A named or positional boolean.
Bubbl.permissions.requestLocation(always: true)iOS, Flutter only
A labelled boolean.
Bubbl.permissions.requestLocation({ always: true })React Native only
An options object.
BubblMessage.fromJson(json)Android only
A message back from its json.
BubblMessage(json:)iOS only
A message back from its json (failable).
Bubbl.sdkVersioniOS only
The SDK's version as a static property. Elsewhere, read sdkVersion from diagnostics.
Bubbl.setPushToken(_:), Bubbl.willPresent(_:completionHandler:), Bubbl.didReceive(_:completionHandler:)iOS only
Manual push hooks, only with BubblAutoIntegrationEnabled set to NO. Flutter and React Native apps call them from native iOS code; React Native also has the Objective-C class BubblPush. Android needs none.

The same types have different names on some platforms:

  • Log levels: BubblOptions.LogLevel on Android (NONE…DEBUG) and iOS (.none….debug), BubblLogLevel on Flutter, LogLevel string values on React Native.
  • Question types: BubblQuestion.Type on Android, BubblQuestion.Kind on iOS (Type is taken in Swift; the property is still type), BubblQuestionType on Flutter, QuestionType on React Native. Choices are BubblQuestion.Choice on Android and iOS, and BubblChoice on Flutter.
  • Permission values: nested enums BubblPermissionStatus.Notifications and .Location on Android and iOS; BubblNotificationPermission and BubblLocationPermission on Flutter; string values on React Native.
  • Diagnostics: Bubbl.Diagnostics on Android and iOS, BubblDiagnostics on Flutter and React Native.
  • The test device result: Bubbl.TestDeviceResult on Android, BubblTestDeviceResult elsewhere. A Sandbox test device: Bubbl.TestDevice on Android, Bubbl.Diagnostics.TestDevice on iOS, BubblTestDevice on Flutter and React Native.
  • Events: BubblEvent.NotificationOpened on Android, .notificationOpened on iOS, BubblNotificationOpened on Flutter, { type: 'notification.opened' } on React Native. See BubblEvent.

Mistakes at start#

A mistake at start (an empty API key, an incomplete credential, a base URL that isn't allowed) throws IllegalArgumentException on native Android. On iOS it's logged and start does nothing. Flutter and React Native never throw: the mistake is logged natively.

Push#

  • Android: FCM, data-only messages. Bubbl receives every push itself, beside any FirebaseMessagingService of your app's, and draws the notification. Firebase comes from your google-services.json, or Bubbl starts it from the dashboard's. No push hooks to call.
  • iOS: APNs, straight from Bubbl, with no Firebase. iOS draws the alert without running your app. Bubbl asks for the device token only once notifications are allowed, and sends it with its APNs environment, read from the app's provisioning profile. It picks up the token and your notification delegate by itself; BubblAutoIntegrationEnabled set to NO in Info.plist turns that off.
  • iOS pictures: add a notification service extension whose class subclasses BubblNotificationService (its own product). It downloads the push's image (https only, at most 10 MB) and attaches it; anything that goes wrong shows the push without it. BubblNotificationService.isBubbl(_:) tells Bubbl's pushes apart there, and the overridable imageTimeout (20 seconds) sets how long the download may take. Flutter and React Native have their own pods for it.
  • isBubblMessage: Flutter and React Native check it in Dart or JavaScript, as bubbl equal to the string '1'. Native iOS also accepts the number 1 or true.

More in Push notifications.

When a notification counts as received#

  • Android: on delivery. The SDK receives every push, and each notification a geofence or a pull brings.
  • iOS: when the app first sees it: a push arriving with the app in front, a push tapped, a pull, or a geofence's notification. iOS draws alert pushes without running the app, so the received event can't come on delivery. The server counts delivery when Apple accepts the push.

The notification listener#

On Android and iOS the listener is asked about every notification, on the main thread. On Flutter and React Native it's asked only while the app is on screen. In the background, or with the app closed, Bubbl shows its own notification. If the Dart or JavaScript listener doesn't answer true (or throws), Bubbl shows it.

Geofences#

  • Watched at once: Android up to 50 from the server, plus the refresh circle. iOS up to 19 of the nearest, plus the refresh region: iOS allows 20 per app, shared with your app's own and other SDKs', so Bubbl takes what's left, and fewer once iOS says the app is over. Bubbl's iOS regions are named bubbl.….
  • In the background: Android uses Play services geofencing and WorkManager. iOS uses CLMonitor conditions and significant location changes, which relaunch the app.
  • Where the device can't watch them, iOS decides entering and leaving from location fixes, as with "While Using". osWatchesGeofences in diagnostics is then false.
  • Before the first unlock after a reboot: Android delivers geofence events once the phone is unlocked. On iOS they're lost, because nothing can be read yet; they're counted in transitionsDroppedWhileLocked.
  • "Always" location: Android 11 and later ask for "Allow all the time" in a separate second step; iOS goes from "While Using" to "Always". requestLocation with always asks for both steps on both.

More in Location and geofences.

Diagnostics extras#

  • Android only: backgroundRestricted and batteryOptimized.
  • iOS only: backgroundRefreshAvailable, lowPowerMode and osWatchesGeofences.
  • On Flutter and React Native, the other platform's extras are null.

See Diagnostics.

Credential storage#

  • Android: the Android Keystore (an HMAC key, not in backups).
  • iOS: the Keychain, available after first unlock and on this device only. Keychain items outlive the app, so a credential found without its install id after a reinstall is dropped, and the install registers as new, as on Android.

Local http base URLs#

Both native SDKs allow http:// only to localhost, 127.0.0.1, 10.0.2.2 and *.test hosts, for local development. On Android, your app's network security config must also allow cleartext traffic to that host. On iOS, App Transport Security blocks it unless your Info.plist sets NSAppTransportSecurity › NSAllowsLocalNetworking.