본문 바로가기
Programming/APP

Flutter MethodChannel 연동 (3) - Android Bluetooth 기기 찾기

by 해도 Haedo 2022. 10. 5.

안녕하세요. 뉴핀입니다.

지난 시간에는 Flutter로 Android Bluetooth Permission 권한 허용을 얻는 방법까지 알아보았습니다.
오늘은 Bluetooth Permission 권한 허용을 받아 Flutter로 Android에서 Bluetooth 기기를 찾는 방법에 대해 알려드리고자 합니다.

Flutter로 Android에서 Bluetooth 기기를 찾기 위해서는 크게 4단계로 구성됩니다.
1) Android 기기에서 블루투스를 사용하기 위한 AndroidManifest에서 퍼미션 설정
2) Flutter에서 Android 기기의 블루투스 퍼미션을 받기 위한 MainActivity - main.dart 간 MethodChannel 통신
3) Flutter에서 블루투스 기기 찾기 및 연결을 위한 pubspec.yaml - flutter_blue 라이브러리 설정

4) Flutter에서 블루투스 탐색을 위한 main.dart UI 및 기기 탐색 예제

블루투스에 대한 사전 작업은 모두 끝마친 상태로 오늘은 Flutter에서만 작업하여 Bluetooth 기기를 찾는 예제에 대해 마무리를 해보려고 합니다.

Android에서 저전력 블루투스(BLE)에 해당하는 정보는 아래 링크를 통해 자세하게 확인하실 수 있습니다.

 

저전력 블루투스 개요  |  Android 개발자  |  Android Developers

저전력 블루투스 개요 Android 4.3(API 레벨 18)에서는 저전력 블루투스(BLE)에 대한 플랫폼 내 지원을 핵심적 역할로 도입하고 앱이 기기를 검색하고, 서비스를 쿼리하고, 정보를 전송하는 데 사용할

developer.android.com


Flutter에서 사용된 라이브러리는 flutterr_blue이며 작성일 기준 가장 최신 버전인 0.8.0으로 세팅해두었습니다.

Flutter 패키지 중 하나인 flutter_blue에 해당하는 정보는 아래 링크를 통해 자세하게 확인하실 수 있습니다.

 

flutter_blue | Flutter Package

Flutter plugin for connecting and communicating with Bluetooth Low Energy devices, on Android and iOS

pub.dev


flutter_blue에 대한 예제를 구글링 하던 도중 발견한 소스코드입니다. 아래 링크를 통해 자세하게 확인하실 수 있습니다.

 

GitHub - dong-higenis/flutter_ble_scan_example: flutter_ble_scan_example

flutter_ble_scan_example. Contribute to dong-higenis/flutter_ble_scan_example development by creating an account on GitHub.

github.com


main.dart

lib > main.dart

우선 flutter_blue 라이브러리를 사용하기 위해 FlutterBlue를 인스턴스 한 후 스캔한 값들을 받아올 리스트를 만듭니다.
그리고 스캔 중인지 아닌지 구분하기 위한 bool 형태의 값도 선언을 해줍니다.

FlutterBlue flutterBlue = FlutterBlue.instance;
List<ScanResult> scanResultList = [];
bool _isScanning = false;

initState() 안에 지난 시간에 사용했던 initBle() 함수를 호출합니다.

void initBle() async {
  await _getNativeValue();
  flutterBlue.isScanning.listen((isScanning) {
    setState(() {});
  });
}

다음은 블루투스 스캔을 위해 비동기 함수로 코드를 입력합니다.

scan() async {
  if(!_isScanning) {
    scanResultList.clear();

    flutterBlue.startScan(timeout: Duration(seconds: 15));

    flutterBlue.scanResults.listen((results) {
      scanResultList = results;
      setState(() {});
    });
  } else {
    flutterBlue.stopScan();
  }
}

해당 함수는 스캔을 위한 버튼 클릭 시 현재 스캔 중인지 여부를 확인하여 스캔 중이라면 스캔을 멈추는 .stopScan()을 호출하고, 스캔 중이 아니라면 기존에 스캔 결과 값을 담아왔던 리스트를 clear() 후 새로 스캔을 시작합니다.
스캔 시간은 초단위로 구현하였으며 저는 15초로 테스트하였습니다.

