Flutter `StreamBuilder` question

389 views
Skip to first unread message

Chris Norman

unread,
Jan 7, 2022, 11:01:01 AM1/7/22
to mi...@dartlang.org
Hi all,
I'm writing a Flutter for Web application, and I've got a `WebSocketChannel` sending command to and from the server. I've been banging my head against the wall with it for a while now, because I just can't seem to find a nice solution.

In response to some of the commands, the Flutter application necessarily needs to render extra widgets. Should I be doing that from the builder callback of my `StreamBuiler`? Or should I be setting properties on my `State` instance, then parsing them out some other way?

I just can't wrap my head around this, but I feel like I can't be the first to have had this problem.

Thanks so much.

Take care,

Chris Norman

osaxma

unread,
Jan 11, 2022, 2:29:39 AM1/11/22
to Dart Misc, chris....@googlemail.com
I typically avoid `StreamBuilder`, especially with streams from network as I have observed few times that StreamBuilder subscribes multiple times to the stream (e.g. each build call) but that's another topic. So I just handle it myself within a `State` or some sort of `ChangeNotifier` model (in case I want to subscribe once and update different widgets at multiple locations in the tree using `notifyListeners`).

This is how I typically do it in a StatefulWidget:

(first time posting code snippet here so I hope it'll be highlighted properly)

```dart
import 'dart:async';
import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);

@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
int myValue = 0;
late final StreamSubscription<int> mySubscription;

@override
void initState() {
super.initState();
// assume this is the stream from your channel
final Stream<int> myStream = numbers();
// start listening to the stream
// and keep a reference of the subscription to cancel it later.
mySubscription = myStream.listen(streamListener);
}

void streamListener(int number) {
// update whatever properties you need here
// update the state if necessary
setState(() {
myValue;
});
}

@override
void dispose() {
mySubscription.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
// build different widget based on the command or pass the value to descendant widgets
if (myValue == 0) {
return const Center(child: CircularProgressIndicator());
}

return Center(
child: Text('current value $myValue'),
);
}
}

// assume this your stream
Stream<int> numbers() async* {
int i = 0;
yield i;
for (;;) {
await Future.delayed(const Duration(seconds: 2));
yield i++;
}
}
```

Osama Alraddadi

unread,
Jan 14, 2022, 5:24:11 AM1/14/22
to mi...@dartlang.org, chris....@googlemail.com

Hi again Chris, 

I would like to make a correction because I was wrong in my statement about StreamBuilder (including in the private exchange we had). 

My observation was from a wrong usage rather than a StreamBuilder fault. 

When I observed the behavior where the stream was subscribing on each build, I was most likely passing a function to the `stream` property that returns a new stream object on each call. 

StreamBuilder actually checks the stream object on every build, and it'll only resubscribe if the objects were different:

@override
void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
}


Here's a simple example to demonstrate: 

// assume we have the following stream
Stream<int> numbersStream() async* {
var i = 0;
yield i;
for (i; i < 100; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}

// and the following state
class _MyNumberStreamState extends State<MyNumberStream> {
// Assign the stream to an object.
final stream = numbersStream();
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamBuilder<int>(
initialData: 0,
key: const ValueKey('myStream'),
// If we pass `numbersStream()` directly to `StreamBuilder(stream: numbersStream())`,
// then on every build call (e.g. setState or when a parent rebuilds), a new subscription
// is created. The reason is simply that `numbersStream()` is returning a different object
// on each call. After each build, StreamBuilder compares the old stream vs the new stream,
// and if they're different, it'll create a new subscription, otherwise it keeps the old one.
// So if `numbersStream()` returns the same object, then assigning directly won't be an issue.
stream: stream, // numbersStream()
builder: (context, snapshot) {
return Text(snapshot.data.toString());
},
),
),
TextButton(
child: const Text('Set State'),
// click the button to observe the behavior in both cases
onPressed: () => setState(() {}),
),
],
);
}
}


I apologize for the wrong information and any confusion that I've caused.

Thanks,
Osama 


--
For more ways to connect visit https://dart.dev/community
---
You received this message because you are subscribed to a topic in the Google Groups "Dart Misc" group.
To unsubscribe from this topic, visit https://groups.google.com/a/dartlang.org/d/topic/misc/QybSe9vO3XA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to misc+uns...@dartlang.org.
To view this discussion on the web visit https://groups.google.com/a/dartlang.org/d/msgid/misc/28e66bd2-cb21-4612-81df-367454e1ccfcn%40dartlang.org.
Reply all
Reply to author
Forward
0 new messages