skip to Main Content

Project structure

I have the next Widget, which uses a StreamBuilder to listen a draw a List.
(I’ve omited irrelevant details)

class CloseUsersFromStream extends StatelessWidget {
  final Stream<List<User>> _stream;
  const CloseUsersFromStream(this._stream);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<User>>(
      stream: _stream,
      builder: _buildWidgetFromSnapshot,
    );
  }

  Widget _buildWidgetFromSnapshot(_, AsyncSnapshot<List<User>> snapshot) {
    if (_snapshotHasLoaded(snapshot)) {
      return UsersButtonsList(snapshot.data!);
    } else {
      return const Center(child: CircularProgressIndicator());
    }
  }

  bool _snapshotHasLoaded(AsyncSnapshot<List<User>> snapshot) {
    return snapshot.hasData;
  }
}

And its parent:

class CloseUsersScreen extends StatefulWidget {
  final ICloseUsersService _closeUsersService;

  const CloseUsersScreen(this._closeUsersService);

  @override
  State<CloseUsersScreen> createState() => _CloseUsersScreenState();
}

class _CloseUsersScreenState extends State<CloseUsersScreen> {
  late final Stream<List<User>> _closeUsersStream;

  @override
  void initState() {
    _initializeStream(context);
    super.initState();
  }

  @override
  void dispose(){
    _closeStream();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        children: [
          //...
          CloseUsersFromStream(_closeUsersStream),
        ],
      ),
    );
  }

  void _initializeStream(BuildContext context) {
    _closeUsersStream = widget._closeUsersService.openCloseUsersSubscription(context);
  }

  void _closeStream(){
    widget._closeUsersService.closeCloseUsersSubscription();
  }
}

The problem

Sometimes, my FutureBuilder doesn’t rebuild when a new event occurs on the Stream.

With the debugger I’ve noticed that the stream works properly and the StreamBuilder receives the data correctly too. But, it doesn’t rebuild as expected.

When I say that the StreamBuilder receives the data correctly, I mean that the debugger stops on the line

return UsersButtonsList(snapshot.data!);

and with the debugger I can see that snapshot.data is the expected value. Nevertheless, the Widget doesn’t redraw.

Unfortunately, I haven’t could work out the pattern when the problem appears yet. I think that it’s probable that it happens after disposing the StreamBuilder under specific conditions (even tough it doesn’t print any error), but I’m not 100% sure. I’m still trying to identify it and I will add it to the post when I do.

Using my own "StreamBuilder"

I’ve tried to build my own StreamBuilder to avoid using the Flutter one, because I thought the widget could be the problem

class CustomStreamBuilder<T> extends StatefulWidget {
  final Stream<T> stream;
  final Widget Function(BuildContext context, T? data) builder;

  const CustomStreamBuilder({required this.stream, required this.builder});

  @override
  _CustomStreamBuilderState<T> createState() => _CustomStreamBuilderState<T>();
}

class _CustomStreamBuilderState<T> extends State<CustomStreamBuilder<T>> {
  late StreamSubscription<T> _subscription;
  T? _data;

  @override
  void initState() {
    super.initState();;
    _subscription = widget.stream.listen((data) {
      setState(() {
        _data = data;
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _data);
  }
}

However, the problem persisted even with this Widget. The Stream still works properly and, according to the debugger, the Widget should be redrawing. I mean, this lines are executed again when the stream receives an event:

  Widget _buildWidgetFromSnapshot(BuildContext context, List<User>? snapshot) {
      if(snapshot == null) {
        return const Center(child: CircularProgressIndicator());
      }else{
        return UsersButtonsList(snapshot,_qualityReducer, _messageService); //<-- This line
      }
  }

And this one:

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _data); //<-- This line
  }

And, as it happened before, the variable _data has the correct value, given from the event.

Other solutions I’ve tried

As I think the problem could be that the StreamBuilder is being disposed before it’s initialization, this are other things I’ve tried:

Delaying the disposing a second to let the StreamBuilder initialize correctly. The problem persisted.

  @override
  void dispose() {
    sleep(const Duration(seconds:1));
    _closeStream();
    super.dispose();
  }

Forcing update the StreamBuilder when it’s build. The problem persisted.

class CloseUsersFromStream extends StatefulWidget {
  final Stream<List<User>> _stream;
  const CloseUsersFromStream(this._stream);

