FlutterでBluetooth接続できるアプリを作ってみた

### 想定読者
- Flutterを使ってBluetooth接続(BLE接続のみ)できるアプリを作りたい方
`Bluetooth接続の古い接続規格Bluetooth classicには対応していません。`
- 同じくBluetooth接続できるアプリを作ったけど、再接続の際のループ処理でバッテリー消費が激しくて困っている方


### 結論
最初に結論を話ししておくと、次の通りです。
- バッテリー消費
→1時間あたり48%→7~8%に抑えた。これが限界...
- iOSでアプリがバックグラウンド状態の時に定期的に処理を実行したい
→未解決。できたら記事を更新予定。


### 概要
- 自動接続方法
- バッテリー消費を抑えた方法
- バックグラウンドでも定期的に処理を実行する方法（※実装中）


### 使用したツールやパッケージ
- [flutter_ble_plus](https://github.com/boskokg/flutter_blue_plus)
- Flutter 3.19.5, Dart 3.3.3


### 実装
コード全体
```scan_screen.dart
// 同じペリフェラル機器でも、iOSとAndroidでremoteIdが異なる
final SAMPLE_PERIPHERAL_INFO = {
  "id": Platform.isIOS
      ? "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
      : "XX:XX:XX:XX:XX:XX",
};

class _ScanScreenState extends State<ScanScreen> {
    List<BluetoothDevice> _systemDevices = [];
    late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
    Timer? _scanTimer;
    
    @override
    void initState() {
        super.initState();

        // スキャン結果を取得
        _scanResultsSubscription =
            FlutterBluePlus.scanResults.listen((List<ScanResult> results) async {
          bool found = results.isNotEmpty &&
              results.any((result) =>
                  result.device.remoteId.toString() ==
                  SAMPLE_PERIPHERAL_INFO['id']);
          var result = results.firstWhere((result) =>
              result.device.remoteId.toString() == SAMPLE_PERIPHERAL_INFO['id']);
          // 対象のデバイスが見つかった場合
          if (found) {
            _systemDevices = [result.device];
            Platform.isIOS
                ? await result.device.connect(autoConnect: true, mtu: null)
                : await result.device.connect(mtu: null);
          }
        });

        // 接続が切れた際に再接続処理
        _systemDevices.forEach((device) {
          device.connectionState.listen((state) {
            if (state == BluetoothConnectionState.disconnected) {
              connectDevice();
            }
          });
        });

        // スキャンの開始
        startScanWithInterval();
    }

    @override
    void dispose() {
        _scanResultsSubscription.cancel();
        FlutterBluePlus.stopScan();
        super.dispose();
    }
  
    void scanDevice() async {
        // 既存のスキャンをキャンセル
        FlutterBluePlus.stopScan();
        FlutterBluePlus.startScan(
          withRemoteIds: [SAMPLE_PERIPHERAL_INFO['id'] as String],
          timeout: const Duration(milliseconds: 1000),
        );
    }
        
    void connectDevice() async {
        // 既存のタイマーをキャンセル
        _scanTimer?.cancel();
        _scanTimer = Timer.periodic(Duration(seconds: 10), (Timer t) async {
          print('start reconnect');
          try {
            // 自動接続
            isIOS
                ? await _systemDevices[0].autoConnect()
                : await _systemDevices[0].connectAndUpdateStream();
          } catch (e) {
            print('connect error: $e');
          }
        });
    }

    // Widgetは省略
}
```
#### 自動接続方法
初回スキャン`FlutterBluePlus.startScan()` をした後、スキャン結果を取得の箇所で `_systemDevices` に対象の機器を保持している。
2回目以降は接続が切れた段階で、connectDevice()を実行し、_systemDevicesを接続する処理を繰り返している。
#### バッテリー消費を抑えた方法
修正前：初回スキャン`FlutterBluePlus.startScan()`を繰り返し実行していた。
修正後：初回スキャンをして対象の機器を１度でも取得した場合は、以降は前回取得した機器情報を使用して接続するようにした。
`result.device.connect(autoConnect: true, mtu: null)`

スキャンを繰り返すことでバッテリー消耗を激しくしていた。
#### バックグラウンドでも定期的に処理を実行する方法(※実装中)