스캔을 시작하여 results로 받은 값들을 scanResultList 리스트에 담아둡니다.

다음은 리스트에 담아두었던 results 값들을 각 리스트 별 이름과 신호 값으로 나눠서 위젯으로 만듭니다.

Widget listItem(ScanResult r) {
  String name = '';
  if(r.device.name.isNotEmpty) {
    name = r.device.name;
  } else if(r.advertisementData.localName.isNotEmpty) {
    name = r.advertisementData.localName;
  } else {
    name = 'N/A';
  }
  return ListTile(
    title: Text(name),
    trailing: Text(r.rssi.toString()),
  );
}

디바이스의 이름을 가지고 있다면 해당 이름을 name에 저장하고, 만약 디바이스 이름이 없고 로컬 네임으로 가지고 있다면 해당 이름을 name에 저장하고, 둘 다 아니라면 이름을 알 수 없기에 'N/A'로 저장합니다.
그리고 디바이스의 rssi 값을 String으로 수정 후 마무리합니다.

마지막으로 UI 부분입니다.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: ListView.separated(
        itemCount: scanResultList.length,
        itemBuilder: (context, index) {
          return listItem(scanResultList[index]);
        },
        separatorBuilder: (BuildContext context, int index) {
          return Divider();
        },
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: scan,
      child: Icon(_isScanning ? Icons.stop : Icons.search),
    ),
  );
}

단순히 Android 기기에서 블루투스 통신으로 기기들을 찾는 것이기 때문에 스캔을 위한 버튼, 블루투스 기기 리스트 이외에는 작성할 것이 없습니다.

이제 테스트를 해보겠습니다.

우측 하단에 있는 검색 버튼을 클릭 시 정말 많은 블루투스 기기들을 볼 수 있습니다.
주변에 블루투스 센서가 있는 기기가 많아 알 수 없는 기기들도 굉장히 많이 뜨는 것을 볼 수 있습니다.

전체 코드입니다.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blue/flutter_blue.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FlutterBlue flutterBlue = FlutterBlue.instance;
  List<ScanResult> scanResultList = [];
  bool _isScanning = false;

  static const platform = MethodChannel('example.com/value');

  String _value = 'null';

  Future<void> _getNativeValue() async {
    String value;

    try {
      value = await platform.invokeMethod('getBle');
    } on PlatformException catch (e) {
      value = 'native code error: ${e.message}';
    }

    setState(() {
      _value = value;
    });
  }

  @override
  void initState() {
    super.initState();

    initBle();
  }

  void initBle() async {
    await _getNativeValue();
    flutterBlue.isScanning.listen((isScanning) {
      setState(() {});
    });
  }

  scan() async {
    if(!_isScanning) {
      scanResultList.clear();

      flutterBlue.startScan(timeout: Duration(seconds: 15));

      flutterBlue.scanResults.listen((results) {
        scanResultList = results;
        setState(() {});
      });
    } else {
      flutterBlue.stopScan();
    }
  }

  Widget listItem(ScanResult r) {
    String name = '';
    if(r.device.name.isNotEmpty) {
      name = r.device.name;
    } else if(r.advertisementData.localName.isNotEmpty) {
      name = r.advertisementData.localName;
    } else {
      name = 'N/A';
    }
    return ListTile(
      title: Text(name),
      trailing: Text(r.rssi.toString()),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: ListView.separated(
          itemCount: scanResultList.length,
          itemBuilder: (context, index) {
            return listItem(scanResultList[index]);
          },
          separatorBuilder: (BuildContext context, int index) {
            return Divider();
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: scan,
        child: Icon(_isScanning ? Icons.stop : Icons.search),
      ),
    );
  }
}


이렇게 해서 오늘은 Flutter로 MethodChannel을 통해 Android에서 네이티브 호출을 하여 Bluetooth에 대한 퍼미션 권한을 허용하고, flutter_blue 라이브러리를 사용하여 Android 기기 주변에 있는 블루투스 기기들을 찾아보았습니다.

감사합니다.

댓글