Integrating iBeacons in Flutter: A Step-by-Step Guide

satyyyaamm
9 min readJan 7, 2024

Introduction

Beacons are small devices broadcasting a Bluetooth signal that can be picked up by nearby devices (usually a smartphone). The true power of beacons, though, lies with the powerful software that allows you to build extraordinary apps for almost any kind of use case.

Assuming that you already understand flutter as you are here, Let’s get started with it:

Let’s get Started

We’re going to use flutter_beacon Package

Add the following Packages to your pubspec.yaml file

dependencies:
flutter_beacon: ^0.5.1
permission_handler: ^10.2.0
geolocator: ^9.0.2
app_settings: ^4.2.0
get: ^4.3.8

Flutter_beacon:- to range/monitor beacons
permission_handler:- to get the status of user permission
geolocator:- to get different types of location access such as Always on, Allow only while using.
app_settings:- To open any app-related settings
get:- For State management

Before we start monitoring Beacons we must get user location permission and Bluetooth permission. flutter_beacon package has a stream listening to Bluetooth and location for the monitoring to work. Also if you’re planning to push it to the store as per the guidelines you must ask the user for permission before start using it, Now getting back to the beacon integration:

Setup specific for Android

For target SDK version 29+ (Android 10, 11) is necessary to add manually ACCESS_FINE_LOCATION

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

and if you want also background scanning:

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

If your app targets Android 6.0 (API level 23) or above, you need to request runtime permissions from the user. Check if your code includes the necessary checks and requests for Bluetooth-related permissions at runtime.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

Setup specific for iOS

To use beacons-related features, apps are required to ask the location permission. It’s a two-step process:

  1. Declare the permission the app requires in configuration files
  2. Request permission from the user when the app is running (the plugin can handle this automatically)

The needed permissions in iOS is when in use.

For more details about what you can do with each permission, see:
https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services

Permission must be declared in ios/Runner/Info.plist:

<dict>
<!-- When in use -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>Reason why app needs location</string>
<!-- Always -->
<!-- for iOS 11 + -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Reason why app needs location</string>
<!-- for iOS 9/10 -->
<key>NSLocationAlwaysUsageDescription</key>
<string>Reason why app needs location</string>
<!-- Bluetooth Privacy -->
<!-- for iOS 13 + -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Reason why app needs bluetooth</string>
</dict>

Starting with the permission services Let’s create a class that holds all the details related to getting permissions from the user,

import 'dart:io';
import 'package:app_settings/app_settings.dart';
import 'package:flutter/cupertino.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';


class PermissionService {
static final PermissionService _permissions = PermissionService._internal();

factory PermissionService() {
return _permissions;
}

PermissionService._internal();

static Future<bool> checkSystemOverlayPermission() async {
return await Permission.systemAlertWindow.status.isGranted;
}

static Future<bool> checkLocationPermission() async {
return await Permission.locationAlways.status.isGranted;
}

static Future<bool> checkBluetoothPermission() async {
return await Permission.bluetooth.status.isGranted;
}

static Future<bool> requestSystemOverLayPermission() async {
PermissionStatus status = await Permission.systemAlertWindow.request();
return status.isGranted;
}

static Future<bool> requestLocationPermission(context) async {
if (Platform.isIOS) {
LocationPermission permission = await Geolocator.requestPermission();
if (LocationPermission.always == permission) {
return true;
} else {
return await showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
content: Text(AppString.set_permissions_to_always),
actions: <Widget>[
CupertinoDialogAction(
child: Text("Continue"),
onPressed: () {
Geolocator.openLocationSettings();
Navigator.pop(context, true);
},
),
CupertinoDialogAction(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context, true);
},
),
],
));
}
} else {
var status = await Permission.locationAlways.request();
if (status.isGranted) {
return status.isGranted;
} else if (status.isPermanentlyDenied) {
return await Geolocator.openLocationSettings();
} else if (status.isPermanentlyDenied) {
return await Geolocator.openLocationSettings();
} else if (status.isRestricted) {
return await Geolocator.openLocationSettings();
} else {
return await Geolocator.openLocationSettings();
}
}
}

static Future<bool> requestBluetoothPermission(context) async {
if (Platform.isIOS) {
PermissionStatus permission = await Permission.bluetooth.request();
if (PermissionStatus.granted == permission) {
return true;
} else {
return await showCupertinoDialog(
context: context,
builder: (_) => CupertinoAlertDialog(
content: Text(
'App wants your bluetooth to be turn ON to enable scanning of beacon around you to enhance your navigation experience.'),
actions: <Widget>[
CupertinoDialogAction(
child: Text("Continue"),
onPressed: ()
AppSettings.openDeviceSettings();
Navigator.pop(context, true);
},
),
CupertinoDialogAction(
child: Text("Cancel"),
onPressed: () {
Navigator.pop(context, true);
},
),
],
));
}
}
}
}

