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 Future
s) to trigger the BLoC's events:
+----+ method calls +------+
| UI | ----------------> | BLoC |
| | <---------------- | |
+----+ Stream, Future +------+
Here, the BLoC returns Stream
s for data that is "live" and Future
s 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 aStream<Account>
, whereAccount
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
Stream
s and - reacting to user actions by triggering them in the BLoC and performing UI actions based on the result.²
- Displaying user data from
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)),
);
},
);
}
}
Related Topics
iOS 9 Orientation Auto-Rotation Animation Not Working, But Always on Main Thread
Uibutton Does Not Work When It in Uiscrollview
Why Is Our Monotouch App Breaking in the Garbage Collector? It Is Not Out of Memory
Https iOS with Self Signed Certificate
1St April Dates of 80S Failed to Parse in iOS 10.0
How to Use Pull to Refresh in Swift
How to Programmatically Find Swift's Version
How to Fix Failed to Fetch Default Token Error
Facebook Sdk 3.1 for iOS - Runs on iOS6, But Crashes on iOS 5.X
Working with Live Photos in Playground
Dynamic Height for Static Table Cells with Wrapping Labels
Swiftui: Navigationdestinationlink Deprecated
Swiftui Withanimation Completion Callback
Method Load() Defines Objective-C Class Method 'Load', Which Is Not Permitted by Swift 1.2