Flutter StreamBuilder Refresh Screen with updated list data from API

Issue

I am trying to convert my app to use a streamBuilder periodically of three seconds.
however, what I have now ends up with the error.
Bad state: Stream has already been listened to.
Note that I am using two list views on the screen to display active and non active jobs.
here is the code from the request to the stream.

List<Ticket> sortedListAwaiting = [];
List<Ticket> sortedListActive = [];

Future<List<Ticket>> getAssignedAwaitingTickets() async {
  client.badCertificateCallback =
      ((X509Certificate cert, String host, int port) => true);
  HttpClientRequest request =
      await client.getUrl(Uri.parse("$emulatorHost/Jobs"));
  request.headers.add("Authorization", "Bearer " + jwt);
  HttpClientResponse result = await request.close();
  if (result.statusCode == 200) {
    List<dynamic> jsonData =
        jsonDecode(await result.transform(utf8.decoder).join());

    if (sortedListAwaiting.isNotEmpty) {
      sortedListAwaiting.clear();
      for (var i in jsonData) {
        if (i['status'] == "Ready-To-Start" &&
            i['staffName'] == name.toLowerCase()) {
          sortedListAwaiting.add(new Ticket(
            i['jobID'],
            i['staffName'],
            i['clientName'],
            i['jobName'],
            i['jobAddress'],
            i['clientContact'],
            i['jobDescription'],
            i['jobDate'],
            i['jobTime'],
            i['status'],
            i['isEditing'],
          ));
        }
      }
    } else {
      for (var i in jsonData) {
        if (i['status'] == "Ready-To-Start" &&
            i['staffName'] == name.toLowerCase()) {
          sortedListAwaiting.add(
            new Ticket(
              i['jobID'],
              i['staffName'],
              i['clientName'],
              i['jobName'],
              i['jobAddress'],
              i['clientContact'],
              i['jobDescription'],
              i['jobDate'],
              i['jobTime'],
              i['status'],
              i['isEditing'],
            ),
          );
        }
      }
    }
  }
  return sortedListAwaiting;
}

Stream<List<Ticket>> getSortedListAwaiting() =>
    Stream.periodic(Duration(seconds: 3))
        .asyncMap((event) => getAssignedAwaitingTickets());

Future<List<Ticket>> getAssignedActiveTickets() async {
  client.badCertificateCallback =
      ((X509Certificate cert, String host, int port) => true);
  HttpClientRequest request =
      await client.getUrl(Uri.parse("$emulatorHost/Jobs"));
  request.headers.add("Authorization", "Bearer " + jwt);
  HttpClientResponse result = await request.close();
  if (result.statusCode == 200) {
    List<dynamic> jsonData =
        jsonDecode(await result.transform(utf8.decoder).join());

    if (sortedListActive.isNotEmpty) {
      sortedListActive.clear();
      for (var i in jsonData) {
        if (i['status'] == "In-Route" && i['staffName'] == name.toLowerCase()) {
          sortedListActive.add(new Ticket(
            i['jobID'],
            i['staffName'],
            i['clientName'],
            i['jobName'],
            i['jobAddress'],
            i['clientContact'],
            i['jobDescription'],
            i['jobDate'],
            i['jobTime'],
            i['status'],
            i['isEditing'],
          ));
        }
      }
    } else {
      for (var i in jsonData) {
        if (i['status'] == "In-Route" && i['staffName'] == name.toLowerCase()) {
          sortedListActive.add(
            new Ticket(
              i['jobID'],
              i['staffName'],
              i['clientName'],
              i['jobName'],
              i['jobAddress'],
              i['clientContact'],
              i['jobDescription'],
              i['jobDate'],
              i['jobTime'],
              i['status'],
              i['isEditing'],
            ),
          );
        }
      }
    }
  }
  return sortedListActive;
}

Stream<List<Ticket>> getSortedListActive() =>
    Stream.periodic(Duration(seconds: 3))
        .asyncMap((event) => getAssignedAwaitingTickets());

First List View