Create a Controller to manage the state of Ranging Beacons and display it accordingly

class RequirementStateController extends GetxController {
var bluetoothState = BluetoothState.stateOff.obs;
var authorizationStatus = AuthorizationStatus.notDetermined.obs;
var locationService = false.obs;
StreamSubscription<RangingResult> streamRanging;
final beacons = <Beacon>[].obs;
var _startBroadcasting = false.obs;
var _startScanning = false.obs;
var _pauseScanning = false.obs;
bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
bool get authorizationStatusOk =>
authorizationStatus.value == AuthorizationStatus.allowed ||
authorizationStatus.value == AuthorizationStatus.always;
bool get locationServiceEnabled => locationService.value;

updateBluetoothState(BluetoothState state) {
bluetoothState.value = state;
}

updateAuthorizationStatus(AuthorizationStatus status) {
authorizationStatus.value = status;
}

updateLocationService(bool flag) {
locationService.value = flag;
}

startBroadcasting() {
_startBroadcasting.value = true;
}

stopBroadcasting() {
_startBroadcasting.value = false;
}

startScanning() {
_startScanning.value = true;
_pauseScanning.value = false;
}

pauseScanning() {
_startScanning.value = false;
_pauseScanning.value = true;
}

stopStreamRanging() async {
await streamRanging?.cancel();
}

Stream<bool> get startBroadcastStream {
return _startBroadcasting.stream;
}

Stream<bool> get startStream {
return _startScanning.stream;
}

Stream<bool> get pauseStream {
return _pauseScanning.stream;
}
}

On the Home Screen or the desired screen wherever you want to start ranging the beacons. Make sure to add with WidgetBindingObserver

class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
StreamSubscription<RangingResult> _streamRanging;
StreamSubscription<BluetoothState> _streamBluetooth;
final _regionBeacons = <Region, List<Beacon>>{};
final controller = Get.find<RequirementStateController>();

@override
void initState() {
super.initState();

_checkLocationPermission();
_checkBluetoothPermission();

WidgetsBinding.instance?.addObserver(this);

controller.startStream.listen((flag) {
if (flag == true) {
initScanBeacon();
}
});

controller.pauseStream.listen((flag) {
if (flag == true) {
pauseScanBeacon();
}
});

listeningState();
checkAllRequirements();
initScanBeacon();
}

}

Here’s how all the functions look:
The first few functions are quite self-explanatory _checkLocationPermission()
_checkBluetoothPermission()
showLocationDialog()
showBluetoothDialog()

The listeningState() Function initializes Bluetooth-related functionality and sets up a listener for Bluetooth state changes. When the Bluetooth state changes, it updates a controller and performs additional checks. The exact behavior and purpose of the function may depend on the broader context of the code, including the implementations of the initializeAndCheckScanning, updateBluetoothState, and checkAllRequirements functions, as well as the usage of the flutterBeacon object and associated libraries.

checkAllRequirements() performs checks related to Bluetooth state, authorization status, and location service status. Depending on the results of these checks, it updates the state of a controller object and starts or pauses Bluetooth scanning accordingly. The exact behavior may depend on the specific implementation of the controller object and the associated Flutter/Bluetooth libraries being used.

_checkLocationPermission() async {
try {
bool status = await PermissionService.checkLocationPermission();
if (status) {
print(Permission.bluetoothScan.status);
} else {
print(Permission.bluetoothScan.status);
bool locationPermissionStatus = await PermissionService.requestLocationPermission(context);
if (locationPermissionStatus) {
showBluetoothDialog();
// await _fetchDataFromDataBase();
} // SystemNavigator.pop();
}
} catch (e) {
print(e);
}
}

_checkBluetoothPermission() async {
try {
bool status = await PermissionService.checkBluetoothPermission();
if (status) {
print('Bluetooth Permission: ${Permission.bluetoothScan.status}');
} else {
print('Bluetooth Permission: ${Permission.bluetoothScan.status}');
bool bluetoothPermissionStatus =
await PermissionService.requestBluetoothPermission(context);
if (bluetoothPermissionStatus) {
showBluetoothDialog();
// await _fetchDataFromDataBase();
} // SystemNavigator.pop();
}
} catch (e) {}
}

