iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Mobile Development

Flutter - 複製貼上到開發套件之旅系列 第 24

【第二四天 - 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' : ''}'),
    );
  }
}

上一篇
【第二三天 - Flutter iBeacon 官方範例講解(上)】
下一篇
【第二五天 - Flutter 知名外送平台畫面練習(上)】
系列文
Flutter - 複製貼上到開發套件之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言