Flutter, "Stream has already been listened to" error in GridView
The Stream only needs to use a Broadcast if the same stream has to be listened to multiple times.
i.e.
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
flex: 1,
child: StreamBuilder()),
Expanded(
flex: 1,
child: StreamBuilder()),
],
);
}
I can't seem to replicate the same error on my own implementation of displaying images from network on a GridView.
The Stream in the sample below doesn't need to use a Broadcast to refresh the Stream since it's being listened to by a single client. The GridView can be refreshed by triggering RefreshIndicator onRefresh
. Continuous call on Future<T>().then((response) => StreamController.add(response));
in this sample doesn't cause Bad state: Stream has already been listened to.
errors.
If you can provide a complete minimal repro that I can run locally, I can help check what might be causing the errors thrown by the Stream.
Here's a full sample that you can try. Pull the page to refresh and reset the pagination, and scroll to the bottom of the page to load the next images. Images displayed in the sample was fetched from https://jsonplaceholder.typicode.com/photos
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _streamController = StreamController<List<Album>>();
var _scrollController = ScrollController();
// GridView has 3 columns set
// Succeeding pages should display in rows of 3 for uniformity
loadMoreImages(bool increment) {
setState(() {
if(!increment) _imageGridCursorEnd = 21;
else _imageGridCursorEnd += 21;
});
}
// Call to fetch images
// if refresh set to true, it will trigger setState() to reset the GridView
loadImages(bool refresh){
fetchAlbum().then((response) => _streamController.add(response));
if(refresh)loadMoreImages(!refresh); // refresh whole GridView
}
@override
void initState() {
super.initState();
loadImages(false);
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == 0)
print('Grid scroll at top');
else {
print('Grid scroll at bottom');
loadMoreImages(true);
}
}
});
}
@override
void dispose() {
super.dispose();
_streamController.close();
}
var _imageGridCursorStart = 0, _imageGridCursorEnd = 21;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _streamController.stream,
builder: (BuildContext context, AsyncSnapshot<List<Album>> snapshot) {
if (snapshot.hasData) {
// This ensures that the cursor won't exceed List<Album> length
if (_imageGridCursorEnd > snapshot.data.length)
_imageGridCursorEnd = snapshot.data.length;
debugPrint('Stream snapshot contains ${snapshot.data.length} item/s');
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: RefreshIndicator(
// onRefresh is a RefreshCallback
// RefreshCallback is a Future Function().
onRefresh: () async => loadImages(true),
child: snapshot.hasData
? GridView.count(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
primary: false,
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 3,
children: getListImg(snapshot.data
.getRange(_imageGridCursorStart, _imageGridCursorEnd)
.toList()),
)
: Text('Waiting...'),
),
),
);
},
);
}
Future<List<Album>> fetchAlbum() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/photos');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
Iterable iterableAlbum = json.decode(response.body);
var albumList = List<Album>();
List<Map<String, dynamic>>.from(iterableAlbum).map((Map model) {
// Add Album mapped from json to List<Album>
albumList.add(Album.fromJson(model));
}).toList();
return albumList;
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
getListImg(List<Album> listAlbum) {
final listImages = List<Widget>();
for (var album in listAlbum) {
listImages.add(
Container(
padding: const EdgeInsets.all(8),
child: Image.network(album.albumThumbUrl, fit: BoxFit.cover),
// child: Thumbnail(image: imagePath, size: Size(100, 100)),
),
);
}
return listImages;
}
}
class Album {
final int albumId;
final int id;
final String title;
final String albumImageUrl;
final String albumThumbUrl;
Album(
{this.albumId,
this.id,
this.title,
this.albumImageUrl,
this.albumThumbUrl});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
albumId: json['albumId'],
id: json['id'],
title: json['title'],
albumImageUrl: json['url'],
albumThumbUrl: json['thumbnailUrl'],
);
}
}
Demo
baeharam
Updated on December 08, 2022Comments
-
baeharam over 1 year
What I'm doing is to fetch cartoon list and show by GridView. Below code is fetching data
Future<void> _getWebtoonData() async { var response; if(_daysReceivedResponse[_pressedButtonDayIndex]){ response = _daysResponse[_pressedButtonDayIndex]; } else { response= await http.get('https://comic.naver.com/webtoon/weekdayList.nhn?week='+_currentWebtoonAddress); _daysReceivedResponse[_pressedButtonDayIndex] = true; _daysResponse[_pressedButtonDayIndex] = response; } dom.Document document = parser.parse(response.body); final e1 = document.querySelectorAll('.img_list .thumb'); final e2 = document.querySelectorAll('.img_list .desc'); final e3 = document.querySelectorAll('.img_list .rating_type'); List<List<String>> infoCollection = List<List<String>>(); List<String> info = List<String>(); for(int i=0; i<e1.length; i++){ info.add(e1[i].getElementsByTagName('img')[0].attributes['src']); info.add(e1[i].getElementsByTagName('a')[0].attributes['title']); info.add(e2[i].getElementsByTagName('a')[0].innerHtml); info.add(e3[i].getElementsByTagName('strong')[0].innerHtml); infoCollection.add(info); } _controller.sink.add(infoCollection); }
And I'm showing this images, titles, artists and rate by GridView like below
Widget _getWebtoonGridView() { return StreamBuilder( stream: _controller.stream.asBroadcastStream(), builder: (BuildContext context, AsyncSnapshot<List> snapshot){ if(snapshot.hasError) print(snapshot.error); else if(snapshot.hasData){ return GridView.count( crossAxisCount: 3, childAspectRatio: 0.6, children: List.generate(snapshot.data.length, (index){ return _getWebtoonInfo(index, snapshot.data[index]); }), ); } else if(snapshot.connectionState != ConnectionState.done) return Center(child: CircularProgressIndicator()); }, ); }
But "Stream has already been listened to" error is occurring continuously, what is the problem about my StreamController??
How can I fix it?
StackTrace
I/flutter (21411): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ I/flutter (21411): The following StateError was thrown building Expanded(flex: 1): I/flutter (21411): Bad state: Stream has already been listened to. I/flutter (21411): I/flutter (21411): When the exception was thrown, this was the stack: I/flutter (21411): #4 _StreamBuilderBaseState._subscribe (package:flutter/src/widgets/async.dart:135:37) I/flutter (21411): #5 _StreamBuilderBaseState.initState (package:flutter/src/widgets/async.dart:109:5) I/flutter (21411): #6 StatefulElement._firstBuild(package:flutter/src/widgets/framework.dart:3830:58) I/flutter (21411): #7 ComponentElement.mount(package:flutter/src/widgets/framework.dart:3696:5) I/flutter (21411): #8 Element.inflateWidget (package:flutter/src/widgets/framework.dart:2950:14) I/flutter (21411): #9 Element.updateChild(package:flutter/src/widgets/framework.dart:2753:12)
StreamController variable
StreamController<List<List<String>>> _controller = StreamController<List<List<String>>>.broadcast();
-
pskink over 5 yearswhats the stacktrace (first few frames)?
-
baeharam over 5 years@pskink I added
-
pskink over 5 yearshow do you your
StreamController
? -
baeharam over 5 years@pskink I added
-
pskink over 5 yearsso why do you need
.asBroadcastStream()
? remove it,hot restart
your app and see what happens -
baeharam over 5 yearssame errors.....
-
pskink over 5 yearsdid you do
hot restart
? nothot reaload
-
baeharam over 5 yearsGot it, but another problem was occured, how can I StreamBuilder to GridView? GridView itself or item of that?
-
pskink over 5 yearsi have no idea what you mean
-
baeharam over 5 yearssee my above code
-