Pass StreamBuilder between BottomNavigationBar tabs => Bad state: Stream has already been listened to
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.
DomeDev
Updated on December 25, 2022Comments
-
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(), ), ); } }