Connect to Bluetooth devices with StreamProvider and Flutter Reactive BLE Part 2

Apr, 08 2022

This is the next article in the series about how to combine Flutter Provider with Reactive BLE for Bluetooth devices.

The previous article discussed how to scan and connect to Bluetooth devices using a Consumer widget.

Let’s work on interacting with the device after we’ve connected to it. We are looking to do things such as: read, write, and subscribe to our peripheral’s characteristics.

I’ll be assuming your code is up to date with the previous post. Take a look at this branch if you want to start fresh.

Consumer Widget with Reactive BLE Connection State

Let’s take a look at what happens after we tap the ListTile, connecting to the device we’re observing. It should be around line 62, the onTap call back.

onTap: () async { widget.stopScan(); widget.deviceConnector.connect(device.id); }),

Add a route builder to navigate to a new screen so we can interact with our device.

Create a new file in the lib/src/ui directory named device_interactor_screen.dart. Put the following code in.

import 'package:flutter/material.dart'; class DeviceInteractorScreen extends StatelessWidget { const DeviceInteractorScreen({Key? key}) : super(key: key); Widget build(BuildContext context) { return Scaffold( body: Container(), ); } }

Then, update the onTap callback with a Navigator.push call so that we can get to our new screen.

onTap: () async { widget.stopScan(); widget.deviceConnector.connect(device.id); // New! await Navigator.push( context, MaterialPageRoute( builder: (context) => DeviceInteractorScreen(deviceId: device.id)), ); // Once we return, disconnect and start scanning again widget.deviceConnector.disconnect(device.id); widget.startScan([]); }),

In our device_interactor_screen.dart we’ll need to add some imports so we can observe the connection state of our device. Up top add the following:

import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:provider/provider.dart'; import 'package:stream_provider_ble/src/ble/ble_device_interactor.dart';

Next, in the Scaffold widget, let’s replace our empty container with a Consumer2 widget from the Provider package. We’ll then check the connection states and conditionally return a Text widget, like so:

import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:provider/provider.dart'; import 'package:stream_provider_ble/src/ble/ble_device_interactor.dart'; class DeviceInteractorScreen extends StatelessWidget { final String deviceId; const DeviceInteractorScreen({Key? key, required this.deviceId}) : super(key: key); Widget build(BuildContext context) { return Scaffold( body: Center( child: Consumer2<ConnectionStateUpdate, BleDeviceInteractor>( builder: (_, connectionStateUpdate, deviceInteractor, __) { if (connectionStateUpdate.connectionState == DeviceConnectionState.connected) { return Text('connected'); } else if (connectionStateUpdate.connectionState == DeviceConnectionState.connecting) { return Text('connecting'); } else { return Text('error'); } }, ), ), ); } }

For now we’re just observing the connection state. Let’s subscribe to a characteristic from our peripheral device.

Consumer Widget with Reactive BLE Device Interactor

Underneath our DeviceInteractorScreen widget, let’s add a new stateful widget in the same file.

class DeviceInteractor extends StatefulWidget { final BleDeviceInteractor deviceInteractor; final String deviceId; const DeviceInteractor( {Key? key, required this.deviceInteractor, required this.deviceId}) : super(key: key); State<DeviceInteractor> createState() => _DeviceInteractorState(); } class _DeviceInteractorState extends State<DeviceInteractor> { final Uuid _myServiceUuid = Uuid.parse("19b10000-e8f2-537e-4f6c-6969768a1214"); final Uuid _myCharacteristicUuid = Uuid.parse("19b10001-e8f2-537e-4f6c-6969768a1214"); Stream<List<int>>? subscriptionStream; Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('connected'), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: subscriptionStream != null ? null : () async { setState(() { subscriptionStream = widget.deviceInteractor.subScribeToCharacteristic( QualifiedCharacteristic( characteristicId: _myCharacteristicUuid, serviceId: _myServiceUuid, deviceId: widget.deviceId), ); }); }, child: const Text('subscribe'), ), const SizedBox( width: 20, ), ElevatedButton( onPressed: () { Navigator.pop(context); }, child: const Text('disconnect'), ), ], ), subscriptionStream != null ? StreamBuilder<List<int>>( stream: subscriptionStream, builder: (context, snapshot) { if (snapshot.hasData) { print(snapshot.data); return Text(snapshot.data.toString()); } return const Text('No data yet'); }) : const Text('Stream not initalized') ], ); } }

There’s a lot to unpack here. We are building a column that contains a few main components. Our ‘connected’ Text widget is still the first thing.

Next, we have a row containing an ElevatedButton which will subscribe to our characteristic.

This button checks the nullable subscriptionStream we defined at the top of the widget. If it is empty (i.e. we haven’t subscribed yet) provide the onTap callback. If it is NOT empty (we’re listening to our characteristic), then just null out the button.

The onTap callback which subscribes to the characteristic uses the deviceInteractor that we passed the widget.

The deviceInteractor has a method subscribeToCharacteristic. We pass that method a QualifiedCharacteristic and we assign the result to our nullable subscriptionStream and call for a rebuild with setState.

In that same Row we have a disconnect button which just navigates back to the device_list page.

Once the Navigator.pop is called, the following code in the device_list onTap callback executes which terminates the connection and resumes scanning:

widget.deviceConnector.disconnect(device.id); widget.startScan([]);

Finally, we check if the subscriptionStream is not null and return our StreamBuilder widget. Here we just output the characteristic data into a Text widget. This is the meat of the program and is where most of the utility of Bluetooth comes in.

And that’s it, that’s how you subscribe to a Bluetooth characteristic with Flutter Reactive BLE. Check out the Github repo for this project.