【第二四天 - Flutter iBeacon 官方范例讲解(下)】

前言

今日的程序码 => GITHUB

讲解官方范例的权限、扫描、广播的部分。官方范例是使用 GetX 来写。
这边,我有稍微改了一下一些参数的名称,所以会和官方范例有些微不同。

RequirementStateController

这边是手动去检查权限,还要再去记住监听器使否开启。

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;
  }
}

Home Page

这边介绍一下 AppLifeCycler

  1. YourWidgetState with WidgetsBindingObserver 这个interface
  2. initState时call WidgetsBinding.instance.addObserver(this)
  3. override void didChangeAppLifecycleState(AppLifecycleState state)
  4. dispose时call WidgetsBinding.instance.removeObserver(this)

resumed 可见、可操作(进入前景)
inactive可见、不可遭做 ( 如果来了个电话,电话会进入前景,因此会触发此状态, the application is visible and responding to user input)
paused 不可见、不可操作(进入背景)
detached 虽然还在运行,但已经没有任何存在的页面

HomePage 程序码

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'),
              ),
            ],
          );
        },
      );
    }
  }
}

扫描 Scanning 的程序码介绍

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(),
      ),
    );
  }
}

BroadCasting 的程序码

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' : ''}'),
    );
  }
}

<<:  [ 卡卡 DAY 23 ] - React Native 表单套件用 Formik + Yup 验证 (上)

>>:  ITHOME IRONMAN体验 Day 30-完赛心得

[DAY8]k8s必学的设定档-yaml (上)

YAML(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达资料序列化的格式。YA...

人机结合与数据学习

人的科技文明发展始终来自於人性 在现今的科技加速之下,所有的一切都将因为有了网路而有所不同,也因为在...

规划

大家好, 因为工作不太常用到AI/ML, 所以我自身会想要去多看多了解, 才不会脱钩 想当初整整研究...

用 Python 畅玩 Line bot - 05:MessageEvent

除了文字讯息以外, Line 还有很多种讯息型态可以传送,例如图片,音档,贴图......。 我们可...