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.
Related
How to poll the event if new data appeared in the event? I am using SinglR package to get data from websocket. It has way to wotk with events from websockets, but my problem is it has the new event inside List, but dosen't automatically shows it in widget until I reload page manually. I am also using streams to get the pile of data to update by itselfs and at the same time as data appeares in event.
I found only one way: it is add Stream.pireodic, but image which comes from event is flickering, but I don't need that behaviour. How can I solve this issue?
The main issue: the widget is not instantly updated and does not show data from the event
P.S Nothing except SignalR library dosen't work with websocket in my case
List wanted = [];
Future<List?> fetchWantedFixation() async {
final httpConnectionOptions = HttpConnectionOptions(
accessTokenFactory: () => SharedPreferenceService().loginWithToken(),
skipNegotiation: true,
transport: HttpTransportType.WebSockets);
final hubConnection = HubConnectionBuilder()
.withUrl(
'http://websockets/sockets',
options: httpConnectionOptions,
)
.build();
await hubConnection.start();
String? wantedFixation;
int? index;
hubConnection.on('Wanted', (arguments) {
Future.delayed(const Duration(seconds: 7))
.then((value) => alarmplayer.StopAlarm());
alarmplayer.Alarm(url: 'assets/wanted.mp3', volume: 0.25);
wanted = arguments as List;
});
hubConnection.onclose(({error}) {
throw Exception(error);
});
print(wanted);
return wanted;
}
// it makes image flickering, but the event are updating
Stream<List> getEvent() async* {
yield* Stream.periodic(Duration(seconds: 5), (_) {
return wanted;
}).asyncMap((event) async => event);
}
I have been working on an application and I need to implement in app audio and video calling in my app which I have done using Agora.io but the issue is I have to display incoming call notification does not matter if app is in foreground or in background. I have tried many things but still I am unable to configure that out. I am using agora_rtc_engine package for making calls.
Any help would be appreciated.
Thanks
The code I am working with currently:
Call Methods
class CallMethods {
final callRef = FirebaseFirestore.instance.collection('Calls');
Stream<DocumentSnapshot> callstream({#required String id}) =>
callRef.doc(id).snapshots();
Future<bool> makeCall({#required Call call}) async {
try {
log('Making call');
call.hasdialed = true;
Map<String, dynamic> hasDialedMap = call.toMap(call);
call.hasdialed = false;
Map<String, dynamic> hasNotDialedMap = call.toMap(call);
await callRef.doc(call.senderid).set(hasDialedMap);
await callRef.doc(call.receiverid).set(hasNotDialedMap);
return true;
} catch (e) {
print(e);
return false;
}
}
Future<bool> endCall({#required Call call}) async {
try {
log('ending call');
await callRef.doc(call.senderid).delete();
await callRef.doc(call.receiverid).delete();
return true;
} catch (e) {
print(e);
return false;
}
}
}
Call Utils: Which is used to make calls
class CallUtils {
static final CallMethods callmethods = CallMethods();
static dial(
BuildContext context, {
#required User from,
#required var to,
}) async {
Call call = Call(
senderid: from.id,
// senderpic: from.avatar.url,
callername: from.name,
receiverid: to.id,
// receiverpic: to.avatar.url,
receivername: to.name,
channelid: Random().nextInt(999999).toString(),
);
bool callmade = await callmethods.makeCall(call: call);
call.hasdialed = true;
if (callmade) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoCallScreen(call: call),
),
);
}
}
}
After that I have a pickup layout which is used to wrap all the screens to display incoming call notification.
Pickup Call Layout:
(user.value.id != null)
? StreamBuilder<DocumentSnapshot>(
stream: callmethods.callstream(id: user.value.id),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.data() != null) {
Call call = Call.fromMap(snapshot.data.data());
if (!call.hasdialed) {
return PickupScreen(call: call);
} else {
return widget.scaffold;
}
} else {
return widget.scaffold;
}
},
)
: widget.scaffold,
It can be done via firebase push notifications & backend API service.
Sender side:
As soon as a call is made, you would post your backend api service with caller and receiver id, and your backend service is further responsible to send a push notification with a payload to the receiver.
Receiver side:
When receiver gets a push notification, you can configure it to open your app automatically and show a screen with all the payload information. Maybe you can show him a screen with accept and decline button and if he accepts, you can connect him to Agora.
Check this for payload configuration.
Objective is simple
flutter app makes a call to graphql api over websockets
app view calls the controller, controller calls the provider, provider calls the AWS appsync api over websockets or over HTTP api socket call
we receive a stream of data from appsync api or HTTP api socket call over websockets every now and then from backend
streams need to be cascaded back to provider , and then to controller (this is the critical step)
controller (not the provider) would update the obs or reactive variable, make the UI reflect the changes
problem : data is recieved via websockets in the caller, but never passed back as stream to provider or controller to reflect the changes
sample code
actual caller
orderdata.dart
#override
Stream<dynamic> subscribe({
String query,
Map<String, dynamic> variables,
}) async* {
debugPrint('===->subscribe===');
// it can be any stream here, http or file or image or media
final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
GraphQLRequest<String>(
document: query,
variables: variables,
),
onEstablished: () {
debugPrint(
'===->subscribe onEstablished ===',
);
},
);
operation.listen(
(event) async* {
final jsonData = json.decode(event.data.toString());
debugPrint('===->subscription data $jsonData');
yield jsonData;
},
onError: (Object e) => debugPrint('Error in subscription stream: $e'),
);
}
in the provider
orderprovider.dart
Stream<Order> orderSubscription(String placeId) async* {
debugPrint('===->=== $placeId');
subscriptionResponseStream = orderData.subscribe(
query: subscribeToMenuOrder,
variables: {"place_id": placeId},
);
subscriptionResponseStream.listen((event) async* {
debugPrint(
"===->=== yielded $event",
);
yield event;
});
debugPrint('===->=== finished');
}
in the controller
homecontroller.dart
Future<void> getSubscriptionData(String placeId) async {
debugPrint('===HomeController->getSubscriptionData===');
OrderProvider().orderSubscription(placeId).listen(
(data) {
//this block is executed when data event is receivedby listener
debugPrint('Data: $data');
Get.snackbar('orderSubscription', data.toString());
},
onError: (err) {
//this block is executed when error event is received by listener
debugPrint('Error: $err');
},
cancelOnError:
false, //this decides if subscription is cancelled on error or not
onDone: () {
//this block is executed when done event is received by listener
debugPrint('Done!');
},
);
}
homeview calls homecontroller
Try using map for transforming Streams:
#override
Stream<dynamic> subscribe({
String query,
Map<String, dynamic> variables,
}) {
debugPrint('===->subscribe===');
// it can be any stream here, http or file or image or media
final Stream<GraphQLResponse<String>> operation = Amplify.API.subscribe(
GraphQLRequest<String>(
document: query,
variables: variables,
),
onEstablished: () {
debugPrint(
'===->subscribe onEstablished ===',
);
},
);
return operation.map((event) {
return json.decode(event.data);
});
}
// elsewhere
final subscription = subscribe(
query: 'some query',
variables: {},
);
subscription.listen(
(jsonData) {
debugPrint('===->subscription data $jsonData');
},
onError: (Object e) => debugPrint('Error in subscription stream: $e'),
);
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.
I'm using WebSocket from dart:io and IOWebSocketChannel (https://pub.dev/packages/web_socket_channel) to create a connection with channel subscriptions.
The code is largely working, I can connect to my server and subscribe to channels, receive data etc, but I'm struggling with an approach to writing tests.
Given this constructor:
SocketServer.Connect(this.url, {this.onConnect}) {
channel = IOWebSocketChannel.connect(url);
_stream = channel.stream.listen((data) {
this.onConnect();
final payload = jsonDecode(data);
print('Received: ');
print(payload);
}, onError: (_) {
this.disconnect();
});
}
I've tried variations on the following:
test("it should create a connection", () async {
final server = await HttpServer.bind('localhost', 0);
server.transform(WebSocketTransformer()).listen((WebSocket socket) async {
final channel = await IOWebSocketChannel(socket);
channel.sink.add('test');
});
bool connected = false;
final websocketServer = SocketServer.Connect('ws://localhost:${server.port}/cable', onConnect: (SocketServer connection) {
print('connected');
connected = true;
});
expectLater(connected, true);
});
But I don't receive the 'connected' output, since my HttpServer isn't sending back the correct responses.
Would it be best to depend on a real server to handle the connections, or create one in the tests?