mirror of
https://github.com/0015/ThatProject.git
synced 2026-01-12 09:17:42 +03:00
ESP32 | FLUTER | BLE | Local Notifications
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
// Copyright 2017, Paul DeMarco.
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
import 'package:flutter_blue_localnotify/widgets.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||
|
||||
void main() {
|
||||
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
runApp(FlutterBlueApp());
|
||||
}
|
||||
|
||||
class FlutterBlueApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
color: Colors.lightBlue,
|
||||
home: StreamBuilder<BluetoothState>(
|
||||
stream: FlutterBlue.instance.state,
|
||||
initialData: BluetoothState.unknown,
|
||||
builder: (c, snapshot) {
|
||||
final state = snapshot.data;
|
||||
if (state == BluetoothState.on) {
|
||||
return FindDevicesScreen();
|
||||
}
|
||||
return BluetoothOffScreen(state: state);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothOffScreen extends StatelessWidget {
|
||||
const BluetoothOffScreen({Key key, this.state}) : super(key: key);
|
||||
|
||||
final BluetoothState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.lightBlue,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.bluetooth_disabled,
|
||||
size: 200.0,
|
||||
color: Colors.white54,
|
||||
),
|
||||
Text(
|
||||
'Bluetooth Adapter is ${state.toString().substring(15)}.',
|
||||
style: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.subhead
|
||||
.copyWith(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FindDevicesScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Find Devices'),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
FlutterBlue.instance.startScan(timeout: Duration(seconds: 4)),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
StreamBuilder<List<BluetoothDevice>>(
|
||||
stream: Stream.periodic(Duration(seconds: 2))
|
||||
.asyncMap((_) => FlutterBlue.instance.connectedDevices),
|
||||
initialData: [],
|
||||
builder: (c, snapshot) => Column(
|
||||
children: snapshot.data
|
||||
.map((d) => ListTile(
|
||||
title: Text(d.name),
|
||||
subtitle: Text(d.id.toString()),
|
||||
trailing: StreamBuilder<BluetoothDeviceState>(
|
||||
stream: d.state,
|
||||
initialData: BluetoothDeviceState.disconnected,
|
||||
builder: (c, snapshot) {
|
||||
if (snapshot.data ==
|
||||
BluetoothDeviceState.connected) {
|
||||
return RaisedButton(
|
||||
child: Text('OPEN'),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
DeviceScreen(device: d))),
|
||||
);
|
||||
}
|
||||
return Text(snapshot.data.toString());
|
||||
},
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<ScanResult>>(
|
||||
stream: FlutterBlue.instance.scanResults,
|
||||
initialData: [],
|
||||
builder: (c, snapshot) => Column(
|
||||
children: snapshot.data
|
||||
.map(
|
||||
(r) => ScanResultTile(
|
||||
result: r,
|
||||
onTap: () => Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) {
|
||||
r.device.connect();
|
||||
return DeviceScreen(device: r.device);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: StreamBuilder<bool>(
|
||||
stream: FlutterBlue.instance.isScanning,
|
||||
initialData: false,
|
||||
builder: (c, snapshot) {
|
||||
if (snapshot.data) {
|
||||
return FloatingActionButton(
|
||||
child: Icon(Icons.stop),
|
||||
onPressed: () => FlutterBlue.instance.stopScan(),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
} else {
|
||||
return FloatingActionButton(
|
||||
child: Icon(Icons.search),
|
||||
onPressed: () => FlutterBlue.instance
|
||||
.startScan(timeout: Duration(seconds: 4)));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceScreen extends StatefulWidget {
|
||||
const DeviceScreen({Key key, this.device}) : super(key: key);
|
||||
|
||||
final BluetoothDevice device;
|
||||
|
||||
@override
|
||||
_DeviceScreenState createState() => _DeviceScreenState();
|
||||
}
|
||||
|
||||
class _DeviceScreenState extends State<DeviceScreen> {
|
||||
bool isConnected = false;
|
||||
|
||||
List<Widget> _buildServiceTiles(List<BluetoothService> services) {
|
||||
return services
|
||||
.map(
|
||||
(s) => ServiceTile(
|
||||
service: s,
|
||||
characteristicTiles: s.characteristics
|
||||
.map(
|
||||
(c) => CharacteristicTile(
|
||||
characteristic: c,
|
||||
onReadPressed: () => c.read(),
|
||||
onWritePressed: () => c.write([13, 24]),
|
||||
onNotificationPressed: () =>
|
||||
c.setNotifyValue(!c.isNotifying),
|
||||
descriptorTiles: c.descriptors
|
||||
.map(
|
||||
(d) => DescriptorTile(
|
||||
descriptor: d,
|
||||
onReadPressed: () => d.read(),
|
||||
onWritePressed: () => d.write([11, 12]),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
new FlutterLocalNotificationsPlugin();
|
||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
||||
var initializationSettingsAndroid =
|
||||
new AndroidInitializationSettings('app_icon');
|
||||
var initializationSettingsIOS = new IOSInitializationSettings(
|
||||
onDidReceiveLocalNotification: onDidReceiveLocalNotification);
|
||||
var initializationSettings = new InitializationSettings(
|
||||
initializationSettingsAndroid, initializationSettingsIOS);
|
||||
flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
||||
onSelectNotification: onSelectNotification);
|
||||
|
||||
Timer.periodic(new Duration(seconds: 1), (timer) {
|
||||
FlutterBlue.instance.connectedDevices.then((value) {
|
||||
if (value.length >= 1 && !isConnected) {
|
||||
_showNotificationWithNoBadge("${widget.device.name}", "Connected");
|
||||
isConnected = true;
|
||||
} else if (value.length == 0 && isConnected) {
|
||||
_showNotificationWithNoBadge("${widget.device.name}", "Disconnected");
|
||||
isConnected = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showNotificationWithNoBadge(String title, String body) async {
|
||||
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||
'no badge channel', 'no badge name', 'no badge description',
|
||||
channelShowBadge: false,
|
||||
importance: Importance.Max,
|
||||
priority: Priority.High,
|
||||
onlyAlertOnce: true);
|
||||
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
|
||||
var platformChannelSpecifics = NotificationDetails(
|
||||
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
|
||||
await flutterLocalNotificationsPlugin
|
||||
.show(0, title, body, platformChannelSpecifics, payload: 'item x');
|
||||
}
|
||||
|
||||
Future<void> onDidReceiveLocalNotification(
|
||||
int id, String title, String body, String payload) async {
|
||||
// display a dialog with the notification details, tap ok to go to another page
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(body),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: Text('Ok'),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onSelectNotification(String payload) async {
|
||||
if (payload != null) {
|
||||
debugPrint('notification payload: ' + payload);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.device.name),
|
||||
actions: <Widget>[
|
||||
StreamBuilder<BluetoothDeviceState>(
|
||||
stream: widget.device.state,
|
||||
initialData: BluetoothDeviceState.connecting,
|
||||
builder: (c, snapshot) {
|
||||
VoidCallback onPressed;
|
||||
String text;
|
||||
switch (snapshot.data) {
|
||||
case BluetoothDeviceState.connected:
|
||||
onPressed = () => widget.device.disconnect();
|
||||
text = 'DISCONNECT';
|
||||
break;
|
||||
case BluetoothDeviceState.disconnected:
|
||||
onPressed = () => widget.device.connect();
|
||||
text = 'CONNECT';
|
||||
break;
|
||||
default:
|
||||
onPressed = null;
|
||||
text = snapshot.data.toString().substring(21).toUpperCase();
|
||||
break;
|
||||
}
|
||||
return FlatButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.button
|
||||
.copyWith(color: Colors.white),
|
||||
));
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
StreamBuilder<BluetoothDeviceState>(
|
||||
stream: widget.device.state,
|
||||
initialData: BluetoothDeviceState.connecting,
|
||||
builder: (c, snapshot) => ListTile(
|
||||
leading: (snapshot.data == BluetoothDeviceState.connected)
|
||||
? Icon(Icons.bluetooth_connected)
|
||||
: Icon(Icons.bluetooth_disabled),
|
||||
title: Text(
|
||||
'Device is ${snapshot.data.toString().split('.')[1]}.'),
|
||||
subtitle: Text('${widget.device.id}'),
|
||||
trailing: StreamBuilder<bool>(
|
||||
stream: widget.device.isDiscoveringServices,
|
||||
initialData: false,
|
||||
builder: (c, snapshot) => IndexedStack(
|
||||
index: snapshot.data ? 1 : 0,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: () => widget.device.discoverServices(),
|
||||
),
|
||||
IconButton(
|
||||
icon: SizedBox(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation(Colors.grey),
|
||||
),
|
||||
width: 18.0,
|
||||
height: 18.0,
|
||||
),
|
||||
onPressed: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<BluetoothService>>(
|
||||
stream: widget.device.services,
|
||||
initialData: [],
|
||||
builder: (c, snapshot) {
|
||||
return Column(
|
||||
children: _buildServiceTiles(snapshot.data),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright 2017, Paul DeMarco.
|
||||
// All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blue/flutter_blue.dart';
|
||||
|
||||
class ScanResultTile extends StatelessWidget {
|
||||
const ScanResultTile({Key key, this.result, this.onTap}) : super(key: key);
|
||||
|
||||
final ScanResult result;
|
||||
final VoidCallback onTap;
|
||||
|
||||
Widget _buildTitle(BuildContext context) {
|
||||
if (result.device.name.length > 0) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
result.device.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
result.device.id.toString(),
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Text(result.device.id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAdvRow(BuildContext context, String title, String value) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(title, style: Theme.of(context).textTheme.caption),
|
||||
SizedBox(
|
||||
width: 12.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.apply(color: Colors.black),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getNiceHexArray(List<int> bytes) {
|
||||
return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]'
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
String getNiceManufacturerData(Map<int, List<int>> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
List<String> res = [];
|
||||
data.forEach((id, bytes) {
|
||||
res.add(
|
||||
'${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}');
|
||||
});
|
||||
return res.join(', ');
|
||||
}
|
||||
|
||||
String getNiceServiceData(Map<String, List<int>> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
List<String> res = [];
|
||||
data.forEach((id, bytes) {
|
||||
res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}');
|
||||
});
|
||||
return res.join(', ');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionTile(
|
||||
title: _buildTitle(context),
|
||||
leading: Text(result.rssi.toString()),
|
||||
trailing: RaisedButton(
|
||||
child: Text('CONNECT'),
|
||||
color: Colors.black,
|
||||
textColor: Colors.white,
|
||||
onPressed: (result.advertisementData.connectable) ? onTap : null,
|
||||
),
|
||||
children: <Widget>[
|
||||
_buildAdvRow(
|
||||
context, 'Complete Local Name', result.advertisementData.localName),
|
||||
_buildAdvRow(context, 'Tx Power Level',
|
||||
'${result.advertisementData.txPowerLevel ?? 'N/A'}'),
|
||||
_buildAdvRow(
|
||||
context,
|
||||
'Manufacturer Data',
|
||||
getNiceManufacturerData(
|
||||
result.advertisementData.manufacturerData) ??
|
||||
'N/A'),
|
||||
_buildAdvRow(
|
||||
context,
|
||||
'Service UUIDs',
|
||||
(result.advertisementData.serviceUuids.isNotEmpty)
|
||||
? result.advertisementData.serviceUuids.join(', ').toUpperCase()
|
||||
: 'N/A'),
|
||||
_buildAdvRow(context, 'Service Data',
|
||||
getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceTile extends StatelessWidget {
|
||||
final BluetoothService service;
|
||||
final List<CharacteristicTile> characteristicTiles;
|
||||
|
||||
const ServiceTile({Key key, this.service, this.characteristicTiles})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (characteristicTiles.length > 0) {
|
||||
return ExpansionTile(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Service'),
|
||||
Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.body1
|
||||
.copyWith(color: Theme.of(context).textTheme.caption.color))
|
||||
],
|
||||
),
|
||||
children: characteristicTiles,
|
||||
);
|
||||
} else {
|
||||
return ListTile(
|
||||
title: Text('Service'),
|
||||
subtitle:
|
||||
Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CharacteristicTile extends StatelessWidget {
|
||||
final BluetoothCharacteristic characteristic;
|
||||
final List<DescriptorTile> descriptorTiles;
|
||||
final VoidCallback onReadPressed;
|
||||
final VoidCallback onWritePressed;
|
||||
final VoidCallback onNotificationPressed;
|
||||
|
||||
const CharacteristicTile(
|
||||
{Key key,
|
||||
this.characteristic,
|
||||
this.descriptorTiles,
|
||||
this.onReadPressed,
|
||||
this.onWritePressed,
|
||||
this.onNotificationPressed})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<int>>(
|
||||
stream: characteristic.value,
|
||||
initialData: characteristic.lastValue,
|
||||
builder: (c, snapshot) {
|
||||
final value = snapshot.data;
|
||||
return ExpansionTile(
|
||||
title: ListTile(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Characteristic'),
|
||||
Text(
|
||||
'0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
|
||||
style: Theme.of(context).textTheme.body1.copyWith(
|
||||
color: Theme.of(context).textTheme.caption.color))
|
||||
],
|
||||
),
|
||||
subtitle: Text(value.toString()),
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.file_download,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.5),
|
||||
),
|
||||
onPressed: onReadPressed,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.file_upload,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
|
||||
onPressed: onWritePressed,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
characteristic.isNotifying
|
||||
? Icons.sync_disabled
|
||||
: Icons.sync,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
|
||||
onPressed: onNotificationPressed,
|
||||
)
|
||||
],
|
||||
),
|
||||
children: descriptorTiles,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DescriptorTile extends StatelessWidget {
|
||||
final BluetoothDescriptor descriptor;
|
||||
final VoidCallback onReadPressed;
|
||||
final VoidCallback onWritePressed;
|
||||
|
||||
const DescriptorTile(
|
||||
{Key key, this.descriptor, this.onReadPressed, this.onWritePressed})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text('Descriptor'),
|
||||
Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.body1
|
||||
.copyWith(color: Theme.of(context).textTheme.caption.color))
|
||||
],
|
||||
),
|
||||
subtitle: StreamBuilder<List<int>>(
|
||||
stream: descriptor.value,
|
||||
initialData: descriptor.lastValue,
|
||||
builder: (c, snapshot) => Text(snapshot.data.toString()),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.file_download,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.5),
|
||||
),
|
||||
onPressed: onReadPressed,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.file_upload,
|
||||
color: Theme.of(context).iconTheme.color.withOpacity(0.5),
|
||||
),
|
||||
onPressed: onWritePressed,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdapterStateTile extends StatelessWidget {
|
||||
const AdapterStateTile({Key key, @required this.state}) : super(key: key);
|
||||
|
||||
final BluetoothState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.redAccent,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
'Bluetooth adapter is ${state.toString().substring(15)}',
|
||||
style: Theme.of(context).primaryTextTheme.subhead,
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.error,
|
||||
color: Theme.of(context).primaryTextTheme.subhead.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user