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 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
Stream
s 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).