Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Provider: How can I `notifyListener()` within a `StreamBuilder()`? It causes the error `setState() or markNeedsBuild() called during build`

I have a Provider model such as provider_model.dart:

import 'package:flutter/material.dart';

class ProviderModel extends ChangeNotifier {
  final List<String> _myList = [];

  List<String> get myList => [..._myList];

  void addItem(String item) {
    _myList.add(item);
    notifyListeners();
  }
}

Now, Flutter documentation shows us how to listen to websockets. Here I am using their example together with my ProviderModel():

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

import './provider_model.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ChangeNotifierProvider(
          create: (BuildContext context) => ProviderModel(),
          child: Scaffold(
            body: MyHomePage(),
          )),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _controller = TextEditingController();

  final _channel = WebSocketChannel.connect(
    Uri.parse('wss://echo.websocket.events'),
  );

  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderModel>(
      builder: (context, provider, _) {
        return Column(children: [
          TextField(
            controller: _controller,
            decoration: InputDecoration(
                border: OutlineInputBorder(),
                hintText: 'Enter a search term',
                suffixIcon: IconButton(
                    icon: Icon(
                      Icons.send,
                    ),
                    onPressed: _sendMessage)),
          ),
          ...provider.myList.map((e) => Text(e)).toList(),
          StreamBuilder(
            stream: _channel.stream,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                // ERROR HERE!
                provider.addItem('I was added');
                return Text("Item added");
              } else if (snapshot.hasError) {
                return Text(snapshot.error as String);
              } else {
                return CircularProgressIndicator();
              }
            },
          )
        ]);
      },
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      _channel.sink.add(_controller.text);
      print('done');
    }
  }
}

Which the following output (Chrome): enter image description here Now, when I click on the send button, it calls _sendMessage() (code above). And then since the StreamBuilder() hasData it runs this line (code above):

provider.addItem('I was added');

However, this is where my error appears, I am getting the following error:

The following assertion was thrown while dispatching notifications for ProviderModel:
setState() or markNeedsBuild() called during build.
  • Why am I getting this error? Where is the widget rebuilding? See this answer to Flutter Provider setState() or markNeedsBuild() called during build
like image 363
MendelG Avatar asked Jan 18 '26 07:01

MendelG


1 Answers

You can take help from addPostFrameCallback, but the cost is it will keep rebuilding on every frame, to control this behavior you can use a bool on state class.

  bool isDone = false;

  void addItemH() {
    if (!isDone) {
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
        Provider.of<ProviderModel>(context, listen: false)
            .addItem('I was added');
        isDone = true;
      });
    }
  }

And builder

 builder: (context, snapshot) {
                if (snapshot.hasData) {
                  addItemH();
                  return Text("Item added");

And to control the next insertaion.

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      isDone = false;
      _channel.sink.add(_controller.text);
      print('done');
    }
  }
}

If you just want to add data only single time, it is better to do inside inside initState.

like image 152
Yeasin Sheikh Avatar answered Jan 21 '26 00:01

Yeasin Sheikh