flutter_blue_plus を使った BLEスキャンを Flutter で実装してみる： Android Studio で Androidアプリをビルド

## はじめに

今回の内容は、以下の記事の続きのようなものです。

●Flutterアプリ（flutter_blue_plus の Android用サンプル）と micro:bit CreateAI のプログラムを BLE通信でつないでみる - Qiita
　https://qiita.com/youtoy/items/b8e14601ecab557d3f72

上記の記事では、flutter_blue_plus のサンプルアプリを Androidスマホで動かしてみました。

今回は、flutter_blue_plus を使った Flutter での BLEスキャンを実装してみます。

### 利用するパッケージ

前回同様、以下のパッケージを使います。

●flutter_blue_plus | Flutter package
　https://pub.dev/packages/flutter_blue_plus

![2025-03-26_23-28-32.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f4ca86f7-4a2b-486e-aea7-7fe835dd6c4e.jpeg)


## 実際に試していく

### 下準備

Android Studio で、新規の Flutter用プロジェクトを作成し、今回用の下準備を進めます。

#### パッケージのインストール

以下のコマンドで、flutter_blue_plus をインストールします（自分の環境は FVM を使っているので、先頭部分に fvm をつけています）。

```zsh
fvm flutter pub add flutter_blue_plus
```

#### パーミッションの追加

「android/app/src/main/AndroidManifest.xml」にパーミッションを追加します。

追加する内容は、flutter_blue_plus の公式ページの以下に書かれているものです。

●flutter_blue_plus | Flutter package
　https://pub.dev/packages/flutter_blue_plus#add-permissions-for-android-no-location

![2025-03-26_23-49-41.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4d56d6c4-6efc-4959-9ea6-4f34c3e941b7.jpeg)

自分のスマホは Android 15 なので、以下の部分だけ追記します。

```xml
<!-- Tell Google Play Store that your app uses Bluetooth LE
     Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />

<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

追記後の冒頭部分は、以下のとおりです。

![2025-03-27_00-57-03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2ae3fd03-0708-4767-afcd-6904398a1f8a.jpeg)

### スキャンする処理を実装する

公式ページの以下の部分に、スキャンに関するコードが載っています。

●flutter_blue_plus | Flutter package
　https://pub.dev/packages/flutter_blue_plus#scan-for-devices

![2025-03-27_00-58-44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/19de07a1-c3a1-48d4-aa12-1070227ba576.jpeg)

これを、処理の中に組み込んでいきます。

#### 画面構成

今回のアプリの画面構成は、とりあえずシンプルなもので試してみます。
具体的には、「スキャンを開始するボタン」と「スキャンできたデバイスを表示する部分」がメインの要素になる形にしました。


#### コード

コードは以下のとおりです。

```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BLE Scan Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'BLE Scan Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final List<String> _scanResults = [];
  StreamSubscription<List<ScanResult>>? _scanSubscription;

  // スキャン開始
  Future<void> _startScan() async {
    // 前回の結果をクリア
    setState(() {
      _scanResults.clear();
    });

    // BluetoothがONになるまで待機
    await FlutterBluePlus.adapterState
        .firstWhere((state) => state == BluetoothAdapterState.on);

    // 結果の取得
    _scanSubscription = FlutterBluePlus.onScanResults.listen(
          (results) {
        for (var result in results) {
          final deviceId = result.device.remoteId.toString();
          final deviceName = result.advertisementData.advName;
          final info = '$deviceId: "$deviceName"';
          // 重複チェックしつつリストへ追加
          if (!_scanResults.contains(info)) {
            setState(() {
              _scanResults.add(info);
            });
          }
        }
      },
      onError: (e) => print('Error: $e'),
    );

    // タイムアウト15秒でスキャン実行
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));

    await FlutterBluePlus.isScanning
        .firstWhere((isScanning) => isScanning == false);

    FlutterBluePlus.cancelWhenScanComplete(_scanSubscription!);
    _scanSubscription = null;
  }

  @override
  void dispose() {
    _scanSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: _scanResults.isEmpty
          ? const Center(child: Text('スキャン結果はありません'))
          : ListView.builder(
        itemCount: _scanResults.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_scanResults[index]),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _startScan,
        tooltip: 'BLEスキャン',
        child: const Icon(Icons.search),
      ),
    );
  }
}
```

#### スキャン結果

アプリをビルドして Androidスマホ上で動かします。

画面右下のボタンを押下するとスキャンが始まり、冒頭の記事でも扱っていた micro:bit もスキャン結果の中に出てきました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/16943a5d-ca7e-41d8-bfa1-585e5c8ee297.png" width=50%>

## おわりに

とりあえず、flutter_blue_plus を使った BLEスキャンの実装を試せました。

最終的にやりたいことは、特定のデバイスに接続して、そのデバイスと Flutterアプリとの間で通信をさせることなので、その実装も試していければと思います。


## その他

スキャン用の処理が書かれた部分のリンクで、「Common Problems」というものがあって気になったので、（いつか使う時があるかもしれないのもあり）メモしておきます。

●flutter_blue_plus | Flutter package
　https://pub.dev/packages/flutter_blue_plus#common-problems

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/3187842a-ca30-479f-891b-d444f2c58824.png)



## 【追記】 対象の絞り込みを行うもの

その後、「デバイス名に特定の文字列を含むもののみを表示する」という処理を加えたものも作ってみました。

●flutter_blue_plus を使った Flutter での BLEスキャン実装で表示するものをフィルタする - Qiita
　https://qiita.com/youtoy/items/0247179d4065c8d69d19