Navigating to a New Screen When Stream Value in Bloc Changes

Navigating to a new screen when stream value in BLOC changes

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

Bloc navigation on state change

I found the following solution:

if (state is LoggedIn) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Navigation
});
}

I wrapped my navigation with this addPostFrame callback for delaying its appearance.

Bloc, Flutter and Navigation

To get the myth of BLoC being the way forward right out of the way: There is no perfect way for handling state.
Every state management architecture solves some problems better than others; there are always trade-offs and it's important to be aware of them when deciding on an architecture.

Generally, good architecture is practical: It's scalable and extensible while only requiring minimal overhead.
Because people's views on practicability differ, architecture always involves opinion, so take the following with a grain of salt as I will lay out my personal view on how to adopt BLoC for your app.

BLoC is a very promising approach for state management in Flutter because of one signature ingredient: streams.
They allow for decoupling the UI from the business logic and they play well with the Flutter-ish approach of rebuilding entire widget subtrees once they're outdated.
So naturally, every communication from and to the BLoC should use streams, right?

+----+  Stream   +------+
| UI | --------> | BLoC |
| | <-------- | |
+----+ Stream +------+

Well, kind of.

The important thing to remember is that state management architecture is a means to an end; you shouldn't just do stuff for the sake of it but keep an open mind and carefully evaluate the pros and cons of each option.
The reason we separate the BLoC from the UI is that the BLoC doesn't need to care about how the UI is structured – it just provides some nice simple streams and whatever happens with the data is the UI's responsibility.

But while streams have proven to be a fantastic way of transporting information from the BLoC to the UI, they add unnecessary overhead in the other direction:
Streams were designed to transport continuous streams of data (it's even in the name), but most of the time, the UI simply needs to trigger single events in the BLoC. That's why sometimes you see some Stream<void>s or similarly hacky solutions¹, just to adhere to the strictly BLoC-y way of doing things.

Also, if we would push new routes based on the stream from the BLoC, the BLoC would basically control the UI flow – but having code that directly controls both the UI and the business logic is the exact thing we tried to prevent!

That's why some developers (including me) just break with the entirely stream-based solution and adopt a custom way of triggering events in the BLoC from the UI.
Personally, I simply use method calls (that usually return Futures) to trigger the BLoC's events:

+----+   method calls    +------+
| UI | ----------------> | BLoC |
| | <---------------- | |
+----+ Stream, Future +------+

Here, the BLoC returns Streams for data that is "live" and Futures as answers to method calls.

Let's see how that could work out for your example:

  • The BLoC could provide a Stream<bool> of whether the user is signed in, or even a Stream<Account>, where Account contains the user's account information.
  • The BLoC could also provide an asynchronous Future<void> signIn(String username, String password) method that returns nothing if the login was successful or throws an error otherwise.
  • The UI could handle the input management on its own and trigger something like the following once the login button is pressed:
try {
setState(() => _isLoading = true); // This could display a loading spinner of sorts.
await Bloc.of(context).signIn(_usernameController.text, _passwordController.text);
Navigator.of(context).pushReplacement(...); // Push logged in screen.
} catch (e) {
setState(() => _isLoading = false);
// TODO: Display the error on the screen.
}

This way, you get a nice separation of concerns:

  • The BLoC really just does what it's supposed to do – handle the business logic (in this case, signing the user in).
  • The UI just cares about two things:

    • Displaying user data from Streams and
    • reacting to user actions by triggering them in the BLoC and performing UI actions based on the result.²

Finally, I want to point out that this is only one possible solution that evolved over time by trying different ways of handling state in a complex app.
It's important to get to know different points of view on how state management could work so I encourage you to dig deeper into that topic, perhaps by watching the "Pragmatic State Management in Flutter" session from Google I/O.

EDIT: Just found this architecture in Brian Egan's architecture samples, where it's called "Simple BLoC". If you want to get to know different architectures, I really recommend having a look at the repo.


¹ It gets even uglier when trying to provide multiple arguments to a BLoC action – because then you'd need to define a wrapper class just to pass that to the Stream.

² I do admit it gets a little bit ugly when starting the app: You'll need some sort of splash screen that just checks the BLoC's stream and redirects the user to the appropriate screen based on whether they signed in or not. That exception to the rule occurs because the user performed an action – starting the app – but the Flutter framework doesn't directly allow us to hook into that (at least not elegantly, as far as I know).

How to use bloc pattern between two screens

Every time you call BlocProvider, you create new instance of bloc class.

To work with the same bloc instance, you need to create it once, and put it before routing after that you will have access to it via context.

import 'package:bloc/bloc.dart';

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

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BlocSample>(
create: (BuildContext context) => BlocSample(),
),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: FirstScreen(),
),
);
}
}

class BlocSample extends Bloc<EventSample, String> {
@override
String get initialState => 'initial state';

@override
Stream<String> mapEventToState(
EventSample event,
) async* {
yield 'changed state';
}
}

class EventSample {}

class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<BlocSample, String>(
builder: (context, state) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(state),
RaisedButton(
child: Text('change status'),
onPressed: () => BlocProvider.of<BlocSample>(context).add(
EventSample(),
),
),
RaisedButton(
child: Text('change route'),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SecondScreen(),
),
),
)
],
),
),
);
},
);
}
}

class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<BlocSample, String>(
builder: (context, state) {
return Scaffold(
body: Center(child: Text(state)),
);
},
);
}
}

Sample Image



Related Topics



Leave a reply



Submit