5

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?

ArthurEKing
  • 73
  • 1
  • 10
  • What's the scope of `stream` in the second code block? from the snippet you have, it looks like it will never be re-instantiated? – emerssso Feb 11 '20 at 23:44
  • What do you mean by 'cycle of my program'? This may be related to navigation and how the SplashPage is pushed/popped in the next 'cycle'. You can try adding a key to the SplashPage. – kuhnroyal Feb 12 '20 at 09:56
  • @emerssso I feel like that may be one of the problems I'm having, but I'm not certain how to resolve it. My intention is to have the stream instantiated, and listened to when needed, and deleted once it's completed its assigned task. And IF it's needed, a brand new one created. I don't think I'm doing that, but I don't know necessarily how to do so. – ArthurEKing Feb 12 '20 at 14:59
  • @kuhnroyal By "Cycle of this program" I mean that the program is designed to do one thing, and the user would go through a series of prompts to complete this process. Once it has completed the process once, I need the program to completely forget everything that happened originally, and start fresh. If it's trying to re-use the original information, that causes issues. – ArthurEKing Feb 12 '20 at 15:01
  • If the stream is only needed in the SplashPage then just instantiate it there, otherwise you definitely have multiple subscribers. – kuhnroyal Feb 12 '20 at 15:18
  • @kuhnroyal Am I not already doing exactly that? In the Splash Page it shows DataStream stream; instantiating the variable right there. And yes, that is the only place it's ever used. – ArthurEKing Feb 12 '20 at 15:25
  • You are passing it as parameter to the constructor. – kuhnroyal Feb 12 '20 at 15:26
  • I'll assume your correct, and I honestly don't know. (This particular command has been giving me issues for a week now, and I just can't wrap my head around it). That still doesn't explain though exactly how I'm supposed to resolve that, or what I'm supposed to do. I mean what you see in the code is everything I've written that has anything to do with this stream. It has precisely one job, and that's it. – ArthurEKing Feb 12 '20 at 15:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/207684/discussion-between-kuhnroyal-and-arthureking). – kuhnroyal Feb 12 '20 at 16:42

4 Answers4

4

The StreamController default constructor creates a stream that allows only a single listener.

StreamController({void onListen(), void onPause(), void onResume(), dynamic onCancel(), bool sync: false })
A controller with a stream that supports only one single subscriber. [...]

If you want to have multipler listener use broadcast named constructor.

factory StreamController.broadcast({void onListen(), void onCancel(), bool sync: false })
A controller where stream can be listened to more than once. [...]

If you want your stream to have only one subscriber, remember to cancel your subscription in the widget's dispose method.

DataStream stream;
StreamSubscription subscription;

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

@override
void dispose() {
  subscription?.cancel();
  super.dispose();
}

Just keep in mind that's not a proper way of rebuilding your UI based on stream events. Take a look at the Stream Builder class.

Karol Lisiewicz
  • 593
  • 4
  • 11
  • Okay... Thank you for the possible answer, but what do I do if I DON'T want multiple listeners? (Which was my intention. Maybe I can't do it the way I want, but we'll see). What I mean is that I want to completely refresh my program like I never had a stream to begin with, so it shouldn't have this issue. – ArthurEKing Feb 06 '20 at 18:26
  • 2
    Ok, that's the point. The key fact is, that whenever you start listening to a stream `stream.listen((onData() {});` you get an object called `StreamSubscription`. You need to keep a reference to it, and `cancel` the subsription in the widget's `dispose` method. – Karol Lisiewicz Feb 06 '20 at 18:32
  • I think that may be part of my problem, as I AM using the dipose() method (And I checked and it is getting called) but I don't think I'm creating a new stream, and I'm not certain why it isn't creating a new stream and listening to that one. – ArthurEKing Feb 06 '20 at 19:12
  • That's great that you are disposing the stream, but are you cancelling the subscription when the widget is disposed? – Karol Lisiewicz Feb 06 '20 at 20:54
  • How do I do that? lol. (Like I said, newb with this command) – ArthurEKing Feb 06 '20 at 20:59
  • Okay, so weirdly enough my script doesn't include subscriptions at all. It works perfectly well, but there are no subscriptions (See Above). So I'm not entirely certain how to turn that off. – ArthurEKing Feb 06 '20 at 22:01
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/207394/discussion-between-karol-lisiewicz-and-arthureking). – Karol Lisiewicz Feb 07 '20 at 00:05
2

What I would do is to move stream to StatefulWidget and recreate it on "to Splash" tap

In real case scenario put it in to stateful widget in widget tree where all widgets that needs access will be able to find it (in your case even higher than navigator).

import 'package:flutter/cupertino.dart';
import 'dart:async';
import 'package:flutter/material.dart';

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

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;

  DataStream stream = DataStream();

  void changeTest(context) async {
    setState(() {
      stream = DataStream();
    });
    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),
        ),
      ),
    );
  }
}



class DataStream {
  StreamController _streamController;

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

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

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

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

}



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();
    });
  }


}
Filipkowicz
  • 494
  • 5
  • 17
1

Without being able to grasp your problem in its entirety, I'd just like to state that I've been having headaches pretty much every time I've used "normal" streams myself and RxDart has been like Aspirin in the world of streams for me :) Not sure if this is the answer you're looking for, but I thought I'd post it anyway - you never know!

kazume
  • 1,063
  • 1
  • 11
  • 22
0

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.

ArthurEKing
  • 73
  • 1
  • 10