bearound_flutter_sdk 1.3.1
bearound_flutter_sdk: ^1.3.1 copied to clipboard
Bearound Flutter SDK
π» Bearound Flutter SDK #
Official Flutter plugin for integrating Bearound's secure BLE beacon detection and indoor location technology.
β¨ Features #
- π― BLE Beacon Scanning: High-performance beacon detection for iOS and Android
- π Real-time Event Streams: Live beacon detection, sync status, and region monitoring
- βοΈ Configurable Scanning (v1.3.1+): Adjustable scan intervals (5-60s) and backup sizes (5-50 beacons) for optimal performance
- ποΈ Dynamic Configuration: Change scan frequency based on battery level or network conditions
- π‘οΈ Cross-platform: Unified API for iOS and Android with native performance
- π Secure: Built-in token-based authentication and encrypted communication
- ποΈ Permission Management: Automatic handling of location and Bluetooth permissions
- π± Background Support: Continue scanning even when app is in background
- π State Synchronization: Automatic UI sync when app reopens
- π Distance Estimation: Real-time distance calculation to nearby beacons
- π Battery Optimization: Smart filtering and configurable intervals for extended battery life
- π Smart Filtering: Automatically filters invalid beacons (RSSI = 0)
- π§ͺ Well Tested: Comprehensive unit test suite with 25+ test cases
- π Type Safe: Full null-safety support and comprehensive documentation
π¦ Installation #
Add to your pubspec.yaml:
dependencies:
bearound_flutter_sdk: ^1.3.1
Install the package:
flutter pub get
What's New in v1.3.1 #
- βοΈ Configurable Scan Intervals (5-60 seconds) - Balance between battery and detection speed
- πΎ Configurable Backup Sizes (5-50 beacons) - Control failed beacon storage
- π§ Runtime Configuration - Adjust settings dynamically based on battery or network conditions
- π¨ Settings UI Example - Complete configuration screen in example app
- π Smart Beacon Filtering - Automatically filters invalid beacons (RSSI = 0)
- π iOS Bug Fix - Fixed critical initialization issue preventing beacon detection
- π Improved iOS Architecture - Async/await pattern for reliable permission handling
- π± Native SDK Updates - iOS 1.3.1 and Android 1.3.1 with modular architecture
Important iOS Fix: This version resolves a critical bug where beacons were not being detected on iOS. The SDK now properly waits for permissions before starting services, ensuring reliable beacon detection.
See CHANGELOG.md for complete release notes.
βοΈ Platform Setup #
Android Configuration #
1. Project Settings
Important: Add the JitPack repository to your android/settings.gradle.kts file:
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
This configuration is required for the SDK's native Android dependencies to work properly during APK builds.
2. Permissions
Add the following permissions to android/app/src/main/AndroidManifest.xml:
<!-- Required permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Android 12+ (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Background scanning -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
iOS Configuration #
Add the following to ios/Runner/Info.plist:
<!-- Background modes -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>location</string>
</array>
<!-- Permission descriptions -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to detect nearby beacons for location services.</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, even in background.</string>
<key>NSUserTrackingUsageDescription</key>
<string>This app needs tracking permission for beacon detection on iOS 14+.</string>
Note: Requires iOS 13.0+ for optimal performance. Background scanning requires additional iOS configuration.
π Quick Start #
Basic Usage #
import 'package:bearound_flutter_sdk/bearound_flutter_sdk.dart';
class BeaconScanner extends StatefulWidget {
@override
_BeaconScannerState createState() => _BeaconScannerState();
}
class _BeaconScannerState extends State<BeaconScanner> {
bool _isScanning = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Beacon Scanner')),
body: Column(
children: [
ElevatedButton(
onPressed: _isScanning ? _stopScanning : _startScanning,
child: Text(_isScanning ? 'Stop Scanning' : 'Start Scanning'),
),
Expanded(
child: Center(
child: Text(_isScanning ? 'Scanning for beacons...' : 'Press to start'),
),
),
],
),
);
}
Future<void> _startScanning() async {
// Request permissions first
final granted = await BearoundFlutterSdk.requestPermissions();
if (!granted) {
print('Permissions not granted');
return;
}
// Start scanning with your client token
await BearoundFlutterSdk.startScan(
'your-client-token-here',
debug: true, // Enable debug logs
);
setState(() => _isScanning = true);
}
Future<void> _stopScanning() async {
await BearoundFlutterSdk.stopScan();
setState(() => _isScanning = false);
}
@override
void dispose() {
if (_isScanning) {
BearoundFlutterSdk.stopScan();
}
super.dispose();
}
}
Permission Handling Example #
class PermissionManager {
static Future<bool> checkAndRequestPermissions() async {
try {
final granted = await BearoundFlutterSdk.requestPermissions();
return granted;
} catch (e) {
print('Error requesting permissions: $e');
return false;
}
}
static void showPermissionDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Permissions Required'),
content: Text(
'This app needs location and Bluetooth permissions to detect beacons. '
'Please grant the required permissions in the next dialog.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
await checkAndRequestPermissions();
},
child: Text('Grant Permissions'),
),
],
),
);
}
}
Real-time Event Streams #
The SDK provides three event streams for real-time monitoring of beacon activity:
class BeaconMonitor extends StatefulWidget {
@override
_BeaconMonitorState createState() => _BeaconMonitorState();
}
class _BeaconMonitorState extends State<BeaconMonitor> {
List<Beacon> _beacons = [];
String _syncStatus = 'Waiting...';
String _regionStatus = 'Outside region';
StreamSubscription<BeaconsDetectedEvent>? _beaconsSubscription;
StreamSubscription<BeaconEvent>? _syncSubscription;
StreamSubscription<BeaconEvent>? _regionSubscription;
@override
void initState() {
super.initState();
_startListening();
}
void _startListening() {
// Listen to beacon detection events
_beaconsSubscription = BearoundFlutterSdk.beaconsStream.listen((event) {
setState(() {
_beacons = event.beacons;
});
print('Beacons detected (${event.eventType.name}): ${event.beacons.length}');
for (var beacon in event.beacons) {
print(' - UUID: ${beacon.uuid}, Major: ${beacon.major}, Minor: ${beacon.minor}');
print(' RSSI: ${beacon.rssi} dBm, Distance: ${beacon.distanceMeters?.toStringAsFixed(2)}m');
}
});
// Listen to API sync events
_syncSubscription = BearoundFlutterSdk.syncStream.listen((event) {
if (event is SyncSuccessEvent) {
setState(() {
_syncStatus = 'Success: ${event.beaconsCount} beacons synced';
});
print('Sync success: ${event.message}');
} else if (event is SyncErrorEvent) {
setState(() {
_syncStatus = 'Error: ${event.errorMessage}';
});
print('Sync error (${event.errorCode}): ${event.errorMessage}');
}
});
// Listen to region enter/exit events
_regionSubscription = BearoundFlutterSdk.regionStream.listen((event) {
if (event is BeaconRegionEnterEvent) {
setState(() {
_regionStatus = 'Inside region: ${event.regionName}';
});
print('Entered beacon region: ${event.regionName}');
} else if (event is BeaconRegionExitEvent) {
setState(() {
_regionStatus = 'Outside region: ${event.regionName}';
});
print('Exited beacon region: ${event.regionName}');
}
});
}
@override
void dispose() {
_beaconsSubscription?.cancel();
_syncSubscription?.cancel();
_regionSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Beacon Monitor')),
body: Column(
children: [
ListTile(
title: Text('Sync Status'),
subtitle: Text(_syncStatus),
),
ListTile(
title: Text('Region Status'),
subtitle: Text(_regionStatus),
),
Expanded(
child: ListView.builder(
itemCount: _beacons.length,
itemBuilder: (context, index) {
final beacon = _beacons[index];
return ListTile(
title: Text('Beacon ${index + 1}'),
subtitle: Text(
'UUID: ${beacon.uuid}\n'
'Major: ${beacon.major}, Minor: ${beacon.minor}\n'
'RSSI: ${beacon.rssi} dBm',
),
);
},
),
),
],
),
);
}
}
βοΈ Configuration (v1.3.1+) #
The SDK provides configurable scan intervals and backup sizes to optimize performance and battery usage based on your app's needs.
Sync Interval (Beacon Scan Frequency) #
Configure how often the SDK scans for beacons. Lower intervals provide faster detection but consume more battery.
import 'package:bearound_flutter_sdk/bearound_flutter_sdk.dart';
// Set scan interval (5-60 seconds)
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time20); // 20 seconds (default)
// Get current interval
final interval = await BearoundFlutterSdk.getSyncInterval();
print('Current interval: ${interval.seconds} seconds');
Available Intervals:
SyncInterval.time5toSyncInterval.time60(5 to 60 seconds)- Default:
SyncInterval.time20(20 seconds) - Balanced performance and battery
Backup Size (Failed Beacon Storage) #
Configure how many failed beacon detections are stored for retry when API calls fail.
// Set backup size (5-50 beacons)
await BearoundFlutterSdk.setBackupSize(BackupSize.size40); // 40 beacons (default)
// Get current backup size
final size = await BearoundFlutterSdk.getBackupSize();
print('Backup size: ${size.value} beacons');
Available Sizes:
BackupSize.size5toBackupSize.size50(5 to 50 beacons)- Default:
BackupSize.size40(40 beacons)
Configuration Recommendations #
| Scenario | Sync Interval | Backup Size | Use Case |
|---|---|---|---|
| Real-time tracking | time5 - time10 |
size15 - size20 |
Immediate updates, lower backup needed |
| Standard monitoring | time20 - time30 (β default) |
size30 - size40 |
Balanced performance and battery |
| Battery-optimized | time40 - time60 |
size40 - size50 |
Longer intervals, larger backup for reliability |
| Offline-first apps | time30 - time60 |
size50 |
Handle poor network conditions |
Platform-Specific Notes #
- iOS: Both settings can be changed at any time (before or after
startScan()) - Android:
setSyncInterval()can be changed dynamically at runtimesetBackupSize()must be set before callingstartScan()
Example: Battery-Optimized Configuration #
class BatteryOptimizedScanner {
Future<void> startScanning() async {
// Configure for battery optimization
await BearoundFlutterSdk.setBackupSize(BackupSize.size50); // Android: set before startScan
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time60);
// Request permissions
final granted = await BearoundFlutterSdk.requestPermissions();
if (!granted) return;
// Start scanning
await BearoundFlutterSdk.startScan('your-token', debug: false);
}
}
Example: Real-time Tracking Configuration #
class RealtimeTracker {
Future<void> startTracking() async {
// Configure for real-time tracking
await BearoundFlutterSdk.setBackupSize(BackupSize.size20); // Android: set before startScan
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time5);
final granted = await BearoundFlutterSdk.requestPermissions();
if (!granted) return;
await BearoundFlutterSdk.startScan('your-token', debug: true);
}
Future<void> adjustForBatteryLevel(int batteryLevel) async {
// Dynamically adjust based on battery (iOS and Android)
if (batteryLevel < 20) {
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time60);
} else if (batteryLevel < 50) {
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time30);
} else {
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time10);
}
}
}
Background Scanning & State Synchronization #
When your app supports background scanning, the SDK may continue running even after the app is closed. To properly synchronize the UI state when the app reopens, implement lifecycle management:
class BackgroundScannerApp extends StatefulWidget {
@override
_BackgroundScannerAppState createState() => _BackgroundScannerAppState();
}
class _BackgroundScannerAppState extends State<BackgroundScannerApp>
with WidgetsBindingObserver {
bool _isScanning = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_syncStateWithNative();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
// App returned to foreground, sync state
_syncStateWithNative();
}
}
/// Synchronizes UI state with native SDK state
Future<void> _syncStateWithNative() async {
final isRunning = await BearoundFlutterSdk.isInitialized();
if (isRunning && !_isScanning) {
// SDK is running but UI shows stopped - reconnect
print('Detected SDK running in background, reconnecting...');
// Re-register listeners and update state
await BearoundFlutterSdk.startScan('your-token', debug: true);
setState(() {
_isScanning = true;
});
print('Reconnection successful: events restored');
} else if (!isRunning && _isScanning) {
// SDK stopped but UI shows running - update state
setState(() {
_isScanning = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Background Scanner'),
actions: [
Icon(_isScanning ? Icons.wifi_tethering : Icons.wifi_off),
],
),
body: Center(
child: ElevatedButton(
onPressed: _isScanning ? _stopScanning : _startScanning,
child: Text(_isScanning ? 'Stop' : 'Start'),
),
),
);
}
Future<void> _startScanning() async {
final granted = await BearoundFlutterSdk.requestPermissions();
if (!granted) return;
await BearoundFlutterSdk.startScan('your-token', debug: true);
setState(() => _isScanning = true);
}
Future<void> _stopScanning() async {
await BearoundFlutterSdk.stopScan();
setState(() => _isScanning = false);
}
}
Key Benefits:
- β UI stays in sync with background services
- β Handles app restarts gracefully
- β Automatically reconnects event listeners
- β No initialization errors on app reopen
π API Reference #
BearoundFlutterSdk #
The main entry point for the SDK.
Methods
requestPermissions()
Requests all necessary permissions for beacon scanning.
static Future<bool> requestPermissions()
Returns: true if all permissions are granted, false otherwise.
startScan(String clientToken, {bool debug = false})
Starts beacon scanning with the provided client token.
static Future<void> startScan(String clientToken, {bool debug = false})
Parameters:
clientToken(String): Your Bearound client tokendebug(bool): Enable debug logging (default:false)
Throws: Exception if permissions are not granted or scanning fails.
stopScan()
Stops beacon scanning and cleans up resources.
static Future<void> stopScan()
isInitialized() π
Checks if the SDK is currently initialized and running. Useful for state synchronization when app reopens after being closed.
static Future<bool> isInitialized()
Returns: true if SDK is initialized and running, false otherwise.
Example:
final isRunning = await BearoundFlutterSdk.isInitialized();
if (isRunning) {
print('SDK is already running in background');
}
Event Streams π
beaconsStream
Stream of real-time beacon detection events.
static Stream<BeaconsDetectedEvent> get beaconsStream
Event Types:
BeaconEventType.ENTER- Beacon entered rangeBeaconEventType.EXIT- Beacon exited rangeBeaconEventType.FAILED- Beacon detection failed
Example:
BearoundFlutterSdk.beaconsStream.listen((event) {
print('Event: ${event.eventType.name}');
print('Beacons: ${event.beacons.length}');
for (var beacon in event.beacons) {
print(' UUID: ${beacon.uuid}, RSSI: ${beacon.rssi}');
}
});
syncStream
Stream of API synchronization events (success and errors).
static Stream<BeaconEvent> get syncStream
Event Types:
SyncSuccessEvent- Sync completed successfullySyncErrorEvent- Sync failed with error
Example:
BearoundFlutterSdk.syncStream.listen((event) {
if (event is SyncSuccessEvent) {
print('Synced ${event.beaconsCount} beacons: ${event.message}');
} else if (event is SyncErrorEvent) {
print('Sync error (${event.errorCode}): ${event.errorMessage}');
}
});
regionStream
Stream of beacon region entry and exit events.
static Stream<BeaconEvent> get regionStream
Event Types:
BeaconRegionEnterEvent- Entered a beacon regionBeaconRegionExitEvent- Exited a beacon region
Example:
BearoundFlutterSdk.regionStream.listen((event) {
if (event is BeaconRegionEnterEvent) {
print('Entered region: ${event.regionName}');
} else if (event is BeaconRegionExitEvent) {
print('Exited region: ${event.regionName}');
}
});
Beacon Model #
Represents a detected beacon with all its properties.
class Beacon {
final String uuid; // Beacon UUID
final int major; // Major identifier
final int minor; // Minor identifier
final int rssi; // Signal strength in dBm
final String? bluetoothName; // Bluetooth device name (optional)
final String? bluetoothAddress; // Bluetooth MAC address (optional)
final double? distanceMeters; // Estimated distance in meters (optional)
final int? lastSeen; // π Last detection timestamp in milliseconds
}
Properties:
uuid: Universally unique identifier of the beaconmajor: Major value for grouping beacons (e.g., by location)minor: Minor value for identifying specific beaconsrssi: Received Signal Strength Indicator in dBm (higher = closer)bluetoothName: Human-readable name of the Bluetooth devicebluetoothAddress: Physical MAC address of the Bluetooth devicedistanceMeters: Estimated distance from device to beacon in meterslastSeen: Unix timestamp (milliseconds) when beacon was last detected
Methods
fromJson(Map<String, dynamic> json)
Creates a Beacon instance from JSON data.
toJson()
Converts the beacon to JSON format.
π‘οΈ Error Handling #
The SDK provides comprehensive error handling:
try {
final granted = await BearoundFlutterSdk.requestPermissions();
if (!granted) {
throw Exception('Required permissions not granted');
}
await BearoundFlutterSdk.startScan('your-token');
} catch (e) {
print('Error starting beacon scan: $e');
// Handle the error appropriately
}
π§ Troubleshooting #
iOS: Beacons Not Detected #
Problem: SDK initializes successfully but no beacons are detected.
Solution (Fixed in v1.3.1):
- β Ensure you're using v1.3.1 or later (critical fix for iOS beacon detection)
- β Check that Location permission is set to "Always" (not just "When In Use")
- β Verify Bluetooth is enabled on the device
- β
Confirm beacons are broadcasting with UUID:
E25B8D3C-947A-452F-A13F-589CB706D2E5 - β Test with physical beacons (simulators don't support BLE properly)
Debugging:
// Enable debug mode to see detailed logs
await BearoundFlutterSdk.startScan('your-token', debug: true);
// Check if SDK is initialized
final isRunning = await BearoundFlutterSdk.isInitialized();
print('SDK running: $isRunning');
Android: Permission Issues #
Problem: App crashes or beacons not detected on Android 12+.
Solution:
- β
Add all required permissions to
AndroidManifest.xml(see Platform Setup) - β Request runtime permissions before starting scan
- β
For Android 12+, ensure
BLUETOOTH_SCANandBLUETOOTH_CONNECTare granted - β
Add JitPack repository to
settings.gradle.kts
State Synchronization Issues #
Problem: UI shows incorrect state after app reopens from background.
Solution (Available in v1.1.1+):
class MyApp extends StatefulWidget with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_syncStateWithNative(); // Sync with native SDK state
}
}
Future<void> _syncStateWithNative() async {
final isRunning = await BearoundFlutterSdk.isInitialized();
// Update UI based on actual SDK state
}
}
Configuration Not Applied #
Problem: Scan interval or backup size changes don't take effect.
Solution:
- iOS: Both settings can be changed at any time
- Android:
setBackupSize()must be called beforestartScan()
// Correct order for Android
await BearoundFlutterSdk.setBackupSize(BackupSize.size40);
await BearoundFlutterSdk.setSyncInterval(SyncInterval.time20);
await BearoundFlutterSdk.startScan('your-token');
π§ͺ Testing #
The SDK includes a comprehensive test suite. Run tests with:
flutter test
For coverage report:
flutter test --coverage
π€ Contributing #
We welcome contributions! Please read our Contributing Guide for details.
Development Setup #
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass:
flutter test - Check code formatting:
dart format . - Run static analysis:
flutter analyze - Submit a pull request
π Changelog #
See CHANGELOG.md for a detailed list of changes.
π License #
This project is licensed under the MIT License - see the LICENSE file for details.
π Support #
- π Documentation
- π Issue Tracker
- π¬ Discussions
- π§ Email Support