Flutter, "Stream has already been listened to" error in GridView

4,470

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

Demo

Share:
4,470
baeharam
Author by

baeharam

Updated on December 08, 2022

Comments

  • baeharam
    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
      pskink over 5 years
      whats the stacktrace (first few frames)?
    • baeharam
      baeharam over 5 years
      @pskink I added
    • pskink
      pskink over 5 years
      how do you your StreamController?
    • baeharam
      baeharam over 5 years
      @pskink I added
    • pskink
      pskink over 5 years
      so why do you need .asBroadcastStream()? remove it, hot restart your app and see what happens
    • baeharam
      baeharam over 5 years
      same errors.....
    • pskink
      pskink over 5 years
      did you do hot restart? not hot reaload
    • baeharam
      baeharam over 5 years
      Got it, but another problem was occured, how can I StreamBuilder to GridView? GridView itself or item of that?
    • pskink
      pskink over 5 years
      i have no idea what you mean
    • baeharam
      baeharam over 5 years
      see my above code