Search code examples
iosflutterdata-stream

Flutter DataStream not closing and re-building properly. [Bad state: Stream has already been listened to.]


Okay, so I'm aware that a stream can be manufactured to listen to a stream more than once, using a broadcast system, but that's specifically NOT what I'm trying to do here.

I'm also editing this as the one answer I have received isn't currently able to resolve my issue, so any assistance would be greatly appreciated.

Effectively for some reason the code I have is not deleting the stream in it's entirety, and if re-used, it is trying to re-listen to the same stream that has already been listened-to and closed, none of which works (Obviously). Instead of trying to listen to that same stream again, I'm trying to create a NEW stream to listen to. (Deleting and cleaning away all information from the original first stream).

Original post continues below:

I'm using a DataStream template for streaming data to and/or from various parts of my program, and I'm not entirely certain how to rectify this. I'm certain it's a silly newb error, but I haven't used DataStreams enough to understand why this is happening.

Now don't get me wrong, going through a single cycle of my program works perfectly fine, no issues at all. However, once I've completed a single cycle through the program, if I try to go through a second time, I get the error:

Bad state: Stream has already been listened to.

So from this I know my program is not creating a new stream, and instead trying to re-use the original stream, and I'm not 100% certain how to stop this functionality, (Or even if I should). (Honestly the number of times I would expect multiple cycles to be completed are slim to null, but I want to resolve these kinds of errors before they become problems.)

Edit: Minimal Reproducible Example to Follow

File 1 (main.dart)

import 'package:flutter/cupertino.dart';
import 'dart:async';
import './page2.dart';
import './stream.dart';

void main() => runApp(MyApp());

DataStream stream = DataStream();


class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      title: 'Splash Test',
      theme: CupertinoThemeData(
        primaryColor: Color.fromARGB(255, 0, 0, 255),
      ),
      home: MyHomePage(title: 'Splash Test 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> {
  bool textBool = false;
  int counter = 0;

  void changeTest(context) async {
    int counter = 0;
    Timer.periodic(Duration (seconds: 2), (Timer t) {
      counter++;
      stream.dataSink.add(true);
      if (counter >= 3) {
        t.cancel();
        stream.dispose();
        Navigator.pop(context);
      } 
    },);
    Navigator.push(context, CupertinoPageRoute(builder: (context) => Page2(stream: stream)));
  }



  @override
  Widget build(BuildContext context) {

    return CupertinoPageScaffold(
      child: Center(
        child: CupertinoButton(
          child: Text('To Splash'),
          onPressed: () => changeTest(context),
        ),
      ), 
    );
  }
}

File 2 (stream.dart)

import 'dart:async';

class DataStream {
  StreamController _streamController;

    StreamSink<bool> get dataSink =>
      _streamController.sink;

  Stream<bool> get dataStream =>
      _streamController.stream;

  DataStream() {
    _streamController = StreamController<bool>();
  }

  dispose() {
    _streamController?.close();
  }

}

File 3 (page2.dart)

import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';

import './main.dart';
import './stream.dart';


class Page2 extends StatefulWidget {

  DataStream stream;
  Page2({this.stream});

  @override 
  State<StatefulWidget> createState() => new PageState();
}

class PageState extends State<Page2> {


bool textChanger = false;
bool firstText = true;

Text myText() {
  if (textChanger) {
    Text text1 = new Text('Text One', 
      style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
    return text1;
  } else {
    Text text1 = new Text('Text Two', 
      style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
    return text1;
  }
}

void changeText() {
  if (!firstText) {
    if (textChanger) {
      print('Change One');
      setState(() { 
        textChanger = false;      
      });
    } else {
      print('Change Two');
      setState(() {  
        textChanger = true;    
      });
    }  
  } else {
    firstText = false;
  }
}


  @override 
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Container(
        child: Center(
          child: myText()
        ) 
      ),);
  }

@override
  void initState() {
    super.initState();
    widget.stream.dataStream.listen((onData) {
      changeText();
    });
  }


}

Effectively, in this example, you can click on the text, and goto the second page, which will correctly change text when told, and return to the original page once completed. That would be a single "Cycle" of my program.

And you can see that this program then immediately disposes of the stream.

The issue is that if I click the text a second time, it is still trying to listen to the original stream, rather than creating a brand new stream and starting fresh.

Why? And how do I fix this?


Solution

  • Aha! I managed to figure it out. (Thank you all for trying to help, it really is much appreciated). It was actually a really stupid noob mistake that as soon as I took a step back, I saw it.

    You'll notice in the main.dart file I have the line

    DataStream stream = DataStream();
    

    And I have this set as a global variable. So any of the parts of the program can access the information as needed. Which is the way I kinda need to set it up... But I forgot that it can be instanced.

    So what I did was change it to:

    DataStream stream;
    

    And then later in my main.dart file, just prior to pushing the Navigator, I add the line

    stream = new DataStream();
    Navigator.push(context, CupertinoPageRoute(builder: (context) => Page2(stream: stream)));
    

    So now I'm creating a new instance of the stream, after it's been properly disposed from the earlier bits of the program. smacks head. Shoulda figured this out a week ago.