Bloc, Flutter and Navigation
Asked Answered
P

7

76

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

Phillie answered 9/1, 2019 at 16:10 Comment(6)
See #54102089Wheelchair
Thanks Remi, i'm unable to comment on that post but i've been wrapping my head around this - so we have to use stateful widgets? I always thought bloc moved away from stateful widgets. Then the bloc still determines when to navigate by passing values via the Stream. Seems a bit clunky, need to work with it a bit more! Thank you!Phillie
BLoC by no means remove the need for StatefulWidgetWheelchair
Ideally you should be able to listen for the BLoC.stream in the initState. But I face with this approach another problem: The recommended way to supply a BLoC is using a Provider based on Inherited Widget using a call to inheritFromWidgetOfExactType. The problem here is that you cannot call inheritFromWidgetOfExactType in initState, only in didChangeDependencies. But didChangeDependencies is called by the framework on various instances including when you navigate out form a page. So you can enter in a loop: navigation triggers change in dependencies and vice versa.Hevesy
See this question for an exampleFontana
See https://mcmap.net/q/270245/-navigating-to-a-new-screen-when-stream-value-in-bloc-changesCarrick
C
52

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

Cradling answered 9/5, 2019 at 22:52 Comment(2)
Your BL is split into both Bloc and Presentation (which is bad behavior). In your example, you are using setState to handle loading in your presentation.Stromboli
How would you comment on the solution of https://mcmap.net/q/267931/-bloc-flutter-and-navigation ? Isn't it more structured by wrapping the logic (state handling) inside a bloc?Treatise
M
20

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();
                  }),
                );
              }));
Molest answered 27/11, 2019 at 15:31 Comment(4)
links is now goneShibboleth
Thanks @C.Skjerdal I've fixed it now.Molest
This example as one major flow. It might happen that by the time, onSuccess is called, widget is already unmounted leaving context = null.Deakin
this is a nice solution. I can route to another page from the bloc through the callbackSeverally
B
19

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');
            }
        },
    }
)
Bengaline answered 16/11, 2020 at 7:20 Comment(4)
What if I go back and press again the button to go to details but the bloc throw the same state "Success" (just hypothetical), the listener doesn't trigger any pushNamed because is the same state as before, how I can deal with that?Eyebright
@Rodrigoporras In that case, you will have to reset the state on each navigation (even when you go back).Stromboli
Although that approach is the most "Blocy", it complicates a very simple functionality of navigation. IMO, navigation is an exceptional functionality that could be handled from the Bloc itself. Which makes the code much cleaner and simple to understand.Stromboli
@Rodrigoporras the best way to handle this is final signUpState = state as _SignUpStateIdle; emit(const SignUpState.success()); emit(signUpState); // reset to previous idle state, it should contain states for fields as wellTheressathereto
S
6

First of all: if no business logic then no needs to go to YourBloc class.

But from time to time some user's activity required to perform some logic in Bloc class and then Bloc class has to decide what to do next: just rebuild widgets or show dialog or even navigate to next route. In such case you have to send some State to UI to finish the action. Then another problem appear: what shall I do with widgets when Bloc send State to show toast?

And this is the main issue with all of this story.

A lot of answers and articles recommend to use flutter_block. This library has BlocBuilder and BlocListener. With those classes you can solve some issues but not 100% of them.

In my case I used BlocConsumer which manage BlocBuilder and BlocListener and provide brilliant way to manage states.

From the documentation:

BlocConsumer<BlocA, BlocAState>(
  listenWhen: (previous, current) {
    // return true/false to determine whether or not
    // to invoke listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  buildWhen: (previous, current) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

As you can see with BlocConsumer you can filter states: you can easily define states to rebuild widgets and states to show some popups or navigate to next screen.

Soldo answered 28/5, 2020 at 18:19 Comment(0)
F
2

Totally agree with @Volodymyr Yatsykiv's answer in regards to the use of BlocConsumer Widget but just to add an example of the implementation managing the navigation in case of a successful log in and rebuild of widgets when there is a request on going can be helpful so i added below.

class LogInBtn extends StatelessWidget {
  const LogInBtn({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<LogInBloc, LogInStates>(
        listener: (context, state){
          if(state is SuccessLogInState){
            Navigator.of(context).push(MaterialPageRoute(
                builder: (context) => const HomeScreen()));
          }
        },
        builder: (context, state) {
      return state is ProgressLogInState
          ? const Padding(
        padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 100.0),
        child: LinearProgressIndicator(),
      )
          : Padding(
        padding: const EdgeInsets.only(top: 100.0),
        child: Container(
          height: 40,
          decoration: const BoxDecoration(
              color: Color(whiteColor),
              borderRadius: BorderRadius.all(Radius.circular(5.0))),
          child: const Center(
            child: Text('Log In',
                style: TextStyle(
                    color: Color(primaryColor),
                    fontWeight: FontWeight.bold,
                    fontSize: 20,
                    fontFamily: 'Laila')),
          ),
        ),
      );
    });
  }

}

Facient answered 23/8, 2022 at 10:13 Comment(0)
E
1

Complete Flutter BLOC example

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

void main() {
  BlocOverrides.runZoned(
    () => runApp(const App()),
    blocObserver: AppBlocObserver(),
  );
}

/// Custom [BlocObserver] that observes all bloc and cubit state changes.
class AppBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    if (bloc is Cubit) print(change);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }
}

