18

In Flutter how would I call Navigator.push when the value of a stream changes? I have tried the code below but get an error.

StreamBuilder(
        stream: bloc.streamValue,
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
          if (snapshot.hasData && snapshot.data == 1) {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SomeNewScreen()),
            );
          }

          return Text("");
        });

enter image description here

Arizona1911
  • 2,181
  • 9
  • 29
  • 38

2 Answers2

33

You should not use StreamBuilder to handle navigation. StreamBuilder is used to build the content of a screen and nothing else.

Instead, you will have to listen to the stream to trigger side-effects manually. This is done by using a StatefulWidget and overriding initState/dispose as such:

class Example extends StatefulWidget {
  final Stream<int> stream;

  const Example({Key key, this.stream}) : super(key: key);

  @override
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  StreamSubscription _streamSubscription;

  @override
  void initState() {
    super.initState();
    _listen();
  }

  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.stream != widget.stream) {
      _streamSubscription.cancel();
      _listen();
    }
  }

  void _listen() {
    _streamSubscription = widget.stream.listen((value) {
      Navigator.pushNamed(context, '/someRoute/$value');
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Note that if you're using an InheritedWidget to obtain your stream (typically BLoC), you will want to use didChangeDependencies instead of initState/didUpdateWidget.

This leads to:

class Example extends StatefulWidget {
  @override
  ExampleState createState() => ExampleState();
}

class ExampleState extends State<Example> {
  StreamSubscription _streamSubscription;
  Stream _previousStream;

  void _listen(Stream<int> stream) {
    _streamSubscription = stream.listen((value) {
      Navigator.pushNamed(context, '/someRoute/$value');
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final bloc = MyBloc.of(context);
    if (bloc.stream != _previousStream) {
      _streamSubscription?.cancel();
      _previousStream = bloc.stream;
      _listen(bloc.stream);
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • I was hacking around Streambuilder for the longest time with navigation logic. This method makes more sense and the performance gains are visibly noticeable. – S1r-Lanzelot Mar 06 '19 at 02:20
  • 2
    @BangOperator "didChangeDependencies get called(for some reason), thereby creating a new bloc" I don't think is correct, using an inheritedWidget per Rémi's example code you will get the same bloc each time *unless* that InheritedWidget (which is immuatble) changes itself – Maks Aug 02 '19 at 00:47
  • Where the context of Navigator.pushNamed(context, '/someRoute/$value'); come from? – Fernando Santos Aug 24 '20 at 17:42
9

You can extend StreamBuilder with custom listener like this:

typedef StreamListener<T> = void Function(T value);

class StreamListenableBuilder<T> extends StreamBuilder<T> {

  final StreamListener<T> listener;

  const StreamListenableBuilder({
    Key key,
    T initialData,
    Stream<T> stream,
    @required this.listener,
    @required AsyncWidgetBuilder<T> builder,
  }) : super(key: key, initialData: initialData, stream: stream, builder: builder);

  @override
  AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
    listener(data);
    return super.afterData(current, data);
  }
}

Then connect listener for navigation this way:

StreamListenableBuilder(
    stream: bloc.streamValue,
    listener: (value) {
      if (value==1) {
        Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => SomeNewScreen()),
        );
      }
    },
    builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
      return Container();
    });
Makarenkov Oleg
  • 193
  • 1
  • 7
Eugene Brusov
  • 17,146
  • 6
  • 52
  • 68
  • This is the better answer. There might be some merit in exposing the snapshot to the listener callback, but this is a trivial change. – Ian Apr 19 '21 at 10:10