Bloc, Flutter and Navigation

So like most, i'm new to Bloc and flutter and dart and wrapping my head around. I've googled, looked through the posts here but haven't found really any answers.

So this is about navigation with bloc and flutter. Take the example of a Login. So there is a login page with a bloc behind it and at some point someone presses a button to login.

So we can call a function in the bloc that does the validation. I think this is against the strict approach but i see people doing this. But then if login is successful how do you navigate to the next screen? You're not supposed to navigate in a bloc?

But if that login page is using a StreamBuilder to change state then you cannot add a navigate in a builder either can you? You can't return navigation, you return widgets.

The initstate is somewhere you could navigate, but can you have a stream builder in an initstate that listens for state changes in the bloc?

It's all a little confusing right now but i'm persevering as this is supposed to be the way forward...

thanks Paul


Solution 1:

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

Solution 2:

The BlocListener is the widget you probably need. If the state changes to (for example) LoginSuccess, the block listener can then call the usual Navigate.of(context). You can find an example of BlocListener in action near the bottom of this page.

Another option is to pass a callback into the event.

 BlocProvider.of<MyBloc>(context).add(MyEvent(
              data: data,
              onSuccess: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) {
                    return HomePage();
                  }),
                );
              }));

Solution 3:

As mentioned by felangel in Github in the following issue, we can use BlocListner for this purpose.

BlocListener(
    bloc: BlocProvider.of<DataBloc>(context),
    listener: (BuildContext context, DataState state) {
        if (state is Success) {              
            Navigator.of(context).pushNamed('/details');
        }              
    },
    child: BlocBuilder(
        bloc: BlocProvider.of<DataBloc>(context),
        builder: (BuildContext context, DataState state) {        
            if (state is Initial) {
                return Text('Press the Button');
            }
            if (state is Loading) {
                return CircularProgressIndicator();
            }  
            if (state is Success) {
                return Text('Success');
            }  
            if (state is Failure) {
                return Text('Failure');
            }
        },
    }
)