  @override
  State<CloseUsersFromStream> createState() => _CloseUsersFromStreamState();
}

class _CloseUsersFromStreamState extends State<CloseUsersFromStream> {

  @override
  void initState() {
    setState(() {});
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<User>>(
      stream: widget._stream,
      builder: _buildWidgetFromSnapshot,
    );
  }

   //...
}

Not initializing the the StreamBuilder until the Stream is initialized. (This doesn’t make so much sense because the Stream initialization isn’t a future, but I tried anyways)

class _CloseUsersScreenState extends State<CloseUsersScreen> {
  Stream<List<User>>? _closeUsersStream;

  //...

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        children: [
          CloseUsersHeader(),
          if(_closeUsersStream != null) // <- This line here
              CloseUsersFromStream(_closeUsersStream!),
        ],
      ),
    );
  }
  void _initializeStream(BuildContext context) {
    setState(() {
       _closeUsersStream = widget._closeUsersService.openCloseUsersSubscription(context);
    });
  }

  //...
}

As @B0Andrew has indicated me on the comments, I tried changing the FutureBuilder‘s child with a simple Text, discarding that the problem is on it. But, the problem persisted.

  Widget _buildWidgetFromSnapshot(BuildContext context, List<User>? snapshot) {
    if(snapshot == null) {
      return const Center(child: CircularProgressIndicator());
    }else{
      // return UsersButtonsList(snapshot;
      return Text("Event received");
    }
  }

Screenshots

This is a little video showing the problem. Note that the CircularProgressIndicator stays in the screen.

GIF showing the error

But, as you can see, the debugger does stop on the correct line:

Debugger

Github

This is the Github link, for watching the full project.

Other sources I’ve read

I’ve consulted the next sources unsuccessfuly:

Another example

A few days after posting the original issue, I found that this problem happens also in another StreamBuilder of the project. I’ve tried to work out the pattern which makes this error appear but I haven’t could yet.

This is the StreamBuilder state widget (again, details are omitted):

class _ChatRendererState extends State<ChatRenderer> {

  @override
  void dispose() {
    widget._chatService.closeChatStream();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future:widget._chatService.getChat(widget._externalUser),
        builder:_buildChatFromFutureSnapshot,
    );
  }

  Widget _buildChatFromFutureSnapshot(BuildContext context, AsyncSnapshot snapshot){
    if(snapshot.hasData){
      return StreamBuilder<Chat>(
        initialData: snapshot.data,
        stream: widget._chatService.getChatStream(context),
        builder: (context, snapshotStream) {
          return MessagesListWithLazyLoading(snapshotStream.data!); // <- Debugger stops in this line. But, the Widget doesn't rebuild
        },
      );
    }else{
      //...
    }
  }

And this is the class which gets the Stream from (anyways, as i said before, the Stream works properly)

class ChatStreamService implements IChatStreamService{
  StreamController<Chat>? _closeUsersStreamController;
  late WebSocketSubscription _chatSubscription;

  @override
  Stream<Chat> getChatStream(BuildContext context){
    _closeUsersStreamController = StreamController<Chat>(); 
    _initializeSubscription(context);
    return _closeUsersStreamController!.stream;
  }

  @override
  void closeChatStream(){
    _chatSubscription.unsuscribe();
  }

  void _initializeSubscription(BuildContext context){
    _chatSubscription = WebSocketSubscription.activate(
      //...
      callback: _onChatReceived
    );
  }

  void _onChatReceived(String? frameBody){
    final chatJSON = jsonDecode(frameBody!);
    Chat chat = Chat.fromJSON(chatJSON);
    _closeUsersStreamController!.add(chat);      
  }

}

2

Answers


  1. What immediately makes me wonder: Your UsersButtonList has a const constructor.

    Can you try to create your widget no longer via a const constructor? I need to read up on this, but Flutter actually recycles widgets that are const to optimize performance. How and exactly this happens, I have to look again.

    Maybe this is already the solution. I found this similar problem here:

    Why does `const` modifier avoid StreamBuilder rebuild in Flutter?

    See also this from this answer https://stackoverflow.com/a/53495637/5812524

    "In the case of Flutter, the real gain with const is not having less
    instantiation. Flutter has a special treatment for when the instance
    of a widget doesn’t change: it doesn’t rebuild them."

    Login or Signup to reply.
  2. Have you checked the issue:
    https://github.com/flutter/flutter/issues/64916

    If you do not yield any new data after switching the stream, it will keep the old stream data.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search