flutter_ble_central 0.3.0 copy "flutter_ble_central: ^0.3.0" to clipboard
flutter_ble_central: ^0.3.0 copied to clipboard

A Flutter package for scanning BLE data in central mode.

example/lib/main.dart

/*
 * Copyright (c) 2022. Julian Steenbakker.
 * All rights reserved. Use of this source code is governed by a
 * BSD-style license that can be found in the LICENSE file.
 */

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_ble_central/flutter_ble_central.dart';

void main() => runApp(const FlutterBleCentralExample());

/// Example app for the flutter_ble_central plugin
class FlutterBleCentralExample extends StatefulWidget {
  /// Constructor for the example app of the flutter_ble_central plugin
  const FlutterBleCentralExample({super.key});

  @override
  State<FlutterBleCentralExample> createState() =>
      _FlutterBleCentralExampleState();
}

class _FlutterBleCentralExampleState extends State<FlutterBleCentralExample> {
  final _messengerKey = GlobalKey<ScaffoldMessengerState>();
  final _navigatorKey = GlobalKey<NavigatorState>();
  final _ble = FlutterBleCentral();

  final Map<String, ScanResult> _devices = {};
  StreamSubscription<ScanResult>? _scanResultSub;
  StreamSubscription<int>? _scanErrorSub;
  StreamSubscription<CentralState>? _stateChangedSub;

  bool _isSupported = false;
  bool _isScanning = false;
  int _packetsFound = 0;
  CentralState _centralState = CentralState.unknown;

  @override
  void initState() {
    super.initState();
    _ble.enableTimingStats = true;
    _listenToStreams();
    unawaited(_initPlatformState());
  }

  @override
  void dispose() {
    unawaited(_scanResultSub?.cancel());
    unawaited(_scanErrorSub?.cancel());
    unawaited(_stateChangedSub?.cancel());
    super.dispose();
  }

  void _listenToStreams() {
    _scanErrorSub = _ble.onScanError?.listen((event) {
      final error = AndroidError.values[event];
      _showSnackBar('Scan error: $error (code $event)', isError: true);
    });

    _scanResultSub = _ble.onScanResult.listen((result) {
      _packetsFound++;

      final address = result.device?.address;
      if (address != null) {
        _devices[address] = result;
      }

      if (_isScanning) setState(() {});
    });

    _stateChangedSub = _ble.onPeripheralStateChanged.listen((state) {
      setState(() => _centralState = state);
    });
  }

  Future<void> _initPlatformState() async {
    final isSupported = await _ble.isSupported;
    setState(() => _isSupported = isSupported);

    if ((Platform.isWindows ||
            Platform.isAndroid ||
            Platform.isIOS ||
            Platform.isMacOS) &&
        mounted) {
      WidgetsBinding.instance.addPostFrameCallback((_) async {
        await _checkPermissions();
      });
    }
  }

  Future<void> _checkPermissions() async {
    // First check if BLE is supported
    final isSupported = await _ble.isSupported;
    if (!isSupported && mounted) {
      await _showUnsupportedDialog();
      return;
    }

    // Check permissions first (on Apple, we can't determine Bluetooth power
    // state until we have permission - state shows as unauthorized)
    final permission = await _ble.hasPermission();
    if (permission != BluetoothCentralState.granted && mounted) {
      final shouldContinue = await _showPermissionDialog(permission);
      if (shouldContinue != true) return;
    }

    // Now check if Bluetooth is powered on (after permission is granted)
    final isBluetoothOn = await _ble.isBluetoothOn;
    if (!isBluetoothOn && mounted) {
      await _showBluetoothOffDialog();
    }
  }

  Future<void> _showUnsupportedDialog() async {
    final navigatorContext = _navigatorKey.currentContext;
    if (navigatorContext == null) return;

    await showDialog<void>(
      context: navigatorContext,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return AlertDialog(
          icon:
              const Icon(Icons.bluetooth_disabled, color: Colors.red, size: 48),
          title: const Text('Bluetooth Not Supported'),
          content: const Text(
            'This device does not support Bluetooth Low Energy (BLE) scanning.'
            '\n\n'
            'BLE central mode requires compatible hardware.',
          ),
          actions: <Widget>[
            FilledButton(
              onPressed: () => Navigator.of(dialogContext).pop(),
              child: const Text('OK'),
            ),
          ],
        );
      },
    );
  }

  Future<bool?> _showPermissionDialog(
    BluetoothCentralState initialState,
  ) async {
    final navigatorContext = _navigatorKey.currentContext;
    if (navigatorContext == null) return false;

    return showDialog<bool>(
      context: navigatorContext,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return _PermissionDialog(
          initialState: initialState,
          onGranted: () {
            _messengerKey.currentState?.showSnackBar(
              const SnackBar(
                content: Text('Permission granted!'),
                backgroundColor: Colors.green,
              ),
            );
          },
        );
      },
    );
  }

  Future<bool?> _showBluetoothOffDialog() async {
    final navigatorContext = _navigatorKey.currentContext;
    if (navigatorContext == null) return false;

    return showDialog<bool>(
      context: navigatorContext,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return _BluetoothOffDialog(
          onEnabled: () {
            _messengerKey.currentState?.showSnackBar(
              const SnackBar(
                content: Text('Bluetooth enabled!'),
                backgroundColor: Colors.green,
              ),
            );
          },
        );
      },
    );
  }

  void _showSnackBar(String message, {bool isError = false}) {
    _messengerKey.currentState
      ?..clearSnackBars()
      ..showSnackBar(
        SnackBar(
          content: Text(message),
          backgroundColor: isError ? Colors.red : Colors.green,
        ),
      );
  }

  Future<void> _startScan() async {
    final state = await _ble.start();

    switch (state) {
      case BluetoothCentralState.ready:
      case BluetoothCentralState.granted:
        setState(() {
          _isScanning = true;
          _devices.clear();
          _packetsFound = 0;
        });
      case BluetoothCentralState.denied:
        _showSnackBar('Bluetooth denied. You can ask again.', isError: true);
      case BluetoothCentralState.permanentlyDenied:
        _showSnackBar('Bluetooth permanently denied.', isError: true);
      case BluetoothCentralState.turnedOff:
        _showSnackBar('Bluetooth turned off.', isError: true);
      case BluetoothCentralState.unsupported:
        _showSnackBar('Bluetooth unsupported.', isError: true);
      case BluetoothCentralState.restricted:
        _showSnackBar('Bluetooth restricted.', isError: true);
      case BluetoothCentralState.limited:
        _showSnackBar('Bluetooth limited.', isError: true);
      case BluetoothCentralState.unknown:
        _showSnackBar('Bluetooth unavailable.', isError: true);
    }
  }

  Future<void> _stopScan() async {
    await _ble.stop();
    setState(() => _isScanning = false);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: _navigatorKey,
      scaffoldMessengerKey: _messengerKey,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('BLE Central'),
          centerTitle: true,
        ),
        body: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // Status Card
              Padding(
                padding: const EdgeInsets.all(16),
                child: _StatusCard(
                  isSupported: _isSupported,
                  centralState: _centralState,
                  isScanning: _isScanning,
                  packetsFound: _packetsFound,
                  devicesFound: _devices.length,
                ),
              ),

              // Scan Controls
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: _SectionCard(
                  title: 'Scanning',
                  icon: Icons.bluetooth_searching,
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: FilledButton.icon(
                            onPressed: _isScanning ? null : _startScan,
                            icon: const Icon(Icons.play_arrow),
                            label: const Text('Start'),
                          ),
                        ),
                        const SizedBox(width: 12),
                        Expanded(
                          child: OutlinedButton.icon(
                            onPressed: _isScanning ? _stopScan : null,
                            icon: const Icon(Icons.stop),
                            label: const Text('Stop'),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 8),

              // Bluetooth & Permission Controls
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: _SectionCard(
                  title: 'Settings',
                  icon: Icons.settings,
                  children: [
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        ActionChip(
                          avatar:
                              const Icon(Icons.check_circle_outline, size: 18),
                          label: const Text('Check Permission'),
                          onPressed: () async {
                            final status = await _ble.hasPermission();
                            _showSnackBar(
                              'Permission: ${status.name}',
                              isError: status != BluetoothCentralState.granted,
                            );
                          },
                        ),
                        if (!Platform.isIOS && !Platform.isMacOS)
                          ActionChip(
                            avatar:
                                const Icon(Icons.add_circle_outline, size: 18),
                            label: const Text('Request Permission'),
                            onPressed: () async {
                              final status = await _ble.requestPermission();
                              _showSnackBar(
                                'Permission: ${status.name}',
                                isError:
                                    status != BluetoothCentralState.granted,
                              );
                            },
                          ),
                        ActionChip(
                          avatar:
                              const Icon(Icons.settings_bluetooth, size: 18),
                          label: const Text('Bluetooth Settings'),
                          onPressed: _ble.openBluetoothSettings,
                        ),
                        ActionChip(
                          avatar: const Icon(Icons.app_settings_alt, size: 18),
                          label: const Text('App Settings'),
                          onPressed: _ble.openAppSettings,
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 8),

              // Device List
              if (_devices.isEmpty)
                Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        Icons.bluetooth_searching,
                        size: 64,
                        color: Theme.of(context).colorScheme.outline,
                      ),
                      const SizedBox(height: 16),
                      Text(
                        _isScanning
                            ? 'Scanning for devices...'
                            : 'No devices found',
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                              color: Theme.of(context).colorScheme.outline,
                            ),
                      ),
                    ],
                  ),
                )
              else
                ListView.builder(
                  padding: const EdgeInsets.all(16),
                  itemCount: _devices.length,
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  itemBuilder: (context, index) {
                    final scanResult = _devices.values.elementAt(index);
                    final name = scanResult.scanRecord?.deviceName ?? 'Unknown';
                    final address = scanResult.device?.address ?? 'N/A';
                    final rssi = scanResult.rssi ?? 0;

                    return Card(
                      child: ListTile(
                        leading: Container(
                          padding: const EdgeInsets.all(8),
                          decoration: BoxDecoration(
                            color:
                                Theme.of(context).colorScheme.primaryContainer,
                            shape: BoxShape.circle,
                          ),
                          child: Icon(
                            Icons.bluetooth,
                            color: Theme.of(context)
                                .colorScheme
                                .onPrimaryContainer,
                          ),
                        ),
                        title: Text(name),
                        subtitle: Text(address),
                        trailing: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(
                              Icons.signal_cellular_alt,
                              color: _getRssiColor(rssi),
                            ),
                            Text(
                              '$rssi dBm',
                              style: Theme.of(context).textTheme.bodySmall,
                            ),
                          ],
                        ),
                      ),
                    );
                  },
                ),
            ],
          ),
        ),
      ),
    );
  }

  Color _getRssiColor(int rssi) {
    if (rssi >= -50) return Colors.green;
    if (rssi >= -70) return Colors.orange;
    return Colors.red;
  }
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({
    required this.isSupported,
    required this.centralState,
    required this.isScanning,
    required this.packetsFound,
    required this.devicesFound,
  });
  final bool isSupported;
  final CentralState centralState;
  final bool isScanning;
  final int packetsFound;
  final int devicesFound;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    final (icon, color, label) = _getStateInfo(centralState, colorScheme);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: color.withValues(alpha: 0.1),
                shape: BoxShape.circle,
              ),
              child: Icon(
                isScanning ? Icons.bluetooth_searching : icon,
                size: 48,
                color: isScanning ? Colors.blue : color,
              ),
            ),
            const SizedBox(height: 12),
            Text(
              isScanning ? 'Scanning' : label,
              style: Theme.of(context).textTheme.headlineSmall?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
            ),
            const SizedBox(height: 4),
            Text(
              centralState.name.toUpperCase(),
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                    color: color,
                    fontWeight: FontWeight.w600,
                    letterSpacing: 1.2,
                  ),
            ),
            const Divider(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _StatItem(
                  icon: Icons.devices,
                  label: 'Devices',
                  value: devicesFound.toString(),
                ),
                _StatItem(
                  icon: Icons.wifi_tethering,
                  label: 'Packets',
                  value: packetsFound.toString(),
                ),
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Icon(
                      isSupported ? Icons.check_circle : Icons.cancel,
                      size: 16,
                      color: isSupported ? Colors.green : Colors.red,
                    ),
                    const SizedBox(width: 4),
                    Text(
                      isSupported ? 'Supported' : 'Not Supported',
                      style: Theme.of(context).textTheme.bodySmall,
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  (IconData, Color, String) _getStateInfo(
    CentralState state,
    ColorScheme colorScheme,
  ) {
    return switch (state) {
      CentralState.idle => (Icons.bluetooth, colorScheme.primary, 'Ready'),
      CentralState.advertising => (
          Icons.broadcast_on_personal,
          Colors.green,
          'Advertising'
        ),
      CentralState.connected => (Icons.link, Colors.blue, 'Connected'),
      CentralState.poweredOff => (
          Icons.bluetooth_disabled,
          Colors.red,
          'Bluetooth Off'
        ),
      CentralState.unsupported => (
          Icons.error_outline,
          Colors.red,
          'Unsupported'
        ),
      CentralState.unauthorized => (Icons.lock, Colors.orange, 'Unauthorized'),
      CentralState.unknown => (
          Icons.help_outline,
          colorScheme.outline,
          'Unknown'
        ),
    };
  }
}

class _StatItem extends StatelessWidget {
  const _StatItem({
    required this.icon,
    required this.label,
    required this.value,
  });
  final IconData icon;
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
        const SizedBox(height: 4),
        Text(
          value,
          style: Theme.of(context).textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
        ),
        Text(
          label,
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ],
    );
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.title,
    required this.icon,
    required this.children,
  });
  final String title;
  final IconData icon;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Icon(icon, size: 20),
                const SizedBox(width: 8),
                Text(
                  title,
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            ...children,
          ],
        ),
      ),
    );
  }
}

class _PermissionDialog extends StatefulWidget {
  const _PermissionDialog({
    required this.onGranted,
    required this.initialState,
  });
  final VoidCallback onGranted;
  final BluetoothCentralState initialState;

  @override
  State<_PermissionDialog> createState() => _PermissionDialogState();
}

class _PermissionDialogState extends State<_PermissionDialog>
    with WidgetsBindingObserver {
  final _ble = FlutterBleCentral();
  bool _checkingPermission = false;
  bool _requesting = false;
  late BluetoothCentralState _permissionState;

  @override
  void initState() {
    super.initState();
    _permissionState = widget.initialState;
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      unawaited(_checkPermissionAndClose());
    }
  }

  Future<void> _checkPermissionAndClose() async {
    if (_checkingPermission) return;
    _checkingPermission = true;

    final result = await _ble.hasPermission();
    if (result == BluetoothCentralState.granted && mounted) {
      widget.onGranted();
      Navigator.of(context).pop(true);
    } else if (mounted) {
      setState(() => _permissionState = result);
    }

    _checkingPermission = false;
  }

  Future<void> _requestPermission() async {
    if (_requesting) return;
    setState(() => _requesting = true);

    final result = await _ble.requestPermission();
    if (result == BluetoothCentralState.granted && mounted) {
      widget.onGranted();
      Navigator.of(context).pop(true);
    } else if (mounted) {
      setState(() {
        _requesting = false;
        _permissionState = result;
      });
    }
  }

  bool get _isPermanentlyDenied =>
      _permissionState == BluetoothCentralState.permanentlyDenied;

  @override
  Widget build(BuildContext context) {
    if (Platform.isAndroid) {
      return _buildAndroidDialog(context);
    } else if (Platform.isIOS || Platform.isMacOS) {
      return _buildAppleDialog(context);
    } else {
      return _buildWindowsDialog(context);
    }
  }

  Widget _buildAndroidDialog(BuildContext context) {
    return AlertDialog(
      icon: Icon(
        _isPermanentlyDenied ? Icons.block : Icons.bluetooth,
        color: _isPermanentlyDenied ? Colors.red : Colors.blue,
        size: 48,
      ),
      title: Text(
        _isPermanentlyDenied ? 'Permission Denied' : 'Permission Required',
      ),
      content: Text(
        _isPermanentlyDenied
            ? 'Bluetooth permission was denied. You can only grant permission '
                'through the app settings.\n\n'
                'Please open Settings and enable Bluetooth permissions for '
                'this app.'
            : 'BLE scanning requires Bluetooth permissions.\n\n'
                'Please grant the required permissions to continue.',
      ),
      actions: <Widget>[
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),
          child: const Text('Cancel'),
        ),
        if (_isPermanentlyDenied)
          FilledButton.icon(
            onPressed: _ble.openAppSettings,
            icon: const Icon(Icons.settings),
            label: const Text('Open Settings'),
          )
        else ...[
          OutlinedButton.icon(
            onPressed: _ble.openBluetoothSettings,
            icon: const Icon(Icons.settings),
            label: const Text('Settings'),
          ),
          FilledButton.icon(
            onPressed: _requesting ? null : _requestPermission,
            icon: _requesting
                ? const SizedBox(
                    width: 16,
                    height: 16,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.check),
            label: const Text('Grant'),
          ),
        ],
      ],
    );
  }

  Widget _buildAppleDialog(BuildContext context) {
    return AlertDialog(
      icon: const Icon(Icons.bluetooth, color: Colors.blue, size: 48),
      title: const Text('Permission Required'),
      content: const Text(
        'BLE scanning requires Bluetooth permission.\n\n'
        'Please enable Bluetooth access for this app in System Settings.',
      ),
      actions: <Widget>[
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),
          child: const Text('Cancel'),
        ),
        FilledButton.icon(
          onPressed: _ble.openBluetoothSettings,
          icon: const Icon(Icons.settings),
          label: const Text('Open Settings'),
        ),
      ],
    );
  }

  Widget _buildWindowsDialog(BuildContext context) {
    return AlertDialog(
      icon: const Icon(Icons.location_on, color: Colors.blue, size: 48),
      title: const Text('Permission Required'),
      content: const Text(
        'BLE scanning on Windows requires location permission.\n\n'
        'Please grant the required permissions in Settings.',
      ),
      actions: <Widget>[
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),
          child: const Text('Cancel'),
        ),
        FilledButton.icon(
          onPressed: _ble.openAppSettings,
          icon: const Icon(Icons.settings),
          label: const Text('Open Settings'),
        ),
      ],
    );
  }
}

class _BluetoothOffDialog extends StatefulWidget {
  const _BluetoothOffDialog({required this.onEnabled});
  final VoidCallback onEnabled;

  @override
  State<_BluetoothOffDialog> createState() => _BluetoothOffDialogState();
}

class _BluetoothOffDialogState extends State<_BluetoothOffDialog>
    with WidgetsBindingObserver {
  final _ble = FlutterBleCentral();
  bool _checking = false;
  bool _enabling = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      unawaited(_checkBluetoothAndClose());
    }
  }

  Future<void> _checkBluetoothAndClose() async {
    if (_checking) return;
    _checking = true;

    final isOn = await _ble.isBluetoothOn;
    if (isOn && mounted) {
      widget.onEnabled();
      Navigator.of(context).pop(true);
    }

    _checking = false;
  }

  Future<void> _enableBluetooth() async {
    if (_enabling) return;
    setState(() => _enabling = true);

    final success = await _ble.enableBluetooth();
    if (success && mounted) {
      widget.onEnabled();
      Navigator.of(context).pop(true);
    } else if (mounted) {
      setState(() => _enabling = false);
    }
  }

  bool get _isApplePlatform => Platform.isIOS || Platform.isMacOS;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      icon: const Icon(Icons.bluetooth_disabled, color: Colors.red, size: 48),
      title: const Text('Bluetooth is Off'),
      content: Text(
        _isApplePlatform
            ? 'Bluetooth is currently turned off. BLE scanning requires '
                'Bluetooth to be enabled.\n\n'
                'Please enable Bluetooth in Settings.'
            : 'Bluetooth is currently turned off. BLE scanning requires '
                'Bluetooth to be enabled.\n\n'
                'Would you like to turn on Bluetooth?',
      ),
      actions: <Widget>[
        TextButton(
          onPressed: () => Navigator.of(context).pop(false),
          child: const Text('Cancel'),
        ),
        if (_isApplePlatform)
          FilledButton.icon(
            onPressed: _ble.openBluetoothSettings,
            icon: const Icon(Icons.settings),
            label: const Text('Open Settings'),
          )
        else ...[
          OutlinedButton.icon(
            onPressed: _ble.openBluetoothSettings,
            icon: const Icon(Icons.settings),
            label: const Text('Settings'),
          ),
          FilledButton.icon(
            onPressed: _enabling ? null : _enableBluetooth,
            icon: _enabling
                ? const SizedBox(
                    width: 16,
                    height: 16,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.bluetooth),
            label: const Text('Turn On'),
          ),
        ],
      ],
    );
  }
}
1
likes
150
points
276
downloads

Publisher

verified publishersteenbakker.dev

Weekly Downloads

A Flutter package for scanning BLE data in central mode.

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, json_annotation

More

Packages that depend on flutter_ble_central

Packages that implement flutter_ble_central