/// {@template app}
/// A [StatelessWidget] that:
/// * uses [bloc](https://pub.dev/packages/bloc) and
/// [flutter_bloc](https://pub.dev/packages/flutter_bloc)
/// to manage the state of a counter and the app theme.
/// {@endtemplate}
class App extends StatelessWidget {
  /// {@macro app}
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ThemeCubit(),
      child: const AppView(),
    );
  }
}

/// {@template app_view}
/// A [StatelessWidget] that:
/// * reacts to state changes in the [ThemeCubit]
/// and updates the theme of the [MaterialApp].
/// * renders the [CounterPage].
/// {@endtemplate}
class AppView extends StatelessWidget {
  /// {@macro app_view}
  const AppView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ThemeCubit, ThemeData>(
      builder: (_, theme) {
        return MaterialApp(
          theme: theme,
          home: const CounterPage(),
        );
      },
    );
  }
}

/// {@template counter_page}
/// A [StatelessWidget] that:
/// * provides a [CounterBloc] to the [CounterView].
/// {@endtemplate}
class CounterPage extends StatelessWidget {
  /// {@macro counter_page}
  const CounterPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: const CounterView(),
    );
  }
}

/// {@template counter_view}
/// A [StatelessWidget] that:
/// * demonstrates how to consume and interact with a [CounterBloc].
/// {@endtemplate}
class CounterView extends StatelessWidget {
  /// {@macro counter_view}
  const CounterView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('$count', style: Theme.of(context).textTheme.headline1);
          },
        ),
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            child: const Icon(Icons.add),
            onPressed: () => context.read<CounterBloc>().add(Increment()),
          ),
          const SizedBox(height: 4),
          FloatingActionButton(
            child: const Icon(Icons.remove),
            onPressed: () => context.read<CounterBloc>().add(Decrement()),
          ),
          const SizedBox(height: 4),
          FloatingActionButton(
            child: const Icon(Icons.brightness_6),
            onPressed: () => context.read<ThemeCubit>().toggleTheme(),
          ),
        ],
      ),
    );
  }
}

/// Event being processed by [CounterBloc].
abstract class CounterEvent {}

/// Notifies bloc to increment state.
class Increment extends CounterEvent {}

/// Notifies bloc to decrement state.
class Decrement extends CounterEvent {}

/// {@template counter_bloc}
/// A simple [Bloc] that manages an `int` as its state.
/// {@endtemplate}
class CounterBloc extends Bloc<CounterEvent, int> {
  /// {@macro counter_bloc}
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

/// {@template brightness_cubit}
/// A simple [Cubit] that manages the [ThemeData] as its state.
/// {@endtemplate}
class ThemeCubit extends Cubit<ThemeData> {
  /// {@macro brightness_cubit}
  ThemeCubit() : super(_lightTheme);

  static final _lightTheme = ThemeData(
    floatingActionButtonTheme: const FloatingActionButtonThemeData(
      foregroundColor: Colors.white,
    ),
    brightness: Brightness.light,
  );

  static final _darkTheme = ThemeData(
    floatingActionButtonTheme: const FloatingActionButtonThemeData(
      foregroundColor: Colors.black,
    ),
    brightness: Brightness.dark,
  );

  /// Toggles the current brightness between light and dark.
  void toggleTheme() {
    emit(state.brightness == Brightness.dark ? _lightTheme : _darkTheme);
  }
}
Epergne answered 13/8, 2022 at 8:2 Comment(0)
R
0

Navigating with Bloc in Flutter can indeed seem a bit tricky at first, especially if you're trying to adhere strictly to architectural principles. In Bloc architecture, the idea is to keep the Bloc focused on business logic and state management, while delegating UI-related actions, such as navigation, to the widgets or the UI layer.

  1. Bloc Setup for Login
  2. Triggering Login
  3. Handling Navigation
  4. Avoiding Navigation Inside Bloc
  5. Avoid Navigation in StreamBuilder
  6. initState and Stream Subscriptions
  7. Further Exploration
Renown answered 23/4 at 6:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.