Pass StreamBuilder between BottomNavigationBar tabs => Bad state: Stream has already been listened to

231

It's about widgets lifecycle. I can suggest you two options.

1. Move _streamController and _getData() method to _CompanyTabState.

By default BottomNavigationBar destroys tab when you go away from one and init it again when you return back to it. If it's desired behaviour you need to move _streamController and _getData() method into _CompanyTabState. Don't forget to call _streamController.close() inside dispose() method of _CompanyTabState, it's important. _companyService can be injected into _CompanyTabState. It's a matter of it's life time. Should work like this:

...
class _CompanyTabState extends State<CompanyTab> {
  final _streamController = StreamController<List<Company>>();
  final CompanyService _companyService;

  _CompanyTabState(this._companyService);

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

  StreamBuilder companyList() {
    return StreamBuilder<List<Company>>(
        initialData: [],
        stream: _streamController.stream,
        builder: (BuildContext context, AsyncSnapshot<List<Company>> snapshot) {
          if (snapshot.hasError) {
            return Text("Something went wrong");
          }

          if (snapshot.connectionState == ConnectionState.waiting ||
              snapshot.connectionState == ConnectionState.none ||
              snapshot.data == null) {
            return LoadingWidget();
          } else {
            return ListView.builder(
                padding: const EdgeInsets.all(10),
                itemCount: snapshot.data.length,
                itemBuilder: (BuildContext context, int index) {
                  Company company = snapshot.data.elementAt(index);
                  return Padding(
                    padding: const EdgeInsets.symmetric(
                        vertical: 1.0, horizontal: 4.0),
                    child: Card(
                      child: ListTile(
                        onTap: () {},
                        title: Text(company.name),
                        ...
                        ),
                      ),
                    ),
                  );
                });
          }
        });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: companyList(),
      ),
    );
  }

  @override
  void dispose() {
    super.dispose();
    _streamController.close();
  }

  void _getData() {
    _companyService
        .getCompanies()
        .then((value) => _streamController.sink.add(value));
  }
}

2. Use IndexedStack

You can save tab's state and widget data (like scroll offset, entered text etc.) when you go away from the tab. It's iOS UITabBarController-like behaviour. Use IndexedStack to achieve this:

...
    return Scaffold(
    ...
        actions: ...,
      body: IndexedStack(
        children: _tabs,
        index: _currentIndex,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        backgroundColor: BACKGROUND_COLOR,
        type: BottomNavigationBarType.fixed,
        items: [
          BottomNavigationBarItem(
            ...
          ),
          BottomNavigationBarItem(
            ...
          ),
          BottomNavigationBarItem(
            ...
          ),
          BottomNavigationBarItem(
            ...
          )
        ],
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );

What option to use is up to you, you can use both if you want. But I would strongly recommend to move _streamController to _CompanyTabState as their lifecycles should be the same.

Share:
231
DomeDev
Author by

DomeDev

Updated on December 25, 2022

Comments

  • DomeDev
    DomeDev over 1 year

    I'm trying to pass Stream from a tab to another tab but when I comeback in the home.dart May I should close/destroy the Stream when tab changed? When the app runs the data are fetched correctly and everything is good.The problem appears only when I change tab. The data are stored to a firestore database.

    I get this error:

    Bad state: Stream has already been listened to

    Here my Home.dart

    class HomePage extends StatefulWidget {
      HomePage({Key key}) : super(key: key);
      Home createState() => Home();
    }
    
    class Home extends State<HomePage> {
      int _currentIndex;
      var _tabs = [];
    
      List<Company> currentCompaniesList = List();
      StreamController<List<Company>> _streamController;
      Stream<List<Company>> companiesStream;
    
      _getData() async {
        _companyService
            .getCompanies()
            .then((value) => _streamController.sink.add(value));
      }
    
      @override
      void initState() {
        super.initState();
        _currentIndex = 0;
    
        _streamController = StreamController<List<Company>>();
    
        _getData();
        companiesStream = _streamController.stream;
      }
    }
    
    
    @override
      Widget build(BuildContext context) {
        _tabs = [
          CompanyTab(stream: companiesStream),
          MapTab(),
          Center(child: Text('Profile')),
          Center(child: Text('Settings')),
        ];
    
        return Scaffold(
        ...
            actions: ...,
          body: _tabs[_currentIndex],
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: _currentIndex,
            backgroundColor: BACKGROUND_COLOR,
            type: BottomNavigationBarType.fixed,
            items: [
              BottomNavigationBarItem(
                ...
              ),
              BottomNavigationBarItem(
                ...
              ),
              BottomNavigationBarItem(
                ...
              ),
              BottomNavigationBarItem(
                ...
              )
            ],
            onTap: (index) {
              setState(() {
                _currentIndex = index;
              });
            },
          ),
        );
      }
    }
    

    Here my CompanyTab.dart

    class CompanyTab extends StatefulWidget {
      Stream stream;
    
      CompanyTab({Key key, this.stream}) : super(key: key);
    
      @override
      _CompanyTabState createState() => _CompanyTabState(stream);
    }
    
    class _CompanyTabState extends State<CompanyTab> {
      Stream stream;
    
      _CompanyTabState(this.stream);
    
      @override
      void initState() {
        super.initState();
      }
    
      StreamBuilder companyList() {
        return StreamBuilder<List<Company>>(
            initialData: [],
            stream: stream,
            builder: (BuildContext context, AsyncSnapshot<List<Company>> snapshot) {
              if (snapshot.hasError) {
                return Text("Something went wrong");
              }
    
              if (snapshot.connectionState == ConnectionState.waiting ||
                  snapshot.connectionState == ConnectionState.none ||
                  snapshot.data == null) {
                return LoadingWidget();
              } else {
                return ListView.builder(
                    padding: const EdgeInsets.all(10),
                    itemCount: snapshot.data.length,
                    itemBuilder: (BuildContext context, int index) {
                      Company company = snapshot.data.elementAt(index);
                      return Padding(
                        padding: const EdgeInsets.symmetric(
                            vertical: 1.0, horizontal: 4.0),
                        child: Card(
                          child: ListTile(
                            onTap: () {},
                            title: Text(company.name),
                            ...
                            ),
                          ),
                        ),
                      );
                    });
              }
            });
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Container(
            child: companyList(),
          ),
        );
      }
    }