diff --git a/example/lib/connect_devices_view.dart b/example/lib/connect_devices_view.dart new file mode 100644 index 0000000..4589be3 --- /dev/null +++ b/example/lib/connect_devices_view.dart @@ -0,0 +1,202 @@ +import 'package:example/widgets/battery_info_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; + +import 'widgets/frequency_player_widget.dart'; +import 'widgets/jingle_player_widget.dart'; +import 'widgets/rgb_led_control_widget.dart'; +import 'widgets/sensor_configuration_view.dart'; +import 'widgets/audio_player_control_widget.dart'; +import 'widgets/sensor_view.dart'; +import 'widgets/storage_path_audio_player_widget.dart'; +import 'widgets/grouped_box.dart'; + +class ConnectedDevicesView extends StatelessWidget { + final List connectedDevices; + + const ConnectedDevicesView({ + Key? key, + required this.connectedDevices, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (connectedDevices.isEmpty) { + return const SizedBox.shrink(); + } + + return DefaultTabController( + length: connectedDevices.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + labelColor: Theme.of(context).primaryColor, + unselectedLabelColor: Colors.grey, + isScrollable: true, + tabs: connectedDevices + .map((device) => Tab(text: device.name)) + .toList(), + ), + Builder( + builder: (context) { + final TabController tabController = + DefaultTabController.of(context); + return AnimatedBuilder( + animation: tabController, + builder: (context, _) { + return _buildDeviceTab( + connectedDevices[tabController.index], + ); + }, + ); + }, + ), + ] + .map( + (e) => Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: e, + ), + ) + .toList(), + ), + ); + } + + /// Builds the UI for a single connected device. + Widget _buildDeviceTab(Wearable device) { + List? sensorViews = SensorView.createSensorViews(device); + List? sensorConfigurationViews = + SensorConfigurationView.createSensorConfigurationViews(device); + String? wearableIconPath = device.getWearableIconPath(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GroupedBox( + title: "Device Info", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (wearableIconPath != null) + SvgPicture.asset( + wearableIconPath, + width: 100, + height: 100, + ), + SelectableText("Name: ${device.name}"), + if (device is DeviceIdentifier) + FutureBuilder( + future: (device as DeviceIdentifier).readDeviceIdentifier(), + builder: (context, snapshot) { + return SelectableText( + "Device Identifier: ${snapshot.data}", + ); + }, + ), + if (device is DeviceFirmwareVersion) + FutureBuilder( + future: (device as DeviceFirmwareVersion) + .readDeviceFirmwareVersion(), + builder: (context, snapshot) { + return SelectableText( + "Firmware Version: ${snapshot.data}", + ); + }, + ), + if (device is DeviceHardwareVersion) + FutureBuilder( + future: (device as DeviceHardwareVersion) + .readDeviceHardwareVersion(), + builder: (context, snapshot) { + return SelectableText( + "Hardware Version: ${snapshot.data}", + ); + }, + ), + ], + ), + ), + BatteryInfoWidget(connectedDevice: device), + if (device is RgbLed && device is StatusLed) + GroupedBox( + title: "RGB LED", + child: RgbLedControlWidget( + rgbLed: device as RgbLed, + statusLed: device as StatusLed, + ), + ), + if (device is RgbLed && device is! StatusLed) + GroupedBox( + title: "RGB LED", + child: RgbLedControlWidget(rgbLed: device as RgbLed), + ), + if (device is FrequencyPlayer) + GroupedBox( + title: "Frequency Player", + child: FrequencyPlayerWidget( + frequencyPlayer: device as FrequencyPlayer, + ), + ), + if (device is JinglePlayer) + GroupedBox( + title: "Jingle Player", + child: JinglePlayerWidget( + jinglePlayer: device as JinglePlayer, + ), + ), + if (device is StoragePathAudioPlayer) + GroupedBox( + title: "Storage Path Audio Player", + child: StoragePathAudioPlayerWidget( + audioPlayer: device as StoragePathAudioPlayer, + ), + ), + if (device is AudioPlayerControls) + GroupedBox( + title: "Audio Player Controls", + child: AudioPlayerControlWidget( + audioPlayerControls: device as AudioPlayerControls, + ), + ), + if (sensorConfigurationViews != null && + sensorConfigurationViews.isNotEmpty) + GroupedBox( + title: "Sensor Configurations", + child: Column( + children: sensorConfigurationViews, + ), + ), + if (sensorViews != null && sensorViews.isNotEmpty) + GroupedBox( + title: "Sensors", + child: Column( + children: sensorViews + .map( + (e) => Padding( + padding: const EdgeInsets.only( + top: 6.0, + bottom: 6.0, + ), + child: e, + ), + ) + .toList(), + ), + ), + ].map( + (e) { + return Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0, + ), + child: e, + ); + }, + ).toList(), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 1637d65..40c5f84 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,25 +1,16 @@ import 'dart:async'; -import 'package:example/widgets/battery_info_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:open_earable_flutter/open_earable_flutter.dart'; -import 'widgets/frequency_player_widget.dart'; -import 'widgets/jingle_player_widget.dart'; -import 'widgets/rgb_led_control_widget.dart'; -import 'widgets/sensor_configuration_view.dart'; -import 'widgets/audio_player_control_widget.dart'; -import 'widgets/sensor_view.dart'; -import 'widgets/storage_path_audio_player_widget.dart'; -import 'widgets/grouped_box.dart'; +import 'connect_devices_view.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { - const MyApp({super.key}); + const MyApp({Key? key}) : super(key: key); @override MyAppState createState() => MyAppState(); @@ -30,232 +21,91 @@ class MyAppState extends State { StreamSubscription? _scanSubscription; List discoveredDevices = []; - DiscoveredDevice? _connectingDevice; - Wearable? _connectedDevice; + // Lists for handling multiple connected (and connecting) devices. + final List _connectedDevices = []; + final List _connectingDevices = []; @override Widget build(BuildContext context) { - List? sensorViews; - List? sensorConfigurationViews; - if (_connectedDevice != null) { - sensorViews = SensorView.createSensorViews(_connectedDevice!); - sensorConfigurationViews = - SensorConfigurationView.createSensorConfigurationViews( - _connectedDevice!, - ); - } - - String? wearableIconPath = _connectedDevice?.getWearableIconPath(); - - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Bluetooth Devices'), + return _AppLayout( + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(33, 16, 0, 0), + child: Text( + "SCANNED DEVICES", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12.0, + ), + ), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.fromLTRB(33, 16, 0, 0), - child: Text( - "SCANNED DEVICES", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12.0, - ), - ), - ), - Visibility( - visible: discoveredDevices.isNotEmpty, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Colors.grey, - width: 1.0, - ), - borderRadius: BorderRadius.circular(8.0), - ), - child: ListView.builder( - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - // Disable scrolling, - shrinkWrap: true, - itemCount: discoveredDevices.length, - itemBuilder: (BuildContext context, int index) { - final device = discoveredDevices[index]; - return Column( - children: [ - ListTile( - textColor: Colors.black, - selectedTileColor: Colors.grey, - title: Text(device.name), - titleTextStyle: const TextStyle(fontSize: 16), - visualDensity: const VisualDensity( - horizontal: -4, vertical: -4), - trailing: _buildTrailingWidget(device.id), - onTap: () { - _connectToDevice(device); - }, - ), - if (index != discoveredDevices.length - 1) - const Divider( - height: 1.0, - thickness: 1.0, - color: Colors.grey, - indent: 16.0, - endIndent: 0.0, - ), - ], - ); - }, - ), - ), - ), - Center( - child: ElevatedButton( - onPressed: _startScanning, - child: const Text('Restart Scan'), - ), + Visibility( + visible: discoveredDevices.isNotEmpty, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey, + width: 1.0, ), - if (_connectedDevice != null) - GroupedBox( - title: "Device Info", - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (wearableIconPath != null) - SvgPicture.asset( - wearableIconPath, - width: 100, - height: 100, - ), - SelectableText( - "Name: ${_connectedDevice?.name}", + borderRadius: BorderRadius.circular(8.0), + ), + child: ListView.builder( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: discoveredDevices.length, + itemBuilder: (BuildContext context, int index) { + final device = discoveredDevices[index]; + return Column( + children: [ + ListTile( + textColor: Colors.black, + selectedTileColor: Colors.grey, + title: Text(device.name), + titleTextStyle: const TextStyle(fontSize: 16), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, ), - if (_connectedDevice is DeviceIdentifier) - FutureBuilder( - future: (_connectedDevice as DeviceIdentifier) - .readDeviceIdentifier(), - builder: (context, snapshot) { - return SelectableText( - "Device Identifier: ${snapshot.data}", - ); - }, - ), - if (_connectedDevice is DeviceFirmwareVersion) - FutureBuilder( - future: (_connectedDevice as DeviceFirmwareVersion) - .readDeviceFirmwareVersion(), - builder: (context, snapshot) { - return SelectableText( - "Firmware Version: ${snapshot.data}", - ); - }, - ), - if (_connectedDevice is DeviceHardwareVersion) - FutureBuilder( - future: (_connectedDevice as DeviceHardwareVersion) - .readDeviceHardwareVersion(), - builder: (context, snapshot) { - return SelectableText( - "Hardware Version: ${snapshot.data}", - ); - }, - ), - ], - ), - ), - if (_connectedDevice != null) - BatteryInfoWidget(connectedDevice: _connectedDevice!), - if (_connectedDevice is RgbLed && _connectedDevice is StatusLed) - GroupedBox( - title: "RGB LED", - child: - RgbLedControlWidget( - rgbLed: _connectedDevice as RgbLed, - statusLed: _connectedDevice as StatusLed?, + trailing: _buildTrailingWidget(device.id), + onTap: () { + _connectToDevice(device); + }, ), - ), - if (_connectedDevice is RgbLed && _connectedDevice is! StatusLed) - GroupedBox( - title: "RGB LED", - child: - RgbLedControlWidget(rgbLed: _connectedDevice as RgbLed), - ), - if (_connectedDevice is FrequencyPlayer) - GroupedBox( - title: "Frequency Player", - child: FrequencyPlayerWidget( - frequencyPlayer: _connectedDevice as FrequencyPlayer, - ), - ), - if (_connectedDevice is JinglePlayer) - GroupedBox( - title: "Jingle Player", - child: JinglePlayerWidget( - jinglePlayer: _connectedDevice as JinglePlayer, - ), - ), - if (_connectedDevice is StoragePathAudioPlayer) - GroupedBox( - title: "Storage Path Audio Player", - child: StoragePathAudioPlayerWidget( - audioPlayer: _connectedDevice as StoragePathAudioPlayer, - ), - ), - if (_connectedDevice is AudioPlayerControls) - GroupedBox( - title: "Audio Player Controls", - child: AudioPlayerControlWidget( - audioPlayerControls: - _connectedDevice as AudioPlayerControls, - ), - ), - if (sensorConfigurationViews != null) - GroupedBox( - title: "Sensor Configurations", - child: Column( - children: sensorConfigurationViews, - ), - ), - if (sensorViews != null) - GroupedBox( - title: "Sensors", - child: Column( - children: sensorViews - .map((e) => Padding( - padding: const EdgeInsets.only( - bottom: 6.0, - top: 6.0, - ), - child: e, - )) - .toList(), - ), - ), - ] - .map((e) => Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - top: 8.0, + if (index != discoveredDevices.length - 1) + const Divider( + height: 1.0, + thickness: 1.0, + color: Colors.grey, + indent: 16.0, + endIndent: 0.0, ), - child: e, - )) - .toList(), + ], + ); + }, + ), ), - )), - ), + ), + Center( + child: ElevatedButton( + onPressed: _startScanning, + child: const Text('Restart Scan'), + ), + ), + const SizedBox(height: 16.0), + ConnectedDevicesView(connectedDevices: _connectedDevices), + ], ); } + /// Returns the trailing widget for each discovered device. + /// A green check is shown if the device is connected, or a + /// circular progress indicator if it is in the process of connecting. Widget _buildTrailingWidget(String id) { - if (_connectedDevice?.deviceId == id) { - return const Icon(size: 24, Icons.check, color: Colors.green); - } else if (_connectingDevice?.id == id) { + if (_connectedDevices.any((d) => d.deviceId == id)) { + return const Icon(Icons.check, color: Colors.green, size: 24); + } else if (_connectingDevices.any((d) => d.id == id)) { return const SizedBox( height: 24, width: 24, @@ -265,6 +115,7 @@ class MyAppState extends State { return const SizedBox.shrink(); } + /// Starts scanning for devices. void _startScanning() async { _wearableManager.startScan(); _scanSubscription?.cancel(); @@ -278,24 +129,66 @@ class MyAppState extends State { }); } + /// Connects to the tapped device. + /// + /// The device is first added to [_connectingDevices] (to show a progress indicator) + /// and then, once connected, it is added to [_connectedDevices]. A disconnect listener is attached. Future _connectToDevice(device) async { setState(() { - _connectingDevice = device; + _connectingDevices.add(device); }); _scanSubscription?.cancel(); + Wearable wearable = await _wearableManager.connectToDevice(device); wearable.addDisconnectListener(() { - if (_connectedDevice?.deviceId == wearable.deviceId) { - setState(() { - _connectedDevice = null; - }); - } + setState(() { + _connectedDevices.removeWhere((d) => d.deviceId == wearable.deviceId); + }); }); setState(() { - _connectingDevice = null; - _connectedDevice = wearable; + _connectingDevices.removeWhere((d) => d.id == device.id); + _connectedDevices.add(wearable); }); } } + +class _AppLayout extends StatelessWidget { + final List children; + + const _AppLayout({ + Key? key, + required this.children, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Bluetooth Devices'), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children + .map( + (e) => Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0, + ), + child: e, + ), + ) + .toList(), + ), + ), + ), + ), + ); + } +}