6

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

Following what @Sayyid J told me, I've tried using a ValueListenableBuilder

class MyStreamNotifier<T> extends ValueNotifier implements ValueListenable {
 
  MyStreamNotifier({required Stream<T> stream}) : super(null){
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen((event) {
      //when there is event. just rebuild
      notifyListeners();
      _event = event;
    });

    _subscription.onError((err) {
      //handle the error
    });

  }

  late final StreamSubscription<T> _subscription;

 T? _event;

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

  @override
  get value => _event;

}




class CloseUsersFromStream extends StatelessWidget {
  final Stream<List<User>> _stream;

  const CloseUsersFromStream(this._stream);

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: MyStreamNotifier(stream: _stream),
      builder: (context, event, child) {
        if (event != null){
          return UsersButtonsList(event);
        }else{
          return const Center(child: CircularProgressIndicator());
        }
      });
  }
}

The debugger stops on the line notifyListeners(). However, it doesn't stops on builder function.


Adding a UniqueKey to the StreamBuilder

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

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

This is another view of the problem, showing the debugger and the app:

GIF showing the problem

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);      
  }

}

Run the project in your own PC

I know it would be really helpful providing a minimal snippet to recreate the problem. However, I haven't been able to reproduce it. All I can do is provide the instructions for running the project. I've "dockerized" the backend for making it simple to run. Fortunately, the project is very small and can be running with just a few commands.

This is the Backend Github

and can be running with this comman on the root of the project:

docker compose up

This is the Frontend Github

The only change you have to do to run it is changing the server IP in the config.dart file (lib/config/config.dart) with your own IP.

Map<String, Widget Function(BuildContext)> routes = {
   //...
};

const String serverURL = '192.168.50.92:8080'; <-- This line
const String initalRoute = 'splash';
const String title = "close";
ThemeData themeData = ThemeData(
    //...
)

In my case I run the command

ifconfig  | grep "inet 19"

on the terminal and I get my IP.

Reproduce the problem on the full project