TabBarView(
            children: [
              Center(
                child: Padding(
                  padding: EdgeInsets.only(left: 18, right: 18),
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        Row(
                          children: [
                            Container(
                              margin: EdgeInsets.only(top: 10),
                              child: Text(
                                "Active",
                              ),
                            ),
                          ],
                        ),
                        StreamBuilder<List<Ticket>>(
                            stream: getSortedListActive(),
                            builder: (context,
                                AsyncSnapshot<List<Ticket>> snapshot) {
                              if (snapshot.connectionState ==
                                  ConnectionState.waiting) {
                                return CircularProgressIndicator();
                              }
                              if (snapshot.hasData) {
                                return Container(
                                  margin: EdgeInsets.only(top: 20),
                                  height: 240,
                                  child: ListView.builder(
                                    itemBuilder: (context, index) {
                                      return Padding(
                                        padding:
                                            const EdgeInsets.only(bottom: 16.0),
                                        child: GestureDetector(
                                          onTap: () async {
                                            await getSpecificJob(
                                                activeJobTickets[index].JobID);
                                            await getLatLng(
                                                sortedListActive[index]
                                                    .jobAddress);
                                            print(
                                                activeJobTickets[index].JobID);
                                            Navigator.of(context).push(
                                              MaterialPageRoute(
                                                builder: (context) =>
                                                    TicketScreen(
                                                  activeJobTickets[index].JobID,
                                                  activeJobTickets[index]
                                                      .staffName,
                                                  activeJobTickets[index]
                                                      .clientName,
                                                  activeJobTickets[index]
                                                      .jobName,
                                                  activeJobTickets[index]
                                                      .jobAddress,
                                                  activeJobTickets[index]
                                                      .clientContact,
                                                  activeJobTickets[index]
                                                      .jobDescription,
                                                  activeJobTickets[index]
                                                      .jobDate,
                                                  activeJobTickets[index]
                                                      .jobTime,
                                                  activeJobTickets[index]
                                                      .status,
                                                ),
                                              ),
                                            );
                                          },
                                          child: newTicketWidget(
                                            sortedListActive[index].jobName,
                                            sortedListActive[index]
                                                .jobDescription,
                                            sortedListActive[index].jobAddress,
                                            sortedListActive[index].staffName,
                                            sortedListActive[index].jobDate,
                                            sortedListActive[index].jobTime,
                                          ),
                                        ),
                                      );
                                    },
                                    itemCount: sortedListActive.length,
                                  ),
                                );
                              }
                              return CircularProgressIndicator();
                            }),

Second List View

StreamBuilder<List<Ticket>>(
                          stream: getSortedListAwaiting(),
                          builder:
                              (context, AsyncSnapshot<List<Ticket>> snapshot) {
                            if (snapshot.connectionState ==
                                ConnectionState.waiting) {
                              return CircularProgressIndicator();
                            }
                            if (snapshot.hasData) {
                              return Container(
                                margin: EdgeInsets.only(top: 20),
                                height: 240,
                                child: ListView.builder(
                                  itemBuilder: (context, index) {
                                    return Padding(
                                      padding:
                                          const EdgeInsets.only(bottom: 16.0),
                                      child: GestureDetector(
                                        onTap: () async {
                                          getSpecificJob(
                                              awaitingJobTickets[index].JobID);
                                          await getLatLng(
                                              sortedListAwaiting[index]
                                                  .jobAddress);
                                          Navigator.of(context)
                                              .push(MaterialPageRoute(
                                            builder: (context) => TicketScreen(
                                                sortedListAwaiting[index].JobID,
                                                sortedListAwaiting[index]
                                                    .staffName,
                                                sortedListAwaiting[index]
                                                    .clientName,
                                                sortedListAwaiting[index]
                                                    .jobName,
                                                sortedListAwaiting[index]
                                                    .jobAddress,
                                                sortedListAwaiting[index]
                                                    .clientContact,
                                                sortedListAwaiting[index]
                                                    .jobDescription,
                                                sortedListAwaiting[index]
                                                    .jobDate,
                                                sortedListAwaiting[index]
                                                    .jobTime,
                                                sortedListAwaiting[index]
                                                    .status),
                                          ));
                                        },
                                        child: newTicketWidget(
                                          sortedListAwaiting[index].jobName,
                                          sortedListAwaiting[index]
                                              .jobDescription,
                                          sortedListAwaiting[index].jobAddress,
                                          sortedListAwaiting[index].staffName,
                                          sortedListAwaiting[index].jobDate,
                                          sortedListAwaiting[index].jobTime,
                                        ),
                                      ),
                                    );
                                  },
                                  itemCount: sortedListAwaiting.length,
                                ),
                              );
                            }
                            return CircularProgressIndicator();
                          },
                        ),

I am completely foreign to using StreamBuilder I usually just use a listview.builder on its own but now I need the data to stay refreshed every three seconds. So any guides, explanation and examples will be very appreciative.
Cheers In advance developers.

Edit
I was told to try adding the stream to var and set it in the innit state however I still have the error

late Stream<List<Ticket>> myPeriodicStream;
  late Stream<List<Ticket>> myPeriodicStream1;
  @override
  void initState() {
    refreshLists();

    super.initState();
    myPeriodicStream = getSortedListActive();
    myPeriodicStream1 = getSortedListAwaiting();
  }

Solution

The problem wasn’t with the stream but with the tab controller. I found this by trial and error and concluded that the problem was the DefaultTabBarController. Once I removed the DefaultTabController the stream wasnt given a bad state. A work around possibly coul dbe to figure out a way to dispose the controller within the dispose method. However, I switched to using a bottom navigation bar and used the index of the bar to change the page

Answered By – Riley-Howley

Answer Checked By – Clifford M. (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *