bearound_flutter_sdk 2.2.3
bearound_flutter_sdk: ^2.2.3 copied to clipboard
Bearound Flutter SDK - Beacon detection and proximity tracking
import 'dart:async';
import 'package:bearound_flutter_sdk/bearound_flutter_sdk.dart';
import 'package:flutter/material.dart';
import 'settings_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bearound SDK Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const BeaconHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class BeaconHomePage extends StatefulWidget {
const BeaconHomePage({super.key});
@override
State<BeaconHomePage> createState() => _BeaconHomePageState();
}
class _BeaconHomePageState extends State<BeaconHomePage>
with WidgetsBindingObserver {
bool _hasPermission = false;
bool _isScanning = false;
String _status = 'Parado';
ForegroundScanInterval _foregroundScanInterval =
ForegroundScanInterval.seconds15;
BackgroundScanInterval _backgroundScanInterval =
BackgroundScanInterval.seconds30;
MaxQueuedPayloads _maxQueuedPayloads = MaxQueuedPayloads.medium;
List<Beacon> _detectedBeacons = [];
final List<String> _logs = [];
String? _lastError;
StreamSubscription<List<Beacon>>? _beaconsSubscription;
StreamSubscription<bool>? _scanningSubscription;
StreamSubscription<BearoundError>? _errorSubscription;
StreamSubscription<SyncLifecycleEvent>? _syncLifecycleSubscription;
StreamSubscription<BackgroundDetectionEvent>?
_backgroundDetectionSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_startListening();
_initializeSdk();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_beaconsSubscription?.cancel();
_scanningSubscription?.cancel();
_errorSubscription?.cancel();
_syncLifecycleSubscription?.cancel();
_backgroundDetectionSubscription?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
_refreshScanningState();
}
}
Future<void> _initializeSdk() async {
final granted = await BearoundFlutterSdk.requestPermissions();
setState(() {
_hasPermission = granted;
_status = granted ? 'Permissões OK' : 'Permissões necessárias!';
});
if (!granted) {
return;
}
await _applyConfiguration();
await _refreshScanningState();
}
Future<void> _applyConfiguration() async {
try {
await BearoundFlutterSdk.configure(
businessToken: "your-business-token-here",
foregroundScanInterval: _foregroundScanInterval,
backgroundScanInterval: _backgroundScanInterval,
maxQueuedPayloads: _maxQueuedPayloads,
);
_addLog(
'⚙️ Configurado: FG ${_foregroundScanInterval.seconds}s, '
'BG ${_backgroundScanInterval.seconds}s, '
'Queue ${_maxQueuedPayloads.value} '
'(BLE metadata e scan periódico são automáticos em v2.2.0)',
);
} catch (error) {
setState(() {
_lastError = error.toString();
});
_addLog('❌ Erro ao configurar: $error');
}
}
Future<void> _refreshScanningState() async {
final isRunning = await BearoundFlutterSdk.isScanning();
setState(() {
_isScanning = isRunning;
_status = isRunning ? 'Scaneando…' : 'Parado';
});
}
void _startListening() {
// Beacons stream
_beaconsSubscription = BearoundFlutterSdk.beaconsStream.listen((beacons) {
debugPrint('[DEBUG] 📡 Beacons callback: ${beacons.length} beacons');
setState(() {
_detectedBeacons = beacons;
});
_addLog('📡 Beacons detectados: ${beacons.length}');
for (final beacon in beacons) {
debugPrint(
'[DEBUG] - Beacon ${beacon.major}.${beacon.minor}: RSSI ${beacon.rssi}dBm',
);
}
});
// Scanning state stream
_scanningSubscription = BearoundFlutterSdk.scanningStream.listen((
isScanning,
) {
debugPrint('[DEBUG] 🎯 Scanning state changed: $isScanning');
setState(() {
_isScanning = isScanning;
_status = isScanning ? 'Scaneando…' : 'Parado';
});
_addLog('🔄 Scanning ${isScanning ? 'ativo' : 'parado'}');
});
// Error stream
_errorSubscription = BearoundFlutterSdk.errorStream.listen((error) {
debugPrint('[DEBUG] ❌ Error callback: ${error.message}');
setState(() {
_lastError = error.message;
});
_addLog('❌ Erro: ${error.message}');
});
// v2.2.0: Sync lifecycle stream (NEW!)
_syncLifecycleSubscription = BearoundFlutterSdk.syncLifecycleStream.listen((
event,
) {
debugPrint(
'[DEBUG] 🔄 Sync lifecycle: ${event.type}, beacons: ${event.beaconCount}',
);
if (event.isStarted) {
_addLog('🚀 Sync iniciado com ${event.beaconCount} beacon(s)');
} else if (event.isCompleted) {
if (event.success == true) {
_addLog('✅ Sync completo: ${event.beaconCount} beacon(s) enviados');
} else {
_addLog('❌ Sync falhou: ${event.error ?? "erro desconhecido"}');
}
}
});
// v2.2.0: Background detection stream (NEW!)
_backgroundDetectionSubscription = BearoundFlutterSdk
.backgroundDetectionStream
.listen((event) {
debugPrint(
'[DEBUG] 🌙 Background detection: ${event.beaconCount} beacons',
);
_addLog('🌙 Background: ${event.beaconCount} beacon(s) detectado(s)');
});
debugPrint('[DEBUG] ✅ Todos os streams inicializados');
}
void _addLog(String log) {
final timestamp = DateTime.now().toString().substring(11, 19);
if (!mounted) {
return;
}
setState(() {
_logs.insert(0, '[$timestamp] $log');
if (_logs.length > 50) {
_logs.removeLast();
}
});
}
Future<void> _startScan() async {
if (!_hasPermission) {
_addLog('⚠️ Permissões necessárias');
return;
}
// Configuration already applied in _initializeSdk() or when settings change
// No need to call _applyConfiguration() here, it would restart the scan twice
try {
await BearoundFlutterSdk.startScanning();
_addLog('🚀 Scanner iniciado');
} catch (error) {
setState(() {
_lastError = error.toString();
});
_addLog('❌ Erro ao iniciar: $error');
}
}
Future<void> _stopScan() async {
try {
await BearoundFlutterSdk.stopScanning();
} catch (error) {
setState(() {
_lastError = error.toString();
});
_addLog('❌ Erro ao parar: $error');
}
setState(() {
_detectedBeacons = [];
_lastError = null;
});
_addLog('🛑 Scanner parado');
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Bearound SDK'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.radar), text: 'Beacons'),
Tab(icon: Icon(Icons.sync), text: 'Status'),
Tab(icon: Icon(Icons.article), text: 'Logs'),
],
),
actions: [
Container(
margin: const EdgeInsets.only(right: 16),
child: Center(
child: Row(
children: [
Icon(
_isScanning ? Icons.wifi_tethering : Icons.wifi_off,
color: _isScanning ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(_status),
],
),
),
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
final result = await Navigator.push<SdkSettings>(
context,
MaterialPageRoute(
builder: (context) => SettingsPage(
initialForegroundScanInterval: _foregroundScanInterval,
initialBackgroundScanInterval: _backgroundScanInterval,
initialMaxQueuedPayloads: _maxQueuedPayloads,
),
),
);
if (result != null) {
setState(() {
_foregroundScanInterval = result.foregroundScanInterval;
_backgroundScanInterval = result.backgroundScanInterval;
_maxQueuedPayloads = result.maxQueuedPayloads;
});
if (_hasPermission) {
await _applyConfiguration();
}
}
},
tooltip: 'Configurações',
),
],
),
body: TabBarView(
children: [_buildBeaconsTab(), _buildStatusTab(), _buildLogsTab()],
),
bottomNavigationBar: _buildBottomBar(),
),
);
}
Widget _buildBeaconsTab() {
return Column(
children: [
if (_lastError != null)
Container(
padding: const EdgeInsets.all(16),
color: Colors.red.shade50,
child: Row(
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 8),
Expanded(
child: Text(
_lastError!,
style: const TextStyle(fontSize: 14, color: Colors.red),
),
),
],
),
),
Expanded(
child: _detectedBeacons.isEmpty
? const Center(
child: Text(
'Nenhum beacon detectado ainda',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(
itemCount: _detectedBeacons.length,
itemBuilder: (context, index) {
final beacon = _detectedBeacons[index];
final metadata = beacon.metadata;
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text('${index + 1}'),
),
title: Text(
'Major: ${beacon.major} | Minor: ${beacon.minor}',
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('UUID: ${beacon.uuid}'),
Text('RSSI: ${beacon.rssi} dBm'),
Text(
'Proximidade: ${beacon.proximity.name} | '
'Precisão: ${beacon.accuracy.toStringAsFixed(2)}m',
),
if (metadata != null)
Text(
'Bateria: ${metadata.batteryLevel}% | '
'Temp: ${metadata.temperature}°C | '
'Firmware: ${metadata.firmwareVersion}',
),
],
),
isThreeLine: true,
),
);
},
),
),
],
);
}
Widget _buildStatusTab() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status do SDK',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
padding: const EdgeInsets.all(16),
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_isScanning ? Icons.wifi_tethering : Icons.wifi_off,
color: _isScanning ? Colors.green : Colors.grey,
),
const SizedBox(width: 8),
Text(
'Scanning: ${_isScanning ? 'ativo' : 'parado'}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _isScanning ? Colors.green : Colors.grey,
),
),
],
),
const SizedBox(height: 8),
Text('Beacons detectados: ${_detectedBeacons.length}'),
],
),
),
const SizedBox(height: 16),
const Text(
'Configuração Atual',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text('Foreground: ${_foregroundScanInterval.seconds}s'),
Text('Background: ${_backgroundScanInterval.seconds}s'),
Text('Fila de retry: ${_maxQueuedPayloads.value} batches'),
const SizedBox(height: 8),
const Text(
'✨ Automático em v2.2.0:',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const Text('• Bluetooth metadata: sempre ativo'),
const Text('• Scan periódico: FG ativo, BG contínuo'),
if (_lastError != null) ...[
const SizedBox(height: 16),
Text(
'Último erro: $_lastError',
style: const TextStyle(color: Colors.red),
),
],
],
),
);
}
Widget _buildLogsTab() {
return Column(
children: [
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey.shade100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Total de logs: ${_logs.length}'),
TextButton.icon(
onPressed: () {
setState(() {
_logs.clear();
});
},
icon: const Icon(Icons.delete),
label: const Text('Limpar'),
),
],
),
),
Expanded(
child: _logs.isEmpty
? const Center(
child: Text(
'Nenhum log ainda',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(
itemCount: _logs.length,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
),
),
child: Text(
_logs[index],
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
);
},
),
),
],
);
}
Widget _buildBottomBar() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: Row(
children: [
if (!_hasPermission)
Expanded(
child: ElevatedButton.icon(
onPressed: _initializeSdk,
icon: const Icon(Icons.lock_open),
label: const Text('Solicitar Permissões'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
if (_hasPermission && !_isScanning)
Expanded(
child: ElevatedButton.icon(
onPressed: _startScan,
icon: const Icon(Icons.play_arrow),
label: const Text('Iniciar Scan'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
if (_isScanning)
Expanded(
child: ElevatedButton.icon(
onPressed: _stopScan,
icon: const Icon(Icons.stop),
label: const Text('Parar Scan'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
),
],
),
),
);
}
}