Once the project is running, the problem can appear from different ways. One is reproducing the next sequence:

  1. Register (is very easy. It just has 3 fields and doesn't need any verification)

enter image description here enter image description here enter image description here enter image description here

  1. Go to the second screen and click on "Cerrar sesión"

enter image description here enter image description here

  1. Login with the same credentials

enter image description here

  1. Go to the second screen again

enter image description here

  1. Go back to the main screen

enter image description here

And now, the problem should have appear. If doesn't just try reproducing it again. Sometimes it takes one or two tries.

Here is the full sequence in video:

Error reproduction full sequence

Lucas Tomic
  • 62
  • 2
  • 14
  • 1
    widget._closeUsersService.closeCloseUsersSubscription(); isn't the problem? Does it close the stream used by the widget which may prevents a new widget to use it? – VincentDR Aug 09 '23 at 12:31
  • Yes, it's exactly what It does. But, it's not a problem because getCloseUsersStream initializes it again (I've already changed that confusing name). The stream works properly – Lucas Tomic Aug 09 '23 at 12:41
  • @LucasTomic I see that you provided a detailed explation, BUT: if you can create a minimal snippet that we can copy/paste and reproduce the problem that helps a lot – MendelG Aug 09 '23 at 12:44
  • Sure, let me add the Stream logic. Also I'll add the github repo if it helps. – Lucas Tomic Aug 09 '23 at 12:47
  • @LucasTomic Try adding `key: UniqueKey()` to the `StreamBuilder` see if it helps – MendelG Aug 09 '23 at 12:50
  • Unfortunately, I couldn't recreate the error on a minimal snippet. I've added a video showing the error and the Github link. If someone needs, I can pass the backend (it's very small and can be running with just 2 commands) – Lucas Tomic Aug 09 '23 at 15:28
  • The stream builder seems to work as expected since the breakpoint inside is hit. This means that the problem is most likely inside `UsersButtonsList` widget. Is this a `StatefulWidget`? If yes, try to replace it with a simpler `StatelessWidget` like a `Text` and see if it is updated with new values in the stream. – B0Andrew Aug 14 '23 at 12:15
  • Thanks for asking. I've repleaced with a `Text` and the problem persisted. As you can see at the post's final, I've got the same problem with another `FutureBuilder`, so the problem is not in its child. Even so, I'm adding this valuable info to the post. – Lucas Tomic Aug 14 '23 at 20:55
  • Can you add your UserButtonList widget to the question/github? Is the example plug and play executable? Or is there a detailed instruction what to do for it? Would like to try this out tonight. – Ozan Taskiran Aug 15 '23 at 15:34
  • Hello. The `UserButtonList` is on the Github. I haven't tried to copy and paste the example. As I said, there more information omitted to make the code more easy to read. I'll try to make a snippet to reproduce the problem as soon as possible. If I can't, as is a small project, I will add the few instructions to run it from a clone from Github. – Lucas Tomic Aug 15 '23 at 15:51
  • I couldn't find UserButtonList in your project, i did a global search .. can you link the file on github here? – Ozan Taskiran Aug 16 '23 at 09:03
  • Sure, here you go! https://github.com/lucastomic/close_frontend/blob/main/lib/widgets/router_screen/close_users_screen/users_buttons_list.dart It's strange you couldn't find it with a global search. I'm sorry I couldn't upload the snippet I said yesterday. I will try today. – Lucas Tomic Aug 16 '23 at 09:11
  • Ah, I had searched for UserButtonList and not UsersButtonList, that explains ... Probably it's the const constructor, I answered you under the question. :D – Ozan Taskiran Aug 16 '23 at 13:42

3 Answers3

2

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."

Ozan Taskiran
  • 2,922
  • 1
  • 13
  • 23
0

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.

yellowgray
  • 4,006
  • 6
  • 28
  • Thanks for asking. This doesn't corresponds to my problem, I do receive new data. I will add a GIF to the post which I think will explain the problem clearly – Lucas Tomic Aug 17 '23 at 07:11
  • Correct. StreamBuilder will keep the state until the stream it listens to receives new data, even if the new stream data is different. Have you tried sending the new stream some data after the stream changed? – yellowgray Aug 17 '23 at 07:18
  • You can also try to set `UniqueKey()` the second example of the StreamBuilder (_ChatRendererState). StreamBuilder won't update data by changing the initialData that way. – yellowgray Aug 17 '23 at 07:20
  • I've tried before! I forgot to add it to the post. No results – Lucas Tomic Aug 17 '23 at 07:24
  • I've added the GIF I told you before. Is in the screenshots sections of the post – Lucas Tomic Aug 17 '23 at 07:28
  • I see. I think you can share your tree structure of [Flutter inspector](https://docs.flutter.dev/tools/devtools/inspector). Select the CircularProgressIndicator to see where the widget is now. – yellowgray Aug 17 '23 at 07:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254948/discussion-between-lucas-tomic-and-yellowgray). – Lucas Tomic Aug 17 '23 at 16:14
0

cant reproduce the problem, but maybe i can help you to force rebuild when new event occurs.

first lets create notifier with event as value :

class MyStreamNotifier<T> extends ValueNotifier implements ValueListenable {
 
  MyStreamNotifier({required Stream<T> stream}) : super(null){
    notifyListeners();
    _subscription = stream.asBroadcastStream().listen((event) {
      //when there is event. just rebuild
      notifyListeners();
      _event = event;
    });

    _subscription.onError((err) {
      //handle the error
    });

  }

  late final StreamSubscription<T> _subscription;

 T? _event;

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

  @override
  get value => _event;

}

you can listen to this notifier class with ValueListenableBuilder :

ValueListenableBuilder(
     valueListenable: MyStreamNotifier(stream: stream),
       builder: (context, event, child) {
                 return child!;
               }),
Sayyid J
  • 1,215
  • 1
  • 4
  • 18
  • Hello, thanks for your answer. The debugger stops on the `notifyListeners()` with the correct stream event. However, the Widget doesn't rebuild. The debugger doesn't event stops on the builder function. – Lucas Tomic Aug 19 '23 at 19:09
  • its look like you working with a list operation, could you please elaborate what kind of event to trigger this ? the list is immutable or not? – Sayyid J Aug 20 '23 at 04:44
  • The stream drivers a List of objects from the class User, it's immutable. This list is sended from my backend, which sends the list to the websocket every two seconds periodically. However, as you can see at the bottom of the post, there is other `StreamBuilder` where the same problem appears. This seconds `StreamBuilder` gets a `Message` object every time a user uf the chat sends a message. – Lucas Tomic Aug 20 '23 at 14:43