Is there a way to access data coming from BLE device from single file which keeps updating? - flutter

I want to access characteristic values of BLE from one dart file, What I am doing is that I am connecting the device from one activity and then sending the device info to all other activities. But to get values I have to write the same code again and again to all activities/dart files.
For example i am connecting device in an activity like this:
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();
print('DEVICE CONNECTED');
return BluetoothConnectedSuccess(device: r.device);
Here device: r.device is the device that i have connected to my Flutter App. Now if i want to display device data i have to initilaze these lines of code everytime i jump to any screen/activity:
class BluetoothConnectedSuccess extends StatefulWidget {
const BluetoothConnectedSuccess({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
#override
_BluetoothConnectedSuccessState createState() =>
_BluetoothConnectedSuccessState();
}
class _BluetoothConnectedSuccessState extends State<BluetoothConnectedSuccess> {
// BLE
final String SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
final String CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
bool isReady;
Stream<List<int>> stream;
List<int> lastValue;
List<double> traceDust = List();
#override
void initState() {
super.initState();
isReady = false;
connectToDevice();
}
connectToDevice() async {
await widget.device.connect();
discoverServices();
}
discoverServices() async {
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) {
characteristic.setNotifyValue(!characteristic.isNotifying);
stream = characteristic.value;
print(stream);
lastValue = characteristic.lastValue;
print(lastValue);
setState(() {
isReady = true;
});
}
});
}
});
}
_dataParser(List<int> data) {
var value = Uint8List.fromList(data);
print("stream.value: $value"); // stream.value: [33]
var hr = ByteData.sublistView(value, 0, 1);
print("Heart rate: ${hr.getUint8(0)}");
return hr.getUint8(0); // Heart rate: 33
}
It's creating a lot of mess to write the same code again and again to the activities where BLE data is needed.
Is there a way to only call this connected device from a single file instead of initializing the same code in every activity?
This is the link to my repo for a look at what I am doing on every activity/screen with BLE device data.
Please help me out as I am new to Flutter. Thank you

Firstly learn basic of state management using Get you can refer to my code here what happens is every time something changes or upadtes it will immediate show in UI using specific Get Widgets like Obx and GetX , these widgets listen to changes in value which are marked with obs (observable).
for exmaple :
Obx(
() => ble.isScan.value ? LinearProgressIndicator() : SizedBox(),
),
this will observe the changes in isScan value .
class BleServices extends GetxController {
FlutterBlue blue = FlutterBlue.instance;
BluetoothDevice d;
var connectedDevices = List<BluetoothDevice>().obs;
var scanResults = List<ScanResult>().obs;
var bleState = BluetoothState.off.obs;
var isScan = false.obs;
var scanRequire = false.obs;
var bluetoothServices = List<BluetoothService>().obs;
var discoveringServices = false.obs;
var characteristics = List<BluetoothCharacteristic>().obs;
#override
void onInit() async {
super.onInit();
final perb = await Permission.bluetooth.status.isGranted;
final perL = await Permission.location.status.isGranted;
if (perb && perL) {
getConnectedDevices();
} else {
await Permission.bluetooth.request();
await Permission.location.request();
}
isScanning();
state();
}
isDiscovering() async {}
getConnectedDevices() async {
final connectedDevice = await blue.connectedDevices;
connectedDevices.value = connectedDevice;
AppLogger.print('connected devices : $connectedDevice');
if (connectedDevice.length == 0) {
scanRequire.value = true;
searchDevices();
}
return connectedDevice;
}
searchDevices() {
// AppLogger.print('pppppppppppppp');
blue
.scan(timeout: Duration(seconds: 20))
.distinct()
.asBroadcastStream()
.listen((event) {
AppLogger.print(event.toString());
scanResults.addIf(!scanResults.contains(event), event);
});
Future.delayed(Duration(seconds: 20), () {
blue.stopScan();
Get.showSnackbar(GetBar(
message: 'scan is finished',
));
});
}
isScanning() {
blue.isScanning.listen((event) {
AppLogger.print(event.toString());
isScan.value = event;
});
}
state() {
blue.state.listen((event) {
AppLogger.print(event.toString());
bleState.value = event;
});
}
}

