Navigating to a new screen when stream value in BLOC changes
Asked Answered
M

2

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

Moser answered 9/1, 2019 at 0:15 Comment(2)
May we know what error you got?Waly
@JeromeEscalante I've added the errorMoser
P
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();
  }
}
Phenomenalism answered 9/1, 2019 at 12:16 Comment(3)
I was hacking around Streambuilder for the longest time with navigation logic. This method makes more sense and the performance gains are visibly noticeable.Administration
@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 itselfHawkeyed
Where the context of Navigator.pushNamed(context, '/someRoute/$value'); come from?Ashleighashlen
W
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();
    });
Woodberry answered 15/12, 2019 at 19:52 Comment(1)
This is the better answer. There might be some merit in exposing the snapshot to the listener callback, but this is a trivial change.Jeggar

© 2022 - 2024 — McMap. All rights reserved.