Initialize a null safe variable without a default constructor? - flutter

I'm sure this problem has been asked before, but I can't figure out how to even properly word it.
I am trying to get Bluetooth Device data into my flutter app. The examples I have found either use non-null safe versions of dart code or they hide all of the important details.
I am trying to build a very simple prototype from scratch so I can get a better grasp on things.
I want to make a stateful widget that updates based on notifyListeners(). The idea is I start out with "noName bluetooth device", then once I have a device connected, I can update the object and display the name of the connected device.
I keep running into this same roadblock and I can't get past it. I want to make a default Bluetooth Device, but the device has no default constructor. The default device cannot be null because of null safety.
Can someone help me figure this out. There is something I know I am fundamentally misunderstanding, but I don't know where to start.
The code below makes a ChangeNotifierProvider the parent of my BlueTesting widget that should display details of the connected Bluetooth Device (I haven't written all of the code yet).
The BTDevices class should update the Bluetooth Device object, and notify the app to display the updated data from "the default empty device" to the new connected device.
Thank you for your help!
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:provider/provider.dart';
void main() => runApp(
ChangeNotifierProvider(create: (_) => BTDevices(), child: BlueTesting()));
class BTDevices extends ChangeNotifier {
BluetoothDevice device = BluetoothDevice();
void setDevice(BluetoothDevice newDevice) {
device = newDevice;
notifyListeners();
}
}
UPDATE:
I tried updating the above code to set:
BluetoothDevice device = null;
A value of type 'Null' can't be assigned to a variable of type 'BluetoothDevice'.
Try changing the type of the variable, or casting the right-hand type to 'BluetoothDevice'.dartinvalid_assignment
here is the information on the BluetoothDevice definition:
part of flutter_blue;
class BluetoothDevice {
final DeviceIdentifier id;
final String name;
final BluetoothDeviceType type;
BluetoothDevice.fromProto(protos.BluetoothDevice p)
: id = new DeviceIdentifier(p.remoteId),
name = p.name,
type = BluetoothDeviceType.values[p.type.value];
BehaviorSubject<bool> _isDiscoveringServices = BehaviorSubject.seeded(false);
Stream<bool> get isDiscoveringServices => _isDiscoveringServices.stream;
/// Establishes a connection to the Bluetooth Device.
Future<void> connect({
Duration? timeout,
bool autoConnect = true,
}) async {
var request = protos.ConnectRequest.create()
..remoteId = id.toString()
..androidAutoConnect = autoConnect;
Timer? timer;
if (timeout != null) {
timer = Timer(timeout, () {
disconnect();
throw TimeoutException('Failed to connect in time.', timeout);
});
}
await FlutterBlue.instance._channel
.invokeMethod('connect', request.writeToBuffer());
await state.firstWhere((s) => s == BluetoothDeviceState.connected);
timer?.cancel();
return;
}
/// Cancels connection to the Bluetooth Device
Future disconnect() =>
FlutterBlue.instance._channel.invokeMethod('disconnect', id.toString());
BehaviorSubject<List<BluetoothService>> _services =
BehaviorSubject.seeded([]);
/// Discovers services offered by the remote device as well as their characteristics and descriptors
Future<List<BluetoothService>> discoverServices() async {
final s = await state.first;
if (s != BluetoothDeviceState.connected) {
return Future.error(new Exception(
'Cannot discoverServices while device is not connected. State == $s'));
}
var response = FlutterBlue.instance._methodStream
.where((m) => m.method == "DiscoverServicesResult")
.map((m) => m.arguments)
.map((buffer) => new protos.DiscoverServicesResult.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => p.services)
.map((s) => s.map((p) => new BluetoothService.fromProto(p)).toList())
.first
.then((list) {
_services.add(list);
_isDiscoveringServices.add(false);
return list;
});
await FlutterBlue.instance._channel
.invokeMethod('discoverServices', id.toString());
_isDiscoveringServices.add(true);
return response;
}
/// Returns a list of Bluetooth GATT services offered by the remote device
/// This function requires that discoverServices has been completed for this device
Stream<List<BluetoothService>> get services async* {
yield await FlutterBlue.instance._channel
.invokeMethod('services', id.toString())
.then((buffer) =>
new protos.DiscoverServicesResult.fromBuffer(buffer).services)
.then((i) => i.map((s) => new BluetoothService.fromProto(s)).toList());
yield* _services.stream;
}
/// The current connection state of the device
Stream<BluetoothDeviceState> get state async* {
yield await FlutterBlue.instance._channel
.invokeMethod('deviceState', id.toString())
.then((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
.then((p) => BluetoothDeviceState.values[p.state.value]);
yield* FlutterBlue.instance._methodStream
.where((m) => m.method == "DeviceState")
.map((m) => m.arguments)
.map((buffer) => new protos.DeviceStateResponse.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => BluetoothDeviceState.values[p.state.value]);
}
/// The MTU size in bytes
Stream<int> get mtu async* {
yield await FlutterBlue.instance._channel
.invokeMethod('mtu', id.toString())
.then((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
.then((p) => p.mtu);
yield* FlutterBlue.instance._methodStream
.where((m) => m.method == "MtuSize")
.map((m) => m.arguments)
.map((buffer) => new protos.MtuSizeResponse.fromBuffer(buffer))
.where((p) => p.remoteId == id.toString())
.map((p) => p.mtu);
}
/// Request to change the MTU Size
/// Throws error if request did not complete successfully
Future<void> requestMtu(int desiredMtu) async {
var request = protos.MtuSizeRequest.create()
..remoteId = id.toString()
..mtu = desiredMtu;
return FlutterBlue.instance._channel
.invokeMethod('requestMtu', request.writeToBuffer());
}
/// Indicates whether the Bluetooth Device can send a write without response
Future<bool> get canSendWriteWithoutResponse =>
new Future.error(new UnimplementedError());
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is BluetoothDevice &&
runtimeType == other.runtimeType &&
id == other.id;
#override
int get hashCode => id.hashCode;
#override
String toString() {
return 'BluetoothDevice{id: $id, name: $name, type: $type, isDiscoveringServices: ${_isDiscoveringServices.value}, _services: ${_services.value}';
}
}
enum BluetoothDeviceType { unknown, classic, le, dual }
enum BluetoothDeviceState { disconnected, connecting, connected, disconnecting }

The default device cannot be null because of null safety.
You got this backwards. Null-safety is never the reason something can or cannot be null. You decide whether something can or cannot be null. It has been this way forever, the programmer decides if a variable sometimes is null. All null-safety does is force the programmer to share this secret arcane knowledge with their compiler, so the compiler can do it's job and warn the programmer if their logic contains mistakes.
So if you think that having no device for the start of the program (sounds reasonable), then you can decide to make it nullable. Use BluetoothDevice? as the type and it is nullable. And now your compiler can tell you all the places where you (mistakenly) assume it never is null. That's the great thing about null-safety. It's here to help you with whatever choice you make, not constrain you in your choices.

Make your variable nullable
BluetoothDevice? device;
And check if device is null later

Related

Cache result of transformed stream in dart using StreamStransformer

I want to cache the result of a transformed stream (with StreamTransformer.fromHandlers) to be able to access the last event without listening to stream, similar to rxdart BehaviorSubject.
I have tried setting an internal property and expose it with getter but it has no effect and I have to wait for the stream to emit a value before it changes.
class SimpleBloc {
final BehaviorSubject<String> _contentController = BehaviorSubject();
/// Emits content value or [ValidationError] if invalid
Stream<String> get content$ => _contentController.transform(StreamTransformer.fromHandlers(handleData: (data, sink) {
if (data?.isNotEmpty != true) {
sink.addError(ValidationErrorRequired());
} else {
sink.add(data!);
}
});
String? get _content => _contentController.valueOrNull;
/// Emits boolean when content changes that indicates if bloc is valid
Stream<bool> valid$ => $content.transform(
handleData: (data, sink) => sink.add(_valid = true),
handleError: (error, sink) => sink.add(_valid = false)
);
Function(String) get setContent => _contentController.add;
bool _valid = false;
bool get valid => _valid;
}
test("bloc is invalid when content is invalid", () async {
final bloc = MyBloc();
bloc.setContent("content");
// If I expectLater before the test will pass because apparently it takes some time until transformers are run
// await expectLater(bloc.valid$, emits(true));
expect(bloc.valid, equals(true)); //test fails
});
I have noticed that maybe the getters are the issue so I tried creating the streams in the constructor to the transformers are instantiated but it hadnt had any effect:
class MyBloc {
Stream<bool> get valid$ => _validStream;
Stream<String> get content$ => _contentStream;
MyBloc() {
_contentStream = _contentController.transform(...);
_validStream = _contentStream.transform(...);
}
}
Does anyone have a solution for this?

Null check operator used on a null value when checking with if

Whenever I go on my login page, I get the error that I used null check operator on a null value, it only happens when there is no login/password entered before. In init state I am checking with isNotEmpty, so why am I getting this error? How can I fix it?
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Null check operator used on a null value
#override
void initState() {
super.initState();
UserPreferences.init().then((_) {
if (UserPreferences.getEmail!.isNotEmpty) {
setState(() {
_isChecked = true;
_email.text = UserPreferences.getEmail!;
_password.text = UserPreferences.getPassword!;
});
}
});
}
class UserPreferences {
static SharedPreferences? _preferences;
static Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
}
static setEmail(String username) async {
await _preferences?.setString('email', username);
}
static setPassword(String password) async {
await _preferences?.setString('password', password);
}
static String? get getEmail => _preferences?.getString('email');
static String? get getPassword => _preferences?.getString('password');
}
Generally speaking, using a ! in null checks means your code has bugs. If it didn't have bugs, you would not need this operator.
The ! operator only forces your compiler to go through with what you programmed, even though it knows it could be wrong. It warned you, but you decided that instead of listening to your compiler, you just told it to shut up (by using operator !). If you want to write good code, just forget the operator ! for null checks ever existed.
final previousEmail = UserPreferences.getEmail;
final previousPassword = UserPreferences.getPassword;
if (previousEmail != null) {
setState(() {
_isChecked = true;
_email.text = previousEmail;
if(previousPassword != null) {
_password.text = previousPassword;
}
});
}
Call this init inside FutureBuilder then check the Condition for non being empty. Because initState is not for Initializing the Async Function.

Use StreamSubscription as Future in Dart/Flutter

I want to connect my Flutter app to bluetooth device with flutter_blue library, and return a result (anything) when the connection is ON. But I don't understand how to do.
Here my code :
Future connect(BluetoothDevice device) async {
_btDevice = device;
StreamSubscription<BluetoothDeviceState> subscription;
subscription = device.state.listen((event) async {
if (event != BluetoothDeviceState.connected) {
await device.connect();
} else {
await device.discoverServices().then((value) => _initFeatures(value));
}
})
..onDone(() {
// Cascade
print("onDone");
});
subscription.asFuture();
//subscription.cancel();
}
And when I call this function with
await newDevice.connect(bluetoothDevice).then((value) => print('OK'));
OK is written before the real connection. _initFeatures if well call when the device is connected.
I try to use asFuture from StreamSubscription with onDone, but that change nothing.
Could you help me please ?
UPDATE 12/10
I've worked on another project for few monthes, and when I come back, I can't solve the problem, so I add the full code.
The concept is a class widget calls the connect future in other class and need to receipt the end of work.
Widget
Future<void> _connectDevice() async {
try {
widget.device.connect(widget.btDevice!).then((value) => _initValue());
} catch (e) {
print(e);
}
}
_initValue() is a method to create the rest of the screen
and the Future connect()
Future connect(BluetoothDevice device) async {
_btDevice = device;
StreamSubscription<BluetoothDeviceState> subscription;
subscription = device.state.listen((event) async {
if (event != BluetoothDeviceState.connected) {
await device.connect();
} else {
await device
.discoverServices()
.then((value) => _initFeatures(value))
.then((value) => print("OK"));
}
});
await subscription.asFuture();
await subscription.cancel();
}
What I'd like is the Future finishes when print("OK") is called, in order .then((value) => _initValue()); is called.
The problem is only this end. Maybe it's not the good way to implement this kind of solution.

How to send data to AudioServiceTask class which extends BackgroundAudioTask from UI

Well, I'm stuck on this problem. I have a code for audioservice (audioplayer.dart) which takes a queue to play. I'm getting the queue from playlist.dart in audioplayer.dart using ModalRoute and save in a global variable queue. Then, I initialize the AudioPlayerService. Now everything till here is fine but inside the AudioPlayerTask class which extends BackgroundAudioTask, when I try to access the variable (inside onStart) it comes out to be an empty list. I don't know where the problem is and I'm not very much familier with the BackgroundAudioTask class. Here's how it looks like:
import .....
List<MediaItem> queue = [];
class TempScreen extends StatefulWidget {
#override
_TempScreenState createState() => _TempScreenState();
}
class _TempScreenState extends State<TempScreen> {
#override
Widget build(BuildContext context) {
queue = ModalRoute.of(context).settings.arguments;
// NOW HERE THE QUEUE IS FINE
return Container(.....all ui code);
}
// I'm using this button to start the service
audioPlayerButton() {
AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Audio Service Demo',
androidNotificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
androidEnableQueue: true,
);
AudioService.updateQueue(queue);
print('updated queue at the start');
print('queue now is $queue');
AudioService.setRepeatMode(AudioServiceRepeatMode.none);
AudioService.setShuffleMode(AudioServiceShuffleMode.none);
AudioService.play();
}
}
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
Seeker _seeker;
StreamSubscription<PlaybackEvent> _eventSubscription;
String kUrl = '';
String key = "38346591";
String decrypt = "";
String preferredQuality = '320';
int get index => _player.currentIndex == null ? 0 : _player.currentIndex;
MediaItem get mediaItem => index == null ? queue[0] : queue[index];
// This is just a function i'm using to get song URLs
fetchSongUrl(songId) async {
print('starting fetching url');
String songUrl =
"https://www.jiosaavn.com/api.php?app_version=5.18.3&api_version=4&readable_version=5.18.3&v=79&_format=json&__call=song.getDetails&pids=" +
songId;
var res = await get(songUrl, headers: {"Accept": "application/json"});
var resEdited = (res.body).split("-->");
var getMain = jsonDecode(resEdited[1]);
kUrl = await DesPlugin.decrypt(
key, getMain[songId]["more_info"]["encrypted_media_url"]);
kUrl = kUrl.replaceAll('96', '$preferredQuality');
print('fetched url');
return kUrl;
}
#override
Future<void> onStart(Map<String, dynamic> params) async {
print('inside onStart of audioPlayertask');
print('queue now is $queue');
// NOW HERE QUEUE COMES OUT TO BE AN EMPTY LIST
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
if (queue.length == 0) {
print('queue is found to be null.........');
}
_player.currentIndexStream.listen((index) {
if (index != null) AudioServiceBackground.setMediaItem(queue[index]);
});
// Propagate all events from the audio player to AudioService clients.
_eventSubscription = _player.playbackEventStream.listen((event) {
_broadcastState();
});
// Special processing for state transitions.
_player.processingStateStream.listen((state) {
switch (state) {
case ProcessingState.completed:
AudioService.currentMediaItem != queue.last
? AudioService.skipToNext()
: AudioService.stop();
break;
case ProcessingState.ready:
break;
default:
break;
}
});
// Load and broadcast the queue
print('queue is');
print(queue);
print('Index is $index');
print('MediaItem is');
print(queue[index]);
try {
if (queue[index].extras == null) {
queue[index] = queue[index].copyWith(extras: {
'URL': await fetchSongUrl(queue[index].id),
});
}
await AudioServiceBackground.setQueue(queue);
await _player.setUrl(queue[index].extras['URL']);
onPlay();
} catch (e) {
print("Error: $e");
onStop();
}
}
#override
Future<void> onSkipToQueueItem(String mediaId) async {
// Then default implementations of onSkipToNext and onSkipToPrevious will
// delegate to this method.
final newIndex = queue.indexWhere((item) => item.id == mediaId);
if (newIndex == -1) return;
_player.pause();
if (queue[newIndex].extras == null) {
queue[newIndex] = queue[newIndex].copyWith(extras: {
'URL': await fetchSongUrl(queue[newIndex].id),
});
await AudioServiceBackground.setQueue(queue);
// AudioService.updateQueue(queue);
}
await _player.setUrl(queue[newIndex].extras['URL']);
_player.play();
await AudioServiceBackground.setMediaItem(queue[newIndex]);
}
#override
Future<void> onUpdateQueue(List<MediaItem> queue) {
AudioServiceBackground.setQueue(queue = queue);
return super.onUpdateQueue(queue);
}
#override
Future<void> onPlay() => _player.play();
#override
Future<void> onPause() => _player.pause();
#override
Future<void> onSeekTo(Duration position) => _player.seek(position);
#override
Future<void> onFastForward() => _seekRelative(fastForwardInterval);
#override
Future<void> onRewind() => _seekRelative(-rewindInterval);
#override
Future<void> onSeekForward(bool begin) async => _seekContinuously(begin, 1);
#override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
#override
Future<void> onStop() async {
await _player.dispose();
_eventSubscription.cancel();
await _broadcastState();
// Shut down this task
await super.onStop();
}
Future<void> _seekRelative(Duration offset) async {
var newPosition = _player.position + offset;
// Make sure we don't jump out of bounds.
if (newPosition < Duration.zero) newPosition = Duration.zero;
if (newPosition > mediaItem.duration) newPosition = mediaItem.duration;
// Perform the jump via a seek.
await _player.seek(newPosition);
}
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
if (begin) {
_seeker = Seeker(_player, Duration(seconds: 10 * direction),
Duration(seconds: 1), mediaItem)
..start();
}
}
/// Broadcasts the current state to all clients.
Future<void> _broadcastState() async {
await AudioServiceBackground.setState(
controls: [
MediaControl.skipToPrevious,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.skipToNext,
],
systemActions: [
MediaAction.seekTo,
MediaAction.seekForward,
MediaAction.seekBackward,
],
androidCompactActions: [0, 1, 3],
processingState: _getProcessingState(),
playing: _player.playing,
position: _player.position,
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
);
}
AudioProcessingState _getProcessingState() {
switch (_player.processingState) {
case ProcessingState.idle:
return AudioProcessingState.stopped;
case ProcessingState.loading:
return AudioProcessingState.connecting;
case ProcessingState.buffering:
return AudioProcessingState.buffering;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
}
}
This is the full code for AudioService in-case needed.
(Answer update: Since v0.18, this sort of pitfall doesn't exist since the UI and background code run in a shared isolate. The answer below is only relevant for v0.17 and earlier.)
audio_service runs your BackgroundAudioTask in a separate isolate. In the README, it is put this way:
Note that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the AudioService API to communicate with the background task, while your background task will only use the AudioServiceBackground API to interact with the UI and other clients.
The key point there is that isolates do not share memory. If you set a "global" variable in the UI isolate, it will not be set in the background isolate because the background isolate has its own separate block of memory. That is why your global queue variable is null. It is not actually the same variable, because now you actually have two copies of the variable: one in the UI isolate which has been set with a value, and the other in the background isolate which has not (yet) been set with a value.
Now, your background isolate does "later" set its own copy of the queue variable to something, and this happens via the message passing API where you pass the queue from the UI isolate into updateQueue and the background isolate receive that message and stores it into its own copy of the variable in onUpdateQueue. If you were to print out the queue after this point it would no longer be null.
There is also a line in your onStart where you are attempting to set the queue, although you should probably delete that code and let the queue only be set in onUpdateQueue. You should not attempt to access the queue in onStart since your queue won't receive its value until onUpdateQueue. If you want to avoid any null pointer exception before its set, you can initialise the queue in the background isolate to an empty list, and it will eventually get replaced by a non-empty list in onUpdateQueue without ever being null.
I would also suggest you avoid making queue a global variable. Global variables are generally bad, but in this case, it may actually be confusing you into thinking that that queue variable is the same in both the UI and the background isolate when in reality each isolate will have its own copy of the variable perhaps with different values. Thus, your code will be clearer if you make two separate "local" variables. One inside the UI and one inside the background task.
One more suggestion is that you should note that the methods in the message passing API are asynchronous methods. You should wait for the audio service to start before you send messages to it, such as setting the queue. AND you should wait for the queue to be set before you try to play from the queue:
await AudioService.start(....);
// Now the service has started, it is safe to send messages.
await AudioService.updateQueue(...);
// Now the queue has been updated, it is safe to play from it.

websocket automatic reconnect with flutter & riverpod?

1. OBJECTIVE
I would like the connection between my custom WebSocket server (API) and my Flutter app, to be re-established automatically when encountering network issues or when the WebSocket server encounter issues.
Use case 1: the wifi stops and suddenly comes back.
Use case 2: the API is not started and restarts suddenly.
Constraint: I use Riverpod as a state management library (and I want to keep it :)).
I emphasize about the state management library because I create the WS connection in a StreamProvider (cf. Riverpod).
2. INITIAL SETUP WITHOUT AUTOMATIC RECONNECT
I created a StreamProvider as shown below:
final hostProvider =
StreamProvider.autoDispose.family<Host, String>((ref, ip) async* {
//SOCKET OPEN
final channel = IOWebSocketChannel.connect('ws://$ip:$port/v1/path');
ref.onDispose(() {
// SOCKET CLOSE
return channel.sink.close();
});
await for (final json in channel.stream) {
final jsonStr = jsonDecode(json as String);
yield Host.fromJson(jsonStr as Map<String, dynamic>);
}
});
And I created a widget to consume the data:
useProvider(hostProvider(ip)).when(
data: (data) => show the result
loading: () => show progress bar
error: (error, _) => show error
);
This piece of code works great. However, there is no automatic reconnect mechanism.
3. AUTOMATIC RECONNECT ATTEMPTS
I called a function connectWs in a try/catch whenever exceptions are caught:
final hostProvider =
StreamProvider.autoDispose.family<Host, String>((ref, ip) async* {
// Open the connection
connectWs('ws://$ip:$port/v1/path').then((value) async* {
final channel = IOWebSocketChannel(value);
ref.onDispose(() {
return channel.sink.close();
});
await for (final json in channel.stream) {
final jsonStr = jsonDecode(json as String);
yield Host.fromJson(jsonStr as Map<String, dynamic>);
}
});
});
Future<WebSocket> connectWs(String path) async {
try {
return await WebSocket.connect(path);
} catch (e) {
print("Error! " + e.toString());
await Future.delayed(Duration(milliseconds: 2000));
return await connectWs(path);
}
}
I created a connectProvider provider, as shown here below, I 'watched' in hostProvider in order to create a channel. Whenever there is an exception, I use the refresh function from the Riverpod library to recreate the channel:
// used in hostProvider
ref.container.refresh(connectProvider(ip))
final connectProvider =
Provider.family<Host, String>((ref, ip) {
//SOCKET OPEN
return IOWebSocketChannel.connect('ws://$ip:$port/v1/path');
});
Thanks in advance for your help.
Thanks, #Dewey.
In the end, I found a workaround that works for my use case:
My providers: channelProvider & streamProvider
static final channelProvider = Provider.autoDispose
.family<IOWebSocketChannel, HttpParam>((ref, httpParam) {
log.i('channelProvider | Metrics - $httpParam');
return IOWebSocketChannel.connect(
'ws://${httpParam.ip}:$port/v1/${httpParam.path}');
});
static final streamProvider =
StreamProvider.autoDispose.family<dynamic, HttpParam>((ref, httpParam) {
log.i('streamProvider | Metrics - $httpParam');
log.i('streamProvider | Metrics - socket ${httpParam.path} opened');
var bStream = ref
.watch(channelProvider(httpParam))
.stream
.asBroadcastStream(onCancel: (sub) => sub.cancel());
var isSubControlError = false;
final sub = bStream.listen(
(data) {
ref
.watch(channelProvider(httpParam))
.sink
?.add('> sink add ${httpParam.path}');
},
onError: (_, stack) => null,
onDone: () async {
isSubControlError = true;
await Future.delayed(Duration(seconds: 10));
ref.container.refresh(channelProvider(httpParam));
},
);
ref.onDispose(() {
log.i('streamProvider | Metrics - socket ${httpParam.path} closed');
sub.cancel();
if (isSubControlError == false)
ref.watch(channelProvider(httpParam)).sink?.close(1001);
bStream = null;
});
return bStream;
});
I consume streamProvider that way in my widget:
return useProvider(MetricsWsRepository.streamProvider(HttpParam(
ip: ip,
path: 'dummy-path',
))).when(
data: (data) => deserialize & doSomething1,
loading:() => doSomething2,
error: (_, stack) => doSomething3
)
I'm a bit of a beginner with riverpod but it seems to me you want to use a higher-level redux/bloc style flow to recreate the provider each time it fails ...
This higher level bloc creates the provider when the connection succeeds, and when the connection fails, you dispatch an event to the bloc that tells it to reconnect and recreate the provider ...
That's my thought, but again, I'm a beginner with this package.