showLocationDialog() {
print('location dialog');
if (Platform.isIOS) {
return CupertinoAlertDialog(
title: new Text("UJ Wayfinder Location"),
content: new Text(
"App collects location data to enable scanning of beacon around you to enhance your navigation experience even when the app is close or not in use."),
actions: <Widget>[
CupertinoDialogAction(
isDefaultAction: true,
child: Text("Yes"),
onPressed: () async {
Navigator.pop(context);
await Permission.location.request();
// showBluetoothDialog();
Navigator.pop(context);
},
),
CupertinoDialogAction(
child: Text("No"),
onPressed: () {
Navigator.pop(context);
},
)
],
);
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(
'App collects location data to enable scanning of beacon around you to enhance your navigation experience even when the app is close or not in use.'),
actions: [
TextButton(
onPressed: () async {
Navigator.pop(context);
await Permission.location.request();
Navigator.pop(context);
},
child: Text('Allow')),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Deny'))
],
);
});
}
}

showBluetoothDialog() {
print('bluetooth dialog');
if (Platform.isIOS) {
return CupertinoAlertDialog(
title: new Text("UJ Wayfinder Location"),
content: new Text(
'App wants your bluetooth to be turn ON to enable scanning of beacon around you to enhance your navigation experience.'),
actions: <Widget>[
CupertinoDialogAction(
isDefaultAction: true,
child: Text("Yes"),
onPressed: () async {
Navigator.pop(context);
await Permission.bluetoothScan.request();
Navigator.pop(context);
},
),
CupertinoDialogAction(
child: Text("No"),
onPressed: () {
Navigator.pop(context);
},
)
],
);
} else {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(
'App wants your bluetooth to be turn ON to enable scanning of beacon around you to enhance your navigation experience.'),
actions: [
TextButton(
onPressed: () async {
OpenSettings.openBluetoothSetting();
},
child: Text('Allow')),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Deny'))
],
);
});
}
}

void listeningState() async {
print('Listening to bluetooth state');
await flutterBeacon.initializeAndCheckScanning;
_streamBluetooth = flutterBeacon.bluetoothStateChanged().listen((BluetoothState state) async {
controller.updateBluetoothState(state);
checkAllRequirements();
});
}

void checkAllRequirements() async {
print('CHECKALLREQUIRMENTS');
final bluetoothState = await flutterBeacon.bluetoothState;
controller.updateBluetoothState(bluetoothState);
print('BLUETOOTH $bluetoothState');
final authorizationStatus = await flutterBeacon.authorizationStatus;
print('AUTHORIZATION 1$authorizationStatus');
controller.updateAuthorizationStatus(authorizationStatus);
print('AUTHORIZATION 2$authorizationStatus');

final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled;
controller.updateLocationService(locationServiceEnabled);
print('LOCATION SERVICE $locationServiceEnabled');

if (controller.bluetoothEnabled &&
controller.authorizationStatusOk &&
controller.locationServiceEnabled) {
print('STATE READY');
print('SCANNING 1');
controller.startScanning();
} else {
print('STATE NOT READY');
controller.pauseScanning();
}
}

The Main Hero of the game!!

initScanBeacon() async {
ibeaconuuid = await SessionManager.getIbeaconuuid();
await flutterBeacon.initializeScanning;
if (!controller.authorizationStatusOk ||
!controller.locationServiceEnabled ||
!controller.bluetoothEnabled) {
print('RETURNED, authorizationStatusOk=${controller.authorizationStatusOk}, '
'locationServiceEnabled=${controller.locationServiceEnabled}, '
'bluetoothEnabled=${controller.bluetoothEnabled}');
return;
}

regions.add(Region(identifier: 'wifi_area', proximityUUID: "8492E75F-4FD6-469D-B132-043FE94921D8"));

if (_streamRanging != null) {
if (_streamRanging.isPaused) {
_streamRanging?.resume();
return;
}
}

_streamRanging = flutterBeacon.ranging(regions).listen((RangingResult result)
print("result: $result");
try {
if (mounted) {
setState(() {
_regionBeacons[result.region] =
result.beacons.forEach((beacon) {
if (_regionBeacons[result.region][0].rssi == 0) {
print('major: ${beacon.major} \n minor: ${beacon.minor} \n rssi: ${beacon.rssi}');
} else {
controller.beacons.clear();
_regionBeacons.values.forEach((list) {
controller.beacons.addAll(list);
controller.beacons.sort(_compareParameters);
});
}
});
});
}
} catch (e) {
print('Flutter beacon ranging error $e');

showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Flutter beacon ranging error'),
);
});
}
});
}

There’s an issue with the Android part of the beacon monitoring where the beacon emits null values in between intervals which causes the beacon to disappear, here’s how you could fix that
Note: Already added in the above function initScanBeacon()

 result.beacons.forEach((beacon) {
if (_regionBeacons[result.region][0].rssi == 0) {
print('major: ${beacon.major} \n minor: ${beacon.minor} \n rssi: ${beacon.rssi}');
} else {
controller.beacons.clear();
_regionBeacons.values.forEach((list) {
controller.beacons.addAll(list);
controller.beacons.sort(_compareParameters);
});
}
});

To pause the Scan:

pauseScanBeacon() async {
_streamRanging?.pause();
if (controller.beacons.isNotEmpty) {
// setState(() {
controller.beacons.clear();
// });
}
}

Compare code:

int _compareParameters(Beacon a, Beacon b) {
int compare = b.proximityUUID.compareTo(a.proximityUUID);

if (compare == 0) {
compare = b.rssi.compareTo(a.rssi);
}

if (compare == 0) {
compare = a.major.compareTo(b.major);
}

if (compare == 0) {
compare = a.minor.compareTo(b.minor);
}

return compare;
}

Add this to your didChangeAppLifecycleState to make sure you check required streams when you get back the app from the notifications tray or any other state.

@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
print('AppLifecycleState = $state');
if (state == AppLifecycleState.resumed) {
if (_streamBluetooth != null) {
if (_streamBluetooth.isPaused) {
_streamBluetooth?.resume();
}
}
checkAllRequirements();
} else if (state == AppLifecycleState.paused) {
_streamBluetooth?.pause();
}
}

Don’t forget to dispose the required items:

  @override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_streamRanging?.cancel();
controller.pauseScanning();
_streamBluetooth?.cancel();
super.dispose();
}

Displaying the beacons:

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Utils.fromHex("5c6569"),
title: Text(
'FLUTTER BEACON',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: Dimens.sixteen,
fontWeight: FontWeight.w400,
),
),
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
tooltip: 'Back',
onPressed: () {
Navigator.of(context).pop(true);
},
),
centerTitle: false,
actions: <Widget>[
Obx(() {
if (!controller.locationServiceEnabled)
return IconButton(
tooltip: 'Not Determined',
icon: Icon(Icons.portable_wifi_off),
color: Colors.grey,
onPressed: () {},
);

if (!controller.authorizationStatusOk)
return IconButton(
tooltip: 'Not Authorized',
icon: Icon(Icons.portable_wifi_off),
color: Colors.red,
onPressed: () async {
await flutterBeacon.requestAuthorization;
},
);

return IconButton(
tooltip: 'Authorized',
icon: Icon(Icons.wifi_tethering),
color: Colors.blue,
onPressed: () async {
await flutterBeacon.requestAuthorization;
},
);
}),
Obx(() {
return IconButton(
tooltip: controller.locationServiceEnabled
? 'Location Service ON'
: 'Location Service OFF',
icon: Icon(
controller.locationServiceEnabled ? Icons.location_on : Icons.location_off,
),
color: controller.locationServiceEnabled ? Colors.blue : Colors.red,
onPressed: controller.locationServiceEnabled ? () {} : handleOpenLocationSettings,
);
}),
Obx(() {
final state = controller.bluetoothState.value;

if (state == BluetoothState.stateOn) {
return IconButton(
tooltip: 'Bluetooth ON',
icon: Icon(Icons.bluetooth_connected),
onPressed: () {},
color: Colors.lightBlueAccent,
);
}

if (state == BluetoothState.stateOff) {
return IconButton(
tooltip: 'Bluetooth OFF',
icon: Icon(Icons.bluetooth),
onPressed: (){
OpenSettings.openBluetoothSetting();
},
color: Colors.red,
);
}

return IconButton(
icon: Icon(Icons.bluetooth_disabled),
tooltip: 'Bluetooth State Unknown',
onPressed: () {},
color: Colors.grey,
);
}),
],
),
body: Obx(
() => SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Total Beacons Found by this UUID: ${controller.beacons.value.length}',
style: TextStyle(
fontSize: 18,
),
),
SizedBox(height: 20),
for (var i = 0; i < controller.beacons.value.length; i++)
Container(child: Text(' RSSI: ${controller.beacons[i].rssi.toString()}')),
SizedBox(height: 20),
Container(
height: 500,
child: controller.beacons.value.isEmpty
? Center(child: CircularProgressIndicator())
: ListView(
children: ListTile.divideTiles(
context: context,
tiles: controller.beacons.value.map(
(beacon) {
return ListTile(
title: Text(
beacon.proximityUUID,
style: TextStyle(fontSize: 15.0),
),
subtitle: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Flexible(
child: Text(
'Major: ${beacon.major}\nMinor: ${beacon.minor}',
style: TextStyle(fontSize: 13.0),
),
flex: 1,
fit: FlexFit.tight,
),
Flexible(
child: Text(
'Accuracy: ${beacon.accuracy}m\nRSSI: ${beacon.rssi}',
style: TextStyle(fontSize: 13.0),
),
flex: 2,
fit: FlexFit.tight,
)
],
),
);
},
),
).toList(),
),
),
],
),
),
));
}

Make sure you test it on a physical device.
Here’s the GitHub link

Make the changes as per your needs
Great, You’ve integrated beacons into your app!👏🏻👏🏻

--

--