이제 BLE 개념에 대해 빠삭하게 알았으니 코드를 이용해서 BLE를 적용하는 방법을 낱낱이 알아보겠다.
코드 공부는 이 분의 코드를 클로론코딩하면서 기능을 익혀보는 방향으로 잡았다.
너무 좋은 자료 감사합니다ㅠㅠ!!
이 단계에서의 목표는, 우선 잘 정리 되어 있는 블로그 글을 클론코딩으로 따라해 본 뒤에 코드를 분석하면서 왜 이런 기능이 들어갔는지를 익히고, 코드를 클래스 단위로 분리해서 작동시켜 보면서 화면을 어떻게 분리할지를 예상해 보는 것!
우선 다음과 같이 main.dart 클론코딩을 완료했다.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final title = 'Flutter BLE Scan Demo';
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<ScanResult> scanResultList = []; //스캔 결과를 담기 위함
bool _isScanning = false;
BluetoothAdapterState _adapterState =
BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
//이 부분 코드 공부 필요
@override
void initState() {
//블루투스 초기화
super.initState();
_adapterStateStateSubscription =
FlutterBluePlus.adapterState.listen((state) {
_adapterState = state;
if (mounted) {
setState(() {});
}
});
}
//여기도 mount가 뭔지, 리스너가 정확히 뭔지 필요
void initBle() {
//블루투스 스캔 상태 얻기 위한 리스너
FlutterBluePlus.isScanning.listen((isScanning) {
_isScanning = isScanning;
if (mounted) {
setState(() {});
}
});
}
/*
스캔 시작/정지 함수
*/
//async = 느려지느 거라 붙이는 걸로 알고 있음
scan() async {
if (!_isScanning) {
//스캔중이 아니라면,
//기존에 스캔된 리스트 삭제
scanResultList.clear();
//스캔 시작, 제한 시간 5초
FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
//스캔 결과 리스너
//함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
FlutterBluePlus.scanResults.listen((results) {
//List<ScanResult> 형태의 results 값을 scanResultList에 복사
scanResultList = results;
//ui 갱신
setState(() {});
});
} else {
//스캔 중이라면, 스캔 정지
//궁금한게 굳이 이걸 왜 넣는걸까?
FlutterBluePlus.stopScan();
}
}
/*
장치별 출력용 함수들
나는 일단 device.id만 출력해본다.
*/
Widget deviceMacAddress(ScanResult r) {
return Text(r.device.remoteId.toString());
}
/* BLE 아이콘 위젯 */
Widget leading(ScanResult r) {
return const CircleAvatar(
backgroundColor: Colors.cyan,
child: Icon(
Icons.bluetooth,
color: Colors.white,
),
);
}
/* 장치 아이템을 탭 했을때 호출 되는 함수 */
//이 부분도 작동에 대한 공부는 필요할듯..
void onTap(ScanResult r) {
// 단순히 이름만 출력
print('${r.device.advName}');
}
/* 장치 아이템 위젯 */
// 이 부분도 어떤 방식인지는 공부가 필요할듯..
Widget listItem(ScanResult r) {
return ListTile(
onTap: () => onTap(r),
leading: leading(r),
title: deviceMacAddress(r),//정의한 macAddress를 return
);
}
/* UI */
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
/*장치 리스트 출력 */
child: ListView.separated(
itemBuilder: (context, index) {
return listItem(scanResultList[index]);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(); //뭐...인덱스 별로 나눈다 이런건지...?
},
itemCount: scanResultList.length)),
floatingActionButton: FloatingActionButton(
onPressed: scan,
//스캔 중이라면 stop 아이콘을, 정지 상태라면 search 아이콘을 표시
child: Icon(_isScanning ? Icons.stop : Icons.search),
),
);
}
}
https://blog.naver.com/chandong83/222414483554
완성 화면
이제 코드를 하나하나 뜯어가면서 여기서 왜 이런 기능이 쓰였는지를 상세히 알아보자.
프론트 화면 ui를 위한 위젯 부분은 넘어가고, 블루투스 기능을 위주로 살펴보겠다
class _MyHomePageState extends State<MyHomePage> {
List<ScanResult> scanResultList = []; //스캔 결과를 담기 위함
bool _isScanning = false;
BluetoothAdapterState _adapterState =
BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
@override
void initState() {
//블루투스 초기화
super.initState();
_adapterStateStateSubscription =
FlutterBluePlus.adapterState.listen((state) {
_adapterState = state;
if (mounted) {
setState(() {});
}
});
}
void initBle() {
//블루투스 스캔 상태 얻기 위한 리스너
FlutterBluePlus.isScanning.listen((isScanning) {
_isScanning = isScanning;
if (mounted) {
setState(() {});
}
});
}
/*
스캔 시작/정지 함수
*/
//async = 느려지느 거라 붙이는 걸로 알고 있음
scan() async {
if (!_isScanning) {
//스캔중이 아니라면,
//기존에 스캔된 리스트 삭제
scanResultList.clear();
//스캔 시작, 제한 시간 5초
FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
//스캔 결과 리스너
//함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
FlutterBluePlus.scanResults.listen((results) {
//List<ScanResult> 형태의 results 값을 scanResultList에 복사
scanResultList = results;
//ui 갱신
setState(() {});
});
} else {
//스캔 중이라면, 스캔 정지
//궁금한게 굳이 이걸 왜 넣는걸까?
FlutterBluePlus.stopScan();
}
}
해당 코드는 StatefulWidget이 실행하는 화면 부분이다. 이 부분에서 블루투스 초기화 및 블루투스 스캔 상태를 얻기 위한 리스너 실행, 스캔 시작이 이뤄진다.
BluetoothAdapterState _adapterState =
BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
@override
void initState() {
//블루투스 초기화
super.initState();
_adapterStateStateSubscription =
FlutterBluePlus.adapterState.listen((state) {
_adapterState = state;
if (mounted) {
setState(() {});
}
});
}
이 부분은 초기화를 위한 부분이다.
initState 메서드는 해당 위젯이 상태 트리에 추가될 때 한 번만 호출되므로, 한 번만 실행되어야 하는 초기화 작업을 여기서 수행할 수 있다.
여기서는 블루투스를 사용하기 위해, initState를 override해서 재정의 한 후 상태를 초기화 해주고 있다.
super.initState() : 상위 클래스틔 initState 메서드를 호출한다. 이렇게 함으로써 상위 클래스의 initState 메서드가 중요한 초기화 로직을 실행할 수 있도록 한다.
_ adapterStateStateSubscription : 블루투스 어댑터 상태 변경을 감지하기 위한 StreamSubscription 객체이다. 블루투스 어댑터의 상태를 수신하기 위해 이 객체가 사용된다.
위에서 선언했던 스트림 객체를 정의하여 adapt 상태 변경을 체크한다.
FlutterBluePlus.adapterState.listen((state) { adapterState = state;} : FlutterBluePlus의 adapterState 스트림에서 상태 변경을 구독하고 있다. 상태가 변경되면 해당 상태를 adapterState 변수에 할당하고, 이후에 mounted 상태인지 확인한 후 setState를 호출하여 UI를 업데이트한다.
void initBle() {
//블루투스 스캔 상태 얻기 위한 리스너
FlutterBluePlus.isScanning.listen((isScanning) {
_isScanning = isScanning;
if (mounted) {
setState(() {});
}
});
}
이 코드는 위 코드와 형식상은 동일하다.
adapt 뿐 아니라 스캔 상태를 확인하기 위한 코드이다.
우리의 경우 이를 이용해서 스캔 상태가 종료 되었을시 해당하는 화면을 띄울 수 있을 것으로 기대된다.
/*
스캔 시작/정지 함수
*/
scan() async {
if (!_isScanning) {
//스캔중이 아니라면,
//기존에 스캔된 리스트 삭제
scanResultList.clear();
//스캔 시작, 제한 시간 5초
FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
//스캔 결과 리스너
//함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
FlutterBluePlus.scanResults.listen((results) {
//List<ScanResult> 형태의 results 값을 scanResultList에 복사
scanResultList = results;
//ui 갱신
setState(() {});
});
} else {
//스캔 중이라면, 스캔 정지
FlutterBluePlus.stopScan();
}
}
다음 log는 실행 되었을때 나타난 log이다. 이를 분석해보자.
D/[FBP-Android]( 4191): [FBP] onMethodCall: startScan
D/BluetoothAdapter( 4191): isLeEnabled(): ON
D/BluetoothLeScanner( 4191): onScannerRegistered() - status=0 scannerId=3 mScannerId=0
D/EGL_emulation( 4191): app_time_stats: avg=3788.37ms min=563.35ms max=7013.39ms count=2
D/[FBP-Android]( 4191): [FBP] onMethodCall: stopScan
휴대폰에 연결해서 결과를 살펴보자