Related

Unhandled Exception: Bad state: Tried to use PaginationNotifier after `dispose` was called

I have a StateNotifierProvider that calls an async function which loads some images from the internal storage and adds them to the AsyncValue data:
//Provider declaration
final paginationImagesProvider = StateNotifierProvider.autoDispose<PaginationNotifier, AsyncValue<List<Uint8List?>>>((ref) {
return PaginationNotifier(folderId: ref.watch(localStorageSelectedFolderProvider), itemsPerBatch: 100, ref: ref);
});
//Actual class with AsyncValue as State
class PaginationNotifier extends StateNotifier<AsyncValue<List<Uint8List?>>> {
final int itemsPerBatch;
final String folderId;
final Ref ref;
int _numberOfItemsInFolder = 0;
bool _alreadyFetching = false;
bool _hasMoreItems = true;
PaginationNotifier({required this.itemsPerBatch, required this.folderId, required this.ref}) : super(const AsyncValue.loading()) {
log("PaginationNotifier created with folderId: $folderId, itemsPerBatch: $itemsPerBatch");
init();
}
final List<Uint8List?> _items = [];
void init() {
if (_items.isEmpty) {
log("fetchingFirstBatch");
_fetchFirstBatch();
}
}
Future<List<Uint8List?>> _fetchNextItems() async {
List<AssetEntity> images = (await (await PhotoManager.getAssetPathList())
.firstWhere((element) => element.id == folderId)
.getAssetListRange(start: _items.length, end: _items.length + itemsPerBatch));
List<Uint8List?> newItems = [];
for (AssetEntity image in images) {
newItems.add(await image.thumbnailData);
}
return newItems;
}
void _updateData(List<Uint8List?> result) {
if (result.isEmpty) {
state = AsyncValue.data(_items);
} else {
state = AsyncValue.data(_items..addAll(result));
}
_hasMoreItems = _numberOfItemsInFolder > _items.length;
}
Future<void> _fetchFirstBatch() async {
try {
_numberOfItemsInFolder = await (await PhotoManager.getAssetPathList()).firstWhere((element) => element.id == folderId).assetCountAsync;
state = const AsyncValue.loading();
final List<Uint8List?> result = await _fetchNextItems();
_updateData(result);
} catch (e, stk) {
state = AsyncValue.error(e, stk);
}
}
Future<void> fetchNextBatch() async {
if (_alreadyFetching || !_hasMoreItems) return;
_alreadyFetching = true;
log("data updated");
state = AsyncValue.data(_items);
try {
final result = await _fetchNextItems();
_updateData(result);
} catch (e, stk) {
state = AsyncValue.error(e, stk);
log("error catched");
}
_alreadyFetching = false;
}
}
Then I use a scroll controller attached to a CustomScrollView in order to call fetchNextBatch() when the scroll position changes:
#override
void initState() {
if (!controller.hasListeners && !controller.hasClients) {
log("listener added");
controller.addListener(() {
double maxScroll = controller.position.maxScrollExtent;
double position = controller.position.pixels;
if ((position > maxScroll * 0.2 || position == 0) && ref.read(paginationImagesProvider.notifier).mounted) {
ref.read(paginationImagesProvider.notifier).fetchNextBatch();
}
});
}
super.initState();
}
The problem is that when the StateNotifierProvider is fetching more data in the async function fetchNextBatch() and I go back on the navigator (like navigator.pop()), Flutter gives me an error:
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Tried to use PaginationNotifier after dispose was called.
Consider checking mounted.
I think that the async function responsible of loading data completes after I've popped the page from the Stack (which triggers a Provider dispose).
I'm probably missing something and I still haven't found a fix for this error, any help is appreciated.

Why I got : Error: MissingPluginException(No implementation found for method startScan on channel flutter_bluetooth_brazilian/methods)?

I'm trying to connect devices with Bluetooth and then share data. The problem now is that I always get: Missing Plugin Exception for flutter_bluetooth bluetooth_Brazilian.
this is bluetooth_manager.dart code:
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';
import 'bluetooth_device.dart';
/// A BluetoothManager.
class BluetoothManager {
static const String NAMESPACE = 'flutter_bluetooth_brazilian';
static const int CONNECTED = 1;
static const int DISCONNECTED = 0;
static const MethodChannel _channel =
const MethodChannel('$NAMESPACE/methods');
static const EventChannel _stateChannel =
const EventChannel('$NAMESPACE/state');
Stream<MethodCall> get _methodStream => _methodStreamController.stream;
final StreamController<MethodCall> _methodStreamController =
StreamController.broadcast();
BluetoothManager._() {
_channel.setMethodCallHandler((MethodCall call) {
_methodStreamController.add(call);
return;
});
}
static BluetoothManager _instance = BluetoothManager._();
static BluetoothManager get instance => _instance;
// Future<bool> get isAvailable async =>
// await _channel.invokeMethod('isAvailable').then<bool>((d) => d);
// Future<bool> get isOn async =>
// await _channel.invokeMethod('isOn').then<bool>((d) => d);
Future<bool> get isConnected async =>
await _channel.invokeMethod('isConnected');
BehaviorSubject<bool> _isScanning = BehaviorSubject.seeded(false);
Stream<bool> get isScanning => _isScanning.stream;
BehaviorSubject<List<BluetoothDevice>> _scanResults =
BehaviorSubject.seeded([]);
Stream<List<BluetoothDevice>> get scanResults => _scanResults.stream;
PublishSubject _stopScanPill = new PublishSubject();
/// Gets the current state of the Bluetooth module
Stream<int> get state async* {
yield await _channel.invokeMethod('state').then((s) => s);
yield* _stateChannel.receiveBroadcastStream().map((s) => s);
}
/// Starts a scan for Bluetooth Low Energy devices
/// Timeout closes the stream after a specified [Duration]
Stream<BluetoothDevice> scan({
Duration timeout,
}) async* {
if (_isScanning.value == true) {
throw Exception('Another scan is already in progress.');
}
// Emit to isScanning
_isScanning.add(true);
final killStreams = <Stream>[];
killStreams.add(_stopScanPill);
if (timeout != null) {
killStreams.add(Rx.timer(null, timeout));
}
// Clear scan results list
_scanResults.add(<BluetoothDevice>[]);
try {
await _channel.invokeMethod('startScan');
} catch (e) {
print('Error starting scan.');
_stopScanPill.add(null);
_isScanning.add(false);
throw e;
}
yield* BluetoothManager.instance._methodStream
.where((m) => m.method == "ScanResult")
.map((m) => m.arguments)
.takeUntil(Rx.merge(killStreams))
.doOnDone(stopScan)
.map((map) {
final device = BluetoothDevice.fromJson(Map<String, dynamic>.from(map));
final List<BluetoothDevice> list = _scanResults.value;
int newIndex = -1;
list.asMap().forEach((index, e) {
if (e.address == device.address) {
newIndex = index;
}
});
if (newIndex != -1) {
list[newIndex] = device;
} else {
list.add(device);
}
_scanResults.add(list);
return device;
});
}
Future startScan({
Duration timeout,
}) async {
await scan(timeout: timeout).drain();
return _scanResults.value;
}
/// Stops a scan for Bluetooth Low Energy devices
Future stopScan() async {
await _channel.invokeMethod('stopScan');
_stopScanPill.add(null);
_isScanning.add(false);
}
Future<dynamic> connect(BluetoothDevice device) =>
_channel.invokeMethod('connect', device.toJson());
Future<dynamic> disconnect() => _channel.invokeMethod('disconnect');
Future<dynamic> destroy() => _channel.invokeMethod('destroy');
Future<dynamic> writeData(List<int> bytes) {
Map<String, Object> args = Map();
args['bytes'] = bytes;
args['length'] = bytes.length;
_channel.invokeMethod('writeData', args);
return Future.value(true);
}
}
flutter_bluetooth_brazilian doesn't support the web platform. You can check the supported platforms right on the plugin's page at https://pub.dev.

Riverpod future provider not rebuilding ui

My problem is that when I run the app, the data doesn't show up on the UI. The code below is rendered under a bottom navigation bar format which is a stateful widget. To my knowledge the below code should work (show data on the initial running of app).
The code works but the data is only shown when I press hot reload. I've tried everything that I know but it still doesn't show data when I start the app.
final imageControllerProvider = Provider((ref) {
return ImageController();
});
final mainScreenImages = FutureProvider<List<String>>((ref) async {
List<String> list = [];
list = await ref.watch(imageControllerProvider).getImages();
return list;
});
class ImageController{
Future<List<String>> getImages() async {
List<String> imageUrls = [];
try {
final Reference reference = _storage.ref().child("weed/");
reference.listAll().then((value) {
for (var element in value.items) {
element.getDownloadURL().then((e) => imageUrls.add(e));
}
});
} catch (e) {
print(e);
}
return imageUrls;
}
}
class GenerateImages extends ConsumerWidget {
const GenerateImages({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final imageList = ref.watch(mainScreenImages);
final double screenwidth = MediaQuery.of(context).size.width;
final double screenheight = MediaQuery.of(context).size.height;
return imageList.when(data: (data) {
return Text('$data');
}, error: (_, __) {
return const Scaffold(
body: Center(
child: Text("OOPS"),
),
);
}, loading: () {
return const Center(child: const CircularProgressIndicator());
});
}
}
I think the problem is because in getImages() you are not awaiting the results instead you are using the then() handler to register callbacks. Replace your getImages() function with this and try.
Future<List<String>> getImages() async {
List<String> imageUrls = [];
try {
final Reference reference = _storage.ref().child("weed/");
final value = await reference.listAll();
for (var element in value.items) {
final url = await element.getDownloadURL();
imageUrls.add(url);
}
} catch (e) {
print(e);
}
return imageUrls;
}
}

flutter context become null if list is not empty

I have a form to create/update activity which is a stateful widget like this:
class ActivityForm extends StatefulWidget {
final Activity activity;
final bool isEdit;
const ActivityForm({Key key, this.activity, this.isEdit}) : super(key: key);
#override
_ActivityFormState createState() => _ActivityFormState();
}
class _ActivityFormState extends State<ActivityForm> {
final _formKey = GlobalKey<FormState>();
List<int> activityDetailIdToBeDeleted;
...
void submitHandler() async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
Navigator.pop(context);
}
in my form there are some detail list of activities. When i press delete it adds the id of
the activity detail to the list of "to be deleted" :
IconButton(
icon: Icon(Icons.delete),
onPressed: () => removeRincianPekerjaan(
activityDetail.description),),
here's the function:
void removeRincianPekerjaan(desc) {
if (widget.isEdit) {
int detailId = rincianPekerjaanList
.where((element) => element.description == desc)
.first
.id;
if (!activityDetailIdToBeDeleted.contains(detailId))
activityDetailIdToBeDeleted.add(detailId);
print(activityDetailIdToBeDeleted);
}
setState(() {
rincianPekerjaanList
.removeWhere((element) => element.description == desc);
});
}
that list of ids will be sent to provider which looks like this:
Future<bool> updateActivity(
Activity activity, List<int> activityDetailToBeDeleted) async {
final response = await ActivityService()
.updateActivity(activity.id.toString(), activity.toMap());
if (response != null) {
// Update Success.
if (response.statusCode == 200) {
var body = json.decode(response.body);
removeActivityToList(activity);
activity = Activity.fromMap(body['activity']);
addActivityToList(activity);
if (activityDetailToBeDeleted.isNotEmpty) {
print("to be deleted is not empty.");
print("ID to be deleted: ${activityDetailToBeDeleted.join(",")}");
final res = await ActivityService()
.deleteActivityDetail(activityDetailToBeDeleted.join(","));
if (res.statusCode == 204) {
print('ID DELETED.');
return true;
}
}
return true;
}
// Error unauthorized / token expired
else if (response.statusCode == 401) {
var reauth = await AuthProvider().refreshToken();
if (reauth) {
return await createActivity(activity);
} else {
return Future.error('unauthorized');
}
}
return Future.error('server error');
}
return Future.error('connection failed');
}
the problem is if the list is empty, or i didn't add the id to the list, builder context is not null but if i add an id to the list "to be deleted", the Navigator.pop(context) will throw an error because context is null. I don't understand why it becomes null.
full code in here
Your [context] must came from the build() method. Like this.
void submitHandler(BuildContext context) async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
});
}

Correct way to call an api by provider in fflutter?

I have been trying to make a app in flutter where an api is called and data is updated in TextField
Used provider for state management, here is the code for it.
class ProfileProvider with ChangeNotifier {
var profileData;
String _url = "http://10.0.2.2:3000/api/v1/user/loggedin_user";
void getData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString('token');
var data = await http.get(
_url,
headers: {
"accept": "application/json",
"content-type": "application/json",
'Token': token,
},
);
var infoOfPerson = json.decode(data.body);
profileData = new ProfileObject(
name: infoOfPerson['name'],
mobile: infoOfPerson['mobile'],
email: infoOfPerson['email'],
role: infoOfPerson['role'],
);
notifyListeners();
}
ProfileObject get profileInfo {
return profileData;
}
}
I am getting the data fine, now i have to show it in the UI, but sometime data is populated, sometime its not. Can someone please point me the right direction why this is happening.
Here is the code for UI.
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
final emailController = TextEditingController(text: '');
final nameController = TextEditingController(text: '');
final mobileController = TextEditingController(text: '');
var _isInit = true;
#override
void didChangeDependencies() {
if (_isInit) {
final profileData = Provider.of<ProfileProvider>(context);
profileData.getData();
if (profileData.profileInfo != null) {
emailController.text = profileData.profileInfo.name;
nameController.text = profileData.profileInfo.email;
mobileController.text = profileData.profileInfo.mobile;
}
_isInit = false;
super.didChangeDependencies();
}
}
#override
Widget build(BuildContext context) {
final profileData = Provider.of<ProfileProvider>(context);
return Scaffold(
drawer: NavigationDrawer(),
body: profileData.profileInfo == null
? Center(
child: CircularProgressIndicator(),
)
: Builder(
builder: (context) => SingleChildScrollView(
child: Padding(.....
Below the padding, there is normal TextField, can someone tell me why the data is being populated sometime and sometime its coming empty, even I wrapped it with CircularProgressIndicator() and a check the notifyListeners(); is not working there. The loader is not being shown and data is not being loaded.
Thanks
for StatelessWidget.
Inside the build method use:
Future.microtask(() async {
context.read<SomeProvider>().fetchSomething();
});
For StatefulWidgets if you want to call it once. Do this inside the initState() or didChangeDependencies (better if the latter). This will be called at the end of the frame which means after the build or rendering finishes..
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SomeProvider>().fetchSomething();
});
}
EDIT: WidgetsBinding will also work on build. I forgot on why I used microtask lol
i've created a function which called nextTick, i call it in initState and it works for now, but want to see others method
void nextTick(Function callback, [int milliseconds = 0]) {
Future.delayed(Duration(milliseconds: 0)).then((_) {
callback();
});
}
then use it like below
#override
void initState() {
super.initState();
nextTick((){
ProfileProvider profileProvider = Provider.of<ProfileProvider>(context);
profileProvider.getProfile();
});
}
Edit: i store couple of variables to manage them on ui, like isLoading, hasError and errorMessage. Here is my provider class
class ProfileProvider extends ChangeNotifier {
bool _hasError = false;
bool _isLoading = true;
String _errorMsg = '';
Profile _profileResponse;
bool get hasError => _hasError;
bool get isLoading => _isLoading;
String get errorMsg => _errorMsg;
Profile get profileResponse => _profileResponse;
Future<void> getProfile() async {
this.setLoading = true;
this.setError = false;
this.setErrorMsg = '';
try {
await dio.post('$api/p/view', data: {}).then((res) {
print(res.data);
_profileResponse = Profile.fromJson(jsonDecode(res.data));
print(_profileResponse.data);
notifyListeners();
}).whenComplete(() {
this.setLoading = false;
});
} catch (e) {
this.setError = true;
this.setErrorMsg = '$e';
}
this.setLoading = false;
}
set setError(bool val) {
if (val != _hasError) {
_hasError = val;
notifyListeners();
}
}
set setErrorMsg(String val) {
if (val != null && val != '') {
_errorMsg = val;
notifyListeners();
}
}
set setLoading(bool val) {
_isLoading = val;
notifyListeners();
}
}