How do I get a dart Stream with uneven intervals? - flutter

I'm new to dart and Flutter and would love to get some advice on an algorithmic problem I'm facing.
I want to connect my Flutter app to a bluetooth device (that part is done, I am connected already) and send messages on uneven intervals. I have the messages in a list and for each of them I know at what time (milliseconds) I want to send the message.
So suppose the following messages are lined up:
start at 0ms
init_app at 100ms
user_pick_x at 500ms
user_start_x at 500ms (will be sent after user_pick_x, order should be guaranteed)
interrupt at 3500ms
I have found the documentation to create streams, but it always talks about a single interval value. https://dart.dev/articles/libraries/creating-streams.
Ideas:
Technically I can pass in a list of Duration objects and work with a custom generator async*, along with the message string.
Alternatively I can set the interval to the lowest time delta and check on each one whether a message / messages should be sent. In the case of the example that would be every 100ms.
It would be nice to be able to pause / cancel the stream as well. Which is something that streams can do natively.

I think the easiest is to just emit those messages at the specified intervals. Something like:
Future<void> _wait(int milliseconds) async =>
await Future<void>.delayed(Duration(milliseconds: milliseconds));
Stream<String> generateMessages() async* {
yield 'start';
await _wait(100);
yield 'init_all';
await _wait(400);
yield 'user_pick_x';
yield 'user_start_x';
await _wait(3000);
yield 'interrupt';
}
void main() {
generateMessages().listen((msg) {
print('${DateTime.now()}: $msg');
});
}
which will print:
2021-07-25 10:21:21.429: start
2021-07-25 10:21:21.531: init_all
2021-07-25 10:21:21.934: user_pick_x
2021-07-25 10:21:21.934: user_start_x
2021-07-25 10:21:24.938: interrupt
If you want to make sure that the listener of the stream receives events asynchronously - hence not interfering with the wait milliseconds, you can explicitly use the StreamController which by default calls the listeners asynchronously (make sure to import dart:async --- dart:io is only used in the example for the sleep to show that even on a blocking action it will run in parallel with the waiting):
import 'dart:async';
import 'dart:io';
Future<void> _wait(int milliseconds) async {
print('WAIT $milliseconds ms');
await Future<void>.delayed(Duration(milliseconds: milliseconds));
}
Stream<String> generateMessages() {
final controller = StreamController<String>(sync: false);
controller.onListen = () async {
controller.add('start');
await _wait(100);
controller.add('init_all');
await _wait(400);
controller.add('user_pick_x');
controller.add('user_start_x');
await _wait(3000);
controller.add('interrupt');
};
return controller.stream;
}
void main() {
generateMessages().listen((msg) {
sleep(const Duration(milliseconds: 120));
print('${DateTime.now()}: $msg');
});
}

Related

How can I send multiple async requests in Flutter that is not just concurrent but parallel?

I am calling a TTS API for multiple lines of texts. Ideally I would like to have them executed in parallel, meaning that one request would not wait for the other to finish before starting, hence speeding up total runtime of the API calls.
Here is an excerpt from my code:
await Future.forEach(rounds, (round) async {
await _fetchAudioForText(
round, voiceA );
totalBytes += round.audio?.lengthInBytes ?? 0;
});
...
Future<bool> _fetchAudioForText(Round round, Voice voice) async {
round.audio =
await ttsUtils.getAudioByteStream(round.text, voice);
return true;
}
getAudioByteStream is essentially calling the TTS API using a HTTP request and has a return type of a byte stream (Uint8List).
The problem is, that it's not actually doing it in parallel. What am I missing here?
remove await in forEach
await Future.forEach(rounds, (round) async {
_fetchAudioForText(
round, voiceA );
totalBytes += round.audio?.lengthInBytes ?? 0;
});

Cancel execution of method after it starts in Flutter

Consider this method:
Future<void> methodWithAlotOfSteps()async{
callMethodA();
await callMethodB();
...
return ...;
}
which makes some computation. Suppose I want the user to be able to stop this process at any point in time (when he taps cancel button for example).
How can I stop the execution of the above method no matter what line in the method the "program counter" has reached when the user presses cancel.
I am looking for something like methodWithAlotOfSteps.cancel();.
I tried using CancelableCompleter, but even though the Future is cancelled and onCancel method of the completer is called, but the function continues execution.
I know I can set a boolean flag and check it after each "step" ("line", "call to a method"),such as :
Future<void> methodWithAlotOfSteps()async{
if(!completer.isCancelled)
callMethodA();
if(!completer.isCancelled)
await callMethodB();
...
return ...;
}
but is there a better way of doing this?
As #Jamesdlin suggested, the apparent way is to check the cancelable completer's state after each async gap:
class MyService{
CancelableCompleter completer = CancelableCompleter();
Future<void> doSomething() async {
doSomeSyncWork(); // Sync work
doAnotherSyncWork(); // Sync work
await doSomeAsyncWork(); // Async work, this will return control to event loop and make it possible to, for example, press a button to cancel the future.
// here we know we have lost control for a while, so we must check if we have been cancelled
if (completer.isCompleted) {
return;
}
doSomeMoreSyncWork();
await doSomeMoreAsyncWork();
// here we know we have lost control for a while, so we must check if we have been cancelled
if (completer.isCompleted) {
return;
}
...
completer.complete();
}
}

Flutter: How to check if stream is empty or has no events?

I'm using flutter_beacon package to scan for the beacons like this,
_streamRanging =
flutterBeacon.ranging(regions).listen((RangingResult result) {
if (result.beacons.isEmpty) {
_streamRanging.cancel();
onBleSearch(false);
}
//do something else
}
);
now as there are no beacons found I want to call onBleSearch(false), but it doesn't go inside the function of .listen((l){}) because I think there are no events to call that function
so how I can check if the stream has no data and call this onBleSearch(false) function?
It's possible to check if the Stream is empty, but be aware that a Stream provides a way to receive a sequence of events. The Stream can be "empty" for an instance, and could possibly hold a value in the next. What you can do here is to continuously listen for events in the Stream and trigger the method depending on your use case.
StreamController.stream.listen(
(event) => print('Value: $event'), // Check if empty
);
Another way is by checking if the List from the Stream has value by using Stream.toList
Use this the .isEmpy to check if a stream is connected
Stream<String>? myStream;
Future<void> checkStream() async {
bool empty = await myStream.isEmpty;
if (empty) {
restart();
setState(() {});
}
}

Flutter- How to know AudioService is stopped?

I am working with audio_service flutter package. I want to pop a player page if Audio Service stops. How to get the Audio Service stopped event? I didn't find any events to check if service is stopped
(Answer update: Since v0.18, the service is effectively always running while the app is running, so there is no longer a need to check. The following answer is for v0.17 and earlier.)
AudioService.running will emit true when the service is running and false when it is not.
To listen to when it changes from true to false, you could try this:
// Cast runningStream from dynamic to the correct type.
final runningStream =
AudioService.runningStream as ValueStream<bool>;
// Listen to stream pairwise and observe when it becomes false
runningStream.pairwise().listen((pair) {
final wasRunning = pair.first;
final isRunning = pair.last;
if (wasRunning && !isRunning) {
// take action
}
});
If you instead want to listen to the stopped playback state, you need to ensure that your background audio task actually emits that state change in onStop:
#override
Future<void> onStop() async {
await _player.dispose();
// the "await" is important
await AudioServiceBackground.setState(
processingState: AudioProcessingState.stopped);
// Shut down this task
await super.onStop();
}
This way, you can listen for this state in the UI:
AudioService.playbackStateStream.listen((state) {
if (state.processingState == AudioProcessingState.stopped)) {
// take action
}
});

Flutter Background Processes Using Android Alarm Manager and Isolates

I'm trying to get a timer (down to the hundredths of seconds) to work in Flutter even when the app is closed. I initially tried to use isolates as I thought they would work yet after testing with a Pixel 4 running Android 11 I found that it was still not firing correctly when the app was closed. After some googleing I came across Android Alarm Manager and I have everything set up again yet it doesn't appear that the periodic function is firing correctly.
Heres the BLoC map for triggering the counter:
Stream<TimerState> _mapTimerStartedToState(TimerStarted start) async* {
AndroidAlarmManager.initialize();
port.listen((_) async => await _incrementCounter());
startCounter();
print(_counter);
yield TimerRunInProgress(start.duration);
}
Here's the startCounter() function:
void startCounter() async {
prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey(countKey)) {
await prefs.setInt(countKey, 0);
}
IsolateNameServer.registerPortWithName(
port.sendPort,
isolateName,
);
await AndroidAlarmManager.periodic(
Duration(milliseconds: 100),
// Ensure we have a unique alarm ID.
Random().nextInt(pow(2, 31)),
callback,
exact: true,
wakeup: true,
);
}
And then here's my callback:
static Future<void> callback() async {
print('Alarm fired!');
// Get the previous cached count and increment it.
final prefs = await
SharedPreferences.getInstance();
int currentCount = prefs.getInt(countKey);
await prefs.setInt(countKey, currentCount + 1);
// This will be null if we're running in the background.
print(currentCount);
uiSendPort ??= IsolateNameServer.lookupPortByName(isolateName);
uiSendPort?.send(null);
}
Am I on the right path here? Can AndroidAlarmManager do what I'm trying to do? I'm not exactly sure why the isolate approach didn't work on its own either, the only explanation I got was that I needed to use AndroidAlarmManager. Now, the events aren't firing at the 100 ms rate as I told them to and are instead firing 1 to several minutes apart.
Android restricts the frequencies for alarms. You cannot schedule alarms as frequently as 100 milliseconds with AlarmManager.
Please refer the note in red background on : https://developer.android.com/reference/android/app/AlarmManager
Note: Beginning with API 19 (Build.VERSION_CODES.KITKAT) alarm
delivery is inexact: the OS will shift alarms in order to minimize
wakeups and battery use. There are new APIs to support applications
which need strict delivery guarantees; see setWindow(int, long, long,
android.app.PendingIntent) and setExact(int, long,
android.app.PendingIntent). Applications whose targetSdkVersion is
earlier than API 19 will continue to see the previous behavior in
which all alarms are delivered exactly when requested.