Why I can't cancel `StreamGroup` subscription immediately - dart

I want to listen to events from multiple Stream sources until I get a stop event. After that, I would like to unsubscribe. I expect that takeWhile cancels the subscription, but it doesn't seem to work until it's finished awaiting Future.
Here is my code below:
void main() async {
await StreamGroup.merge([_test2(), _test1()])
.takeWhile((element) => element != -1)
.forEach((element) {
print('Element=$element');
});
print('Finished!');
}
Stream<int> _test1() async* {
for (var i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
yield -1;
}
Stream<int> _test2() async* {
await longUserAction();
for (var i = 10; i < 20; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
yield -1;
}
Future<void> longUserAction() => Future.delayed(Duration(seconds: 20));
What I except:
Element=0
Element=1
Element=2
Element=3
Element=4
Finished!
What I got:
Element=0
Element=1
Element=2
Element=3
Element=4
*long pause*
Finished!

Here is the solution for my case:
class _SubscriptionData<T> {
final Stream<T> source;
final StreamSubscription<T> subscription;
bool isClosed = false;
_SubscriptionData(this.source, this.subscription);
void cancelSubscription() {
if (!isClosed) {
isClosed = true;
subscription.cancel();
}
}
}
class _MergeStreamController<T> {
final StreamController<T> _controller = StreamController<T>();
int workingStreamsCount = 0;
_MergeStreamController(List<Stream<T>> sources, bool Function(T) predicate) {
workingStreamsCount = sources.length;
final List<_SubscriptionData<T>> subscriptions = sources
.map((source) => _SubscriptionData(source, source.listen(null)))
.toList(growable: false);
void cancelAll() {
subscriptions.forEach((sub) {
sub.cancelSubscription();
});
}
subscriptions.forEach((subsData) {
subsData.subscription.onData((data) {
if (!predicate(data)) {
workingStreamsCount = 0;
_controller.close();
cancelAll();
} else if (!_controller.isClosed) _controller.add(data);
});
subsData.subscription.onDone(() {
if (--workingStreamsCount <= 0) _controller.close();
subsData.cancelSubscription();
});
});
}
}
/// Merges [sources] streams into a single stream channel
/// Stream closes when the first [source] stream emits event which is not satisfying predicate
/// or all streams done its work.
Stream<T> mergeStreamsWhile<T>(
List<Stream<T>> sources, bool Function(T) takeWhile) =>
_MergeStreamController(sources, takeWhile)._controller.stream;
void main() async {
await mergeStreamsWhile(
[_test2(), _test1(), _test3()], (element) => element != -1)
.forEach((element) {
print('Element=$element');
});
print('Finished!');
}
Stream<int> _test1() async* {
for (var i = 0; i < 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
yield -1;
}
Stream<int> _test2() async* {
await longUserAction();
for (var i = 10; i < 20; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
yield -1;
}
Stream<int> _test3() async* {
return; // Simulate an empty stream
}
Future<void> longUserAction() => Future.delayed(Duration(seconds: 20));
Output:
Element=0
Element=1
Element=2
Element=3
Element=4
Finished!

Related

How to break the loop for a stream in dart?

I known the listen can be abort by StreamSubscription. But for some reason, I can not call listen for the File.openRead(). How can I abort the read operation stream?
import 'dart:io';
import 'dart:async';
class Reader {
Stream<int> progess(File file) async* {
var sum = 0;
var fs = file.openRead();
await for (var d in fs) {
// consume d
sum += d.length;
yield sum;
}
}
void cancel() {
// How to abort the above loop without using StreamSubscription returned by listen().
}
}
void main() async {
var reader = Reader();
var file = File.new("a.txt");
reader.progess(file).listen((p) => print("$p"));
// How to cancel it without
Future.delayed(Duration(seconds: 1), () { reader.cancel()});
}
You cannot cancel the stream subscription without calling cancel on the stream subscription.
You might be able to interrupt the stream producer in some other way, using a "side channel" to ask it to stop producing values. That's not a stream cancel, more like a premature stream close.
Example:
class Reader {
bool _cancelled = false;
Stream<int> progess(File file) async* {
var sum = 0;
var fs = file.openRead();
await for (var d in fs) {
// consume d
sum += d.length;
if (_cancelled) return; // <---
yield sum;
}
}
void cancel() {
_cancelled = true;
}
}
Another option is to create a general stream wrapper which can interrupt the stream. Maybe something like
import"dart:async";
class CancelableStream<T> extends Stream<T> {
final Stream<T> _source;
final Set<_CancelableStreamSubscription<T>> _subscriptions = {};
CancelableStream(Stream<T> source) : _source = source;
#override
StreamSubscription<T> listen(
onData, {onError, onDone, cancelOnError}) {
var sub = _source.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
var canSub = _CancelableStreamSubscription<T>(sub, this, cancelOnError ?? false);
_subscriptions.add(canSub);
return canSub;
}
void cancelAll() {
while (_subscriptions.isNotEmpty) {
_subscriptions.first.cancel();
}
}
}
class _CancelableStreamSubscription<T> implements StreamSubscription<T> {
final bool _cancelOnError;
final StreamSubscription<T> _source;
final CancelableStream<T> _owner;
_CancelableStreamSubscription(
this._source, this._owner, this._cancelOnError);
#override
Future<void> cancel() {
_owner._subscriptions.remove(this);
return _source.cancel();
}
#override
void onData(f) => _source.onData(f);
#override
void onError(f) {
if (!_cancelOnError) {
_source.onError(f);
} else {
_source.onError((Object e, StackTrace s) {
_owner._subscriptions.remove(this);
if (f is void Function(Object, StackTrace)) {
f(e, s);
} else {
f?.call(e);
}
});
}
}
#override
bool get isPaused => _source.isPaused;
#override
void onDone(f) => _source.onDone(() {
_owner._subscriptions.remove(this);
f?.call();
});
#override
void pause([resumeFuture]) => _source.pause(resumeFuture);
#override
void resume() => _source.resume;
#override
Future<E> asFuture<E>([E? value]) => _source.asFuture(value);
}
You can then use it like:
void main() async {
Stream<int> foo() async* {
yield 1;
yield 2;
yield 3;
yield 4;
}
var s = CancelableStream<int>(foo());
await for (var x in s) {
print(x);
if (x == 2) s.cancelAll();
}
}

Cloud Functions -How to Update Multiple Refs

I have to fetch data at a couple of refs and update at a couple of refs. Everything works fine except for the fourth ref loopArrAtFourthRef that I have to update. It gets it's values from the third via a loop. The problem is the fifthRef finishes before the fourth ref. I'm a Swift dev and I can't get the hang of Promises, they are way to foreign for me.
exports.updateMultipleRefs = functions.https.onCall((data, context) => {
// ... values from data used in refs
const fetchFromFirstRef = admin.database()....
const updateAtSecondRef = admin.database()....
const runTransactionAtSecondRef = admin.database()....
const fetchArrDataFromThirdRef = admin.database()....
const loopArrAtFourthRef = admin.database()....
const updateFifthRef = admin.database()....
var arr = [];
var promises = [];
var val_1 = 0;
var val_2 = 0;
return fetchFromFirstRef
.once('value')
.then((snapshot) => {
if (snapshot.exists()) {
snapshot.forEach((childSnapshot) => {
// val_1 += data from each childSnapshot
});
return updateAtSecondRef.update({ "val_1":val_1 }).then(()=> {
return runTransactionAtSecondRef.set(admin.database.ServerValue.increment(1));
}).then(() => {
return fetchArrDataFromThirdRef
.once('value')
.then((snapshot) => {
if (snapshot.exists()) {
snapshot.forEach((childSnapshot) => {
var gameId = childSnapshot.key;
arr.push(gameId);
});
} else {
return null;
}
});
}).then(() => {
// this is where I'm having the problem, the fifth ref finishes before this does
for (var i = 0; i < arr.length; i++) {
var gameId = arr[i];
var promiseRef = loopArrAtFourthRef.child(gameId);
promises.push(promiseRef.once('value').then((snap) => {
snap.forEach((child) => {
// val_2 += data from each child
});
}));
return Promise.all(promises);
}
}).then(() => {
return updateFifthRef.update({ "val_2": val_2 });
});
} else {
return null;
}
})
.catch((error) => {
console.log('error', error);
return null;
});
});
Following this answer and this blog I just had to create an array of Promises and run Promise.all() after the outer for-loop finished:
for (var i = 0; i < arr.length; i++) {
var gameId = arr[i];
var promiseRef = loopArrAtFourthRef.child(gameId);
promises.push(promiseRef.once('value').then((snap) => {
snap.forEach((child) => {
// val_2 += data from each child
});
}));
}
return Promise.all(promises);

Dart append to file when i use transform in read text from file

in this simple code i can show all fetched ids when finished reading file and get id from text file, but i want to append this fetched id inside JsonObjectTransformer class, not finished reading file
Future<void> main() async {
final ids = await File('sample.json')
.openRead()
.transform(const Utf8Decoder())
.transform<dynamic>(JsonObjectTransformer())
.map((dynamic json) => json['id'] as String)
.toList();
print(ids); // [#123456, #123456]
}
class JsonObjectTransformer extends StreamTransformerBase<String, dynamic> {
static final _openingBracketChar = '{'.codeUnitAt(0);
static final _closingBracketChar = '}'.codeUnitAt(0);
#override
Stream<dynamic> bind(Stream<String> stream) async* {
final sb = StringBuffer();
var bracketsCount = 0;
await for (final string in stream) {
for (var i = 0; i < string.length; i++) {
final current = string.codeUnitAt(i);
sb.writeCharCode(current);
if (current == _openingBracketChar) {
bracketsCount++;
}
if (current == _closingBracketChar && --bracketsCount == 0) {
yield json.decode(sb.toString());
sb.clear();
}
}
}
/*for example this line*/
//new File('test.txt').writeAsStringSync(sb.toString(), mode: FileMode.APPEND);
}
}
how can i do that?
There are multiple ways to do this but a simple way is to change the JsonObjectTransformer like this:
class JsonObjectTransformer extends StreamTransformerBase<String, dynamic> {
static final _openingBracketChar = '{'.codeUnitAt(0);
static final _closingBracketChar = '}'.codeUnitAt(0);
#override
Stream<dynamic> bind(Stream<String> stream) async* {
final sb = StringBuffer();
var bracketsCount = 0;
final ioSink = File('test.txt').openWrite(mode: FileMode.append);
await for (final string in stream) {
for (var i = 0; i < string.length; i++) {
final current = string.codeUnitAt(i);
sb.writeCharCode(current);
if (current == _openingBracketChar) {
bracketsCount++;
}
if (current == _closingBracketChar && --bracketsCount == 0) {
final dynamic jsonObject = json.decode(sb.toString());
ioSink.writeln(jsonObject['id'] as String);
yield jsonObject;
sb.clear();
}
}
}
await ioSink.flush();
await ioSink.close();
}
}
A more clean solution (since we want some separate of concern) would be to make use of the Stream in your main to write the ID's as each object are parsed. An example how to do that would be:
Future<void> main() async {
final file = File('test.txt').openWrite(mode: FileMode.append);
final ids = <String>[];
await File('sample.json')
.openRead()
.transform(const Utf8Decoder())
.transform<dynamic>(JsonObjectTransformer())
.map((dynamic json) => json['id'] as String)
.forEach((id) {
file.writeln(id);
ids.add(id);
});
await file.flush();
await file.close();
print(ids); // [#123456, #123456]
}

Adding TextEditingControllers in a loop in Flutter isn't working

What I try to achieve is for the TextInputField to be autovalidated once there is more than 1 character entered.
Here's my initState (simplified):
#override
void initState() {
autoValidateList.addAll([
_autoValidateEmail,
_autoValidateCompanyName,
_autoValidatePhoneNo,
_autoValidateName,
_autoValidateSurname
]);
textEditingControllersList.addAll([
_emailController,
_companyNameController,
_phoneNoController,
_nameController,
_surnameController
]);
for (int i = 0; i < textEditingControllersList.length; i++) {
TextEditingController controller = textEditingControllersList[i];
controller.addListener(() => () {
print(
'Listener entered. companyName? ${controller == _companyNameController}');
if (controller.text.length > 0) {
print('=> true');
setState(() => autoValidateList[i] = true);
} else {
print('=> false');
setState(() => autoValidateList[i] = false);
}
});
}
_emailController.text = widget.loginData.email;
super.initState();
}
If I add the listeners not in a loop, for example:
_emailController.addListener(() => setState(() {
if (_emailController.text.length > 0) {
_autoValidateEmail = true;
} else {
_autoValidateEmail = false;
}
}));
It works fine.
None of the print statements get executed. What am I missing here?
There's a very insidious error here. Notice that in your addListener, you're passing a function that returns a function. What you want to execute is the function that is being returned, but you're actually executing the function that you're passing.
In a more clear syntax, you're doing this:
controller.addListener(() {
return () {
// Your code
};
});
So, what is happening is:
controller.addListener(() {
print('This is going to be executed');
return () {
print('This is NOT going to be executed. Your code used to be here.');
};
});
Instead of:
controller.addListener(() => () {
...
});
You should be doing:
controller.addListener(() {
...
});
Also, this is not related, but you should be calling super at the beginning of initState, not at the end.

Flutter FutureBuilder Not Updating

I have a Flutter FutureBuilder that needs to be updated with new data given by the user. However, the UI elements in the FutureBuilder do not update and still contain the old values. I have checked through print statements that the new data is correctly loaded. The issue seems to be with FutureBuilder rebuilding the widget when the new data is loaded. Any help is appreciated.
Future<List<PollItem>> fetchPost(String loc) async {
return new Future(() async {
final response = await http
.post(restip + '/getPosts',
body: {"data": loc});
if (response.statusCode == 200) {
print(response.body);
// If the call to the server was successful, parse the JSON
// This function adds json to list
PollItem.fromJson(json.decode(response.body));
// list is a list of posts gathered based on the string criteria
return list;
} else {
throw Exception('Failed to load polls');
}
});
}
class PollState extends State<Poll> {
TextEditingController textc = new TextEditingController();
static String dropDowntext = "City";
String _name = "Search";
final _names = [''];
Widget build(BuildContext context) {
print("dropdown"+dropDowntext);
textc.text = _name;
print(dropDowntext);
return FutureBuilder<List<PollItem>>(
future: fetchPost(dropDowntext),
initialData: [PollItem()],
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0].question);
});
}
Here is my global file:
List<PollItem> list = new List();
factory PollItem.fromJson(Map<String, dynamic> json) {
int len = json['length'];
if(listNum!=len) {
listNum = len;
list.clear();
for (int i = 0; i < len; i++) {
list.add(PollItem(
answer1: json[i.toString()]['answer1'],
location: json[i.toString()]['location']
)
);
}
}
}
You don't need to create a Future object :
Future<List<PollItem>> fetchPost(String loc) async {
final response = await http.post(restip + '/getPosts',body: {"data": loc});
if (response.statusCode == 200) {
print(response.body);
final data = json.decode(response.body);
int len = data['length'];
final List<PollItem> newList = List();
for (int i = 0; i < len; i++) {
newList.add(PollItem(
answer1: data[i.toString()]['answer1'],
location: data[i.toString()]['location']
)
);
}
print("new list size: ${newList.length}");
return newList;
} else {
throw Exception('Failed to load polls');
}
return null;
}

Resources