今日的程式碼 => GITHUB
講解官方範例的權限、掃描、廣播的部分。官方範例是使用 GetX 來寫。
這邊,我有稍微改了一下一些參數的名稱,所以會和官方範例有些微不同。
這邊是手動去檢查權限,還要再去記住監聽器使否開啟。
class RequirementStateController extends GetxController {
/// 藍芽的狀態
var bluetoothState = BluetoothState.stateOff.obs;
/// 藍芽授權狀態
var authorizationStatus = AuthorizationStatus.notDetermined.obs;
/// 定位開啟狀態
var locationService = false.obs;
/// 是否開始 broadcasting
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;
}
/// 更新 Location 的狀態
updateLocationService(bool flag) {
locationService.value = flag;
}
/// 更新藍芽的登入狀態
startBroadcasting() {
_startBroadcasting.value = true;
}
/// 停止 Broadcasting
stopBroadcasting() {
_startBroadcasting.value = false;
}
/// 停止 Scanning
startScanning() {
_startScanning.value = true;
_pauseScanning.value = false;
}
/// 暫停 Scanning
pauseScanning() {
_startScanning.value = false;
_pauseScanning.value = true;
}
/// get開始 BroadCastStream
Stream<bool> get startBroadcastStream {
return _startBroadcasting.stream;
}
/// get StartScanningStream
Stream<bool> get startScanningStream {
return _startScanning.stream;
}
/// pause scanningString
Stream<bool> get pauseScanningStream {
return _pauseScanning.stream;
}
}
resumed 可見、可操作(進入前景)
inactive可見、不可遭做 ( 如果來了個電話,電話會進入前景,因此會觸發此狀態, the application is visible and responding to user input)
paused 不可見、不可操作(進入背景)
detached 雖然還在運行,但已經沒有任何存在的頁面
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
final controller = Get.find<RequirementStateController>();
/// 監聽 BluetoothState 的狀態。
StreamSubscription<BluetoothState>? _streamBluetooth;
int currentIndex = 0;
@override
void initState() {
// 新增觀察者
WidgetsBinding.instance?.addObserver(this);
super.initState();
listeningState();
}
// 監聽藍芽狀態。
listeningState() async {
/// 下面這一行,是我自己加上去的, initState 的時候,先去檢查權限。
await checkAllRequirements();
/// 監聽狀態,狀態改變檢查權限
_streamBluetooth = flutterBeacon
.bluetoothStateChanged()
.listen((BluetoothState state) async {
controller.updateBluetoothState(state);
await checkAllRequirements();
});
}
/// 檢查權限
checkAllRequirements() async {
final bluetoothState = await flutterBeacon.bluetoothState;
controller.updateBluetoothState(bluetoothState);
print('BLUETOOTH $bluetoothState');
final authorizationStatus = await flutterBeacon.authorizationStatus;
controller.updateAuthorizationStatus(authorizationStatus);
print('AUTHORIZATION $authorizationStatus');
final locationServiceEnabled =
await flutterBeacon.checkLocationServicesIfEnabled;
controller.updateLocationService(locationServiceEnabled);
print('LOCATION SERVICE $locationServiceEnabled');
if (controller.bluetoothEnabled &&
controller.authorizationStatusOk &&
controller.locationServiceEnabled) {
print('STATE READY');
if (currentIndex == 0) {
print('SCANNING');
controller.startScanning();
} else {
print('BROADCASTING');
controller.startBroadcasting();
}
} else {
print('STATE NOT READY');
controller.pauseScanning();
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
print('AppLifecycleState = $state');
if (state == AppLifecycleState.inactive) {
// 如果來了個電話,電話會進入前景,因此會觸發此狀態。
}
if (state == AppLifecycleState.resumed) {
// 應用進入前景
if (_streamBluetooth != null) {
if (_streamBluetooth!.isPaused) {
_streamBluetooth?.resume();
}
}
await checkAllRequirements();
} else if (state == AppLifecycleState.paused) {
// 應用進入背景
_streamBluetooth?.pause();
}
}
@override
void dispose() {
_streamBluetooth?.cancel();
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Beacon'),
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: handleOpenBluetooth,
color: Colors.red,
);
}
return IconButton(
icon: Icon(Icons.bluetooth_disabled),
tooltip: 'Bluetooth State Unknown',
onPressed: () {},
color: Colors.grey,
);
}),
],
),
body: IndexedStack(
index: currentIndex,
children: [
// TabScanning(),
TabScanning(),
TabBroadcasting(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
setState(() {
currentIndex = index;
});
if (currentIndex == 0) {
controller.startScanning();
} else {
controller.pauseScanning();
controller.startBroadcasting();
}
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'Scan',
),
BottomNavigationBarItem(
icon: Icon(Icons.bluetooth_audio),
label: 'Broadcast',
),
],
),
);
}
/// 開啟定位
handleOpenLocationSettings() async {
if (Platform.isAndroid) {
await flutterBeacon.openLocationSettings;
} else if (Platform.isIOS) {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Location Services Off'),
content: Text(
'Please enable Location Services on Settings > Privacy > Location Services.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
);
},
);
}
}
/// 開啟藍芽
handleOpenBluetooth() async {
if (Platform.isAndroid) {
try {
await flutterBeacon.openBluetoothSettings;
} on PlatformException catch (e) {
print(e);
}
} else if (Platform.isIOS) {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Bluetooth is Off'),
content: Text('Please enable Bluetooth on Settings > Bluetooth.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
);
},
);
}
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_beacon/flutter_beacon.dart';
import 'package:flutter_beacon_example/controller/requirement_state_controller.dart';
import 'package:get/get.dart';
class TabScanning extends StatefulWidget {
@override
_TabScanningState createState() => _TabScanningState();
}
class _TabScanningState extends State<TabScanning> {
/// 監聽 RangingResult 的資料,用來管理是否有在監聽。
StreamSubscription<RangingResult>? _streamRanging;
/// 用來記錄 beacons 的資料。
final _regionBeacons = <Region, List<Beacon>>{};
/// 把 _regionBeacons 的 mpa value 全部存入這個 list
final _beacons = <Beacon>[];
final controller = Get.find<RequirementStateController>();
@override
void initState() {
super.initState();
/// 監聽開啟掃描的 bool stream
controller.startScanningStream.listen((flag) {
if (flag == true) {
initScanBeacon();
}
});
/// 監聽開啟掃描的 bool stream
controller.pauseScanningStream.listen((flag) {
if (flag == true) {
pauseScanBeacon();
}
});
}
/// 開始掃描。
initScanBeacon() async {
/// 初始化 Scanning
await flutterBeacon.initializeScanning;
/// 沒權限的話,就停止開始掃描
if (!controller.authorizationStatusOk ||
!controller.locationServiceEnabled ||
!controller.bluetoothEnabled) {
print(
'RETURNED, authorizationStatusOk=${controller
.authorizationStatusOk}, '
'locationServiceEnabled=${controller.locationServiceEnabled}, '
'bluetoothEnabled=${controller.bluetoothEnabled}');
return;
}
/// 定義要掃描的地區。
final regions = <Region>[
Region(
identifier: 'Cubeacon',
proximityUUID: 'CB10023F-A318-3394-4199-A8730C7C1AEC',
),
Region(
identifier: 'BeaconType2',
proximityUUID: '6a84c716-0f2a-1ce9-f210-6a63bd873dd9',
),
];
/// 如果他監聽器被暫停了,就恢復它。
if (_streamRanging != null) {
if (_streamRanging!.isPaused) {
_streamRanging?.resume();
return;
}
}
/// 監聽器開始監聽,並把資料存入變數裡面。
_streamRanging =
flutterBeacon.ranging(regions).listen((RangingResult result) {
print(result);
if (mounted) {
setState(() {
_regionBeacons[result.region] = result.beacons;
_beacons.clear();
_regionBeacons.values.forEach((list) {
_beacons.addAll(list);
});
_beacons.sort(_compareParameters);
});
}
});
}
/// 暫停監聽器、並清空資料。
pauseScanBeacon() async {
_streamRanging?.pause();
if (_beacons.isNotEmpty) {
setState(() {
_beacons.clear();
});
}
}
/// Beacon 的排序
int _compareParameters(Beacon a, Beacon b) {
int compare = a.proximityUUID.compareTo(b.proximityUUID);
if (compare == 0) {
compare = a.major.compareTo(b.major);
}
if (compare == 0) {
compare = a.minor.compareTo(b.minor);
}
return compare;
}
/// 關病監聽器。
@override
void dispose() {
_streamRanging?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _beacons.isEmpty
? Center(child: CircularProgressIndicator())
: ListView(
children: ListTile.divideTiles(
context: context,
tiles: _beacons.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(),
),
);
}
}
class _TabBroadcastingState extends State<TabBroadcasting> {
final controller = Get.find<RequirementStateController>();
final clearFocus = FocusNode();
/// 判斷現在是否有開啟 broadcasting。
bool broadcasting = false;
/// UUID 格式
final regexUUID = RegExp(
r'[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}');
final uuidController =
TextEditingController(text: 'CB10023F-A318-3394-4199-A8730C7C1AEC');
final majorController = TextEditingController(text: '0');
final minorController = TextEditingController(text: '0');
/// 判斷權限
bool get broadcastReady =>
controller.authorizationStatusOk == true &&
controller.locationServiceEnabled == true &&
controller.bluetoothEnabled == true;
@override
void initState() {
super.initState();
controller.startBroadcastStream.listen((flag) {
if (flag == true) {
initBroadcastBeacon();
}
});
}
/// 初始化 scanning
initBroadcastBeacon() async {
await flutterBeacon.initializeScanning;
}
@override
void dispose() {
clearFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(clearFocus),
child: Obx(
() => broadcastReady != true
? Center(child: Text('Please wait...'))
: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
uuidField,
majorField,
minorField,
SizedBox(height: 16),
buttonBroadcast,
],
),
),
),
),
),
);
}
Widget get uuidField {
return TextFormField(
readOnly: broadcasting,
controller: uuidController,
decoration: InputDecoration(
labelText: 'Proximity UUID',
),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Proximity UUID required';
}
if (!regexUUID.hasMatch(val)) {
return 'Invalid Proxmity UUID format';
}
return null;
},
);
}
Widget get majorField {
return TextFormField(
readOnly: broadcasting,
controller: majorController,
decoration: InputDecoration(
labelText: 'Major',
),
keyboardType: TextInputType.number,
validator: (val) {
if (val == null || val.isEmpty) {
return 'Major required';
}
try {
int major = int.parse(val);
if (major < 0 || major > 65535) {
return 'Major must be number between 0 and 65535';
}
} on FormatException {
return 'Major must be number';
}
return null;
},
);
}
Widget get minorField {
return TextFormField(
readOnly: broadcasting,
controller: minorController,
decoration: InputDecoration(
labelText: 'Minor',
),
keyboardType: TextInputType.number,
validator: (val) {
if (val == null || val.isEmpty) {
return 'Minor required';
}
try {
int minor = int.parse(val);
if (minor < 0 || minor > 65535) {
return 'Minor must be number between 0 and 65535';
}
} on FormatException {
return 'Minor must be number';
}
return null;
},
);
}
Widget get buttonBroadcast {
final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom(
onPrimary: Colors.white,
primary: broadcasting ? Colors.red : Theme.of(context).primaryColor,
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
);
return ElevatedButton(
style: raisedButtonStyle,
onPressed: () async {
if (broadcasting) {
await flutterBeacon.stopBroadcast();
} else {
await flutterBeacon.startBroadcast(BeaconBroadcast(
proximityUUID: uuidController.text,
major: int.tryParse(majorController.text) ?? 0,
minor: int.tryParse(minorController.text) ?? 0,
));
}
final isBroadcasting = await flutterBeacon.isBroadcasting();
if (mounted) {
setState(() {
broadcasting = isBroadcasting;
});
}
},
child: Text('Broadcast${broadcasting ? 'ing' : ''}'),
);
}
}