felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.79k stars 3.39k forks source link

Navigation in flutter_bloc #201

Closed felangel closed 5 years ago

felangel commented 5 years ago

Is your feature request related to a problem? Please describe. It's difficult to have navigation or other "side-effects" using flutter_bloc in response to a state change. Developers need to do the navigation and dispatch another Event to reset the state in order to avoid an infinite loop of pushing routes onto the navigation stack.

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) {    
            SchedulerBinding.instance.addPostFrameCallback((_) {
                Navigator.of(context).pushNamed('/details');
            });
            BlocProvider.of<DataBloc>(context).dispatch(ResetState());        
            return Text('Success');
        }
        if (state is Failure) {
            return Text('Failure');
        }
    },
)

Describe the solution you'd like

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');
            }
        },
    }
)
ThinkDigitalSoftware commented 5 years ago

What's the resetState for again? It's looking very Reduxy.

felangel commented 5 years ago

@ThinkDigitalRepair the resetState was just to show what you'd currently have to do in order to navigate in response to a bloc state. If we don't resetState then build can be called over and over and our state will always be success so we'll keep pushing new pages onto the navigation stack. Hope that makes sense 👍

To be clear, I don't want developers to have to reset state. The goal of this enhancement is to make it easy/natural to navigate in response to bloc state changes.

ThinkDigitalSoftware commented 5 years ago

Unless you store the route as the state that’s called and then have something watching that variable, similar to how you would do it with a tabview or pageView.

On Apr 6, 2019, at 11:14 AM, Felix Angelov notifications@github.com wrote:

@ThinkDigitalRepair https://github.com/ThinkDigitalRepair the resetState was just to show what you'd currently have to do in order to navigate in response to a bloc state. If we don't resetState then build can be called over and over and our state will always be success so we'll keep pushing new pages onto the navigation stack. Hope that makes sense 👍

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/felangel/bloc/issues/201#issuecomment-480525728, or mute the thread https://github.com/notifications/unsubscribe-auth/AV-HfT4Fj_wXIIM4zhihHB_JE_YyPqrGks5veOQegaJpZM4cgUp3.

felangel commented 5 years ago

Yes, but then that's coupling the bloc to flutter. I don't think bloc states should not be coupled with navigation or any sort of presentation because then they are no longer reusable between mobile and web.

ThinkDigitalSoftware commented 5 years ago

I didn't mean as a part of the library, I meant as a part of the state of the bloc

felangel commented 5 years ago

Yeah I get what you mean but I think that’s still bad practice because that means your bloc knows about navigation which ideally shouldn’t be the case.

ThinkDigitalSoftware commented 5 years ago

It would simply be a variable, and then have something outside of the bloc subscribed to the bloc to control the navigation. This is all hypothetical speculation. It can listen and if the variable changes, call the function that pushes the route

abinvp commented 5 years ago

The proposed solution looks great and I like the idea of having it as part of the bloc builder. Having it in the presentation layer makes it easy to understand how the app is going to react to the state changes.

mayurdhurpate commented 5 years ago

I faced some issues while developing a Login Flow with Flutter Bloc. The example provided in the docs of Login changes the Widget inside MaterialApp instead of pushing new Scaffold.

I face these issues when I don't wish to rebuild any Widget on State change but want to do some Navigator push (or computation/network call) on state change. Putting a BlocBuilder widget seems a bit of an overkill for these scenarios (+ it makes the already complex Widget Tree even more complex), and I end up defining a custom Listener to the Bloc.

void getStream(AuthenticationState state, BuildContext context) async {
    if (state.toString() == 'AuthenticationAuthenticated') {
      print("Navigate to Main Page");
      await Navigator.pushReplacement<ChatScreen,LoginView>(
        context,
        MaterialPageRoute<ChatScreen>(builder: (context) => ChatScreen()),
      );
    } else if (state.toString() == 'AuthenticationSignup') {
      print("Navigate to Signup Page");
      await Navigator.pushReplacement<Widget,Widget>(
        context,
        MaterialPageRoute<SignupForm>(builder: (context) => SignupForm()),
      );
    }
    else if(state.toString() == 'AuthenticationLoading'){
      Scaffold.of(context).showSnackBar(
        SnackBar(content: Text('Signing you in..')));
    }
  }

Setting up the state via:

final AuthenticationBloc authenticationBloc =
        BlocProvider.of<AuthenticationBloc>(context);

    x = authenticationBloc.state.listen((state) {
      getStream(state, context);
    });

I don't think I'm following the best practices above, and the above approach involves a lot of hassles in terms of listener disposal on Widget close, multiple listeners being active of different routes even after routes are inactive and context being null because the parent Widget got unmounted. Please guide if there are any better approaches to the above situations.

felangel commented 5 years ago

@abinvp thoughts on having a separate BlocListener widget? I'm a bit worried that having it all bundled into BlocBuilder will cause confusion and clutter the BlocBuilder API.

felangel commented 5 years ago

@mayurdhurpate I'm hoping that BlocListener will help in those cases where you just want to do something like Navigation in response to a Bloc state change but don't necessarily want to render a new widget. I'd love to hear your thoughts 👍

trietbui85 commented 5 years ago

@felangel BlocListener seems a smart idea. But I think it's not only for navigation but also for non-UI-related actions. For example:

So, with the same state, we should distinguish them into 2 different behaviors:

felangel commented 5 years ago

@anticafe I completely agree. My intention is that BlocBuilder will be used for rendering widgets based on the bloc state and BlocListener will be used for the “one time actions” which include things like navigation, showing snackbars, showing toasts, showing dialogs, etc... 👍

abinvp commented 5 years ago

@felangel, that sounds good to me. My intention was to not have navigation coupled with the bloc. As long as we have an option to handle it in the presentation layer, either with BlocBuilder or BlocListener, I am good.

ThinkDigitalSoftware commented 5 years ago

You bloc builder or other function can easily listen to the stream and when it changes, if the value is different, push a new route.

On Mon, Apr 8, 2019 at 11:04 PM Abin Paul notifications@github.com wrote:

@felangel https://github.com/felangel, that sounds good to me. My intention was to not have navigation coupled with the bloc. As long as we have an option to handle it in the presentation layer, either with BlocBuilder or BlocListener, I am good.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/felangel/bloc/issues/201#issuecomment-481114520, or mute the thread https://github.com/notifications/unsubscribe-auth/AV-HfdHZ_nKvLy1fojduGjAIV99uY66gks5vfC1igaJpZM4cgUp3 .

-- Think Digital 323-638-9448 760-678-8833 Facebook.com/ThinkDigitalRepair

felangel commented 5 years ago

@thinkdigitalrepair I don’t think it’s that easy. You have to deal with the subscription and it becomes separated from the UI. I agree that it’s possible but this change is more to improve the developer experience. I’m not saying it is impossible currently, just inconvenient in my opinion.

felangel commented 5 years ago

BlocListener is now available in flutter_bloc v0.10.0 🎉

Nothing-Works commented 5 years ago

@felangel any plan that this will be added in docs ?

felangel commented 5 years ago

@Nothing-Works yup I’m working on the docs and vscode extensions snippets as we speak haha 👍

felangel commented 5 years ago

@Nothing-Works published v0.5.0 of the bloc vscode extension which has snippet support for BlocListener and added a SnackBar Recipe to the docs.

Nothing-Works commented 5 years ago

@felangel you are amazing!!!

basnetjiten commented 5 years ago

@felangel please help me here with the problem. I have a tap() method where I want to dispatch the event and at same time based on the result of dispatch event I want to navigate to the next page. How do I solve the problem

felangel commented 5 years ago

@basnetjiten check out the navigation recipe and let me know if that helps 👍

lmartinez10 commented 5 years ago

Hi,

Good Day.

Can someone help me with this problem. I have a generic bloc provider and multiple blocs, the problem is when i set on app.dart LoginBloc with bloc provider it works fine but then on the login page i have username and password and when i click onpressed event and navigate it to any page it cause me an error the bloc is being null. Code below.

app.dart

import 'package:flutter/material.dart'; import 'package:night_out/core/blocs/login_bloc.dart'; import './core/blocs/bloc_provider.dart'; import 'package:nightout/router.dart'; //import './ui/widgets/login.dart'; class App extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( child: MaterialApp( theme: new ThemeData( primaryColor: Color.fromRGBO(58, 66, 86, 1.0), fontFamily: 'Raleway'), initialRoute: 'login', onGenerateRoute: Router.generateRoute, ), onDispose: (, bloc) => bloc.dispose(), ); } }

router.dart import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:night_out/event/event.dart'; import './ui/widgets/login.dart'; import './ui/widgets/registration.dart'; import './ui/widgets/registration_profile.dart';

class Router { static Route generateRoute(RouteSettings settings) { switch (settings.name) { case '/': return MaterialPageRoute(builder: () => Login()); case 'login': return MaterialPageRoute(builder: () => Login()); case 'registration': return MaterialPageRoute(builder: () => Registration()); case 'registration-profile': return MaterialPageRoute(builder: () => RegistrationProfile()); case 'event-page': return MaterialPageRoute(builder: () => EventPage()); default: return MaterialPageRoute( builder: () => Scaffold( body: Center( child: Text('No route defined for ${settings.name}'), ), )); } } }

login.dart

import 'package:flutter/material.dart'; import '../../core/blocs/login_bloc.dart'; import '../../core/blocs/bloc_provider.dart';

class Login extends StatelessWidget { @override Widget build(BuildContext context) { final bloc = Provider.of(context);

return new Scaffold(
  appBar: appBar(context),
  body: SingleChildScrollView(
    child: new Container(
      padding: EdgeInsets.all(16.0),
      child: new Column(
          children: <Widget>[
            email(bloc),
            password(bloc),
            login(bloc),
            register(bloc)
          ],
        ),
    ),
  ),
);

}

Widget appBar(BuildContext context) { return new AppBar( title: new Text('Night out - Login'), centerTitle: true, ); }

Widget email(LoginBloc bloc) => StreamBuilder( stream: bloc.email, builder: (context, snap) { return TextFormField( keyboardType: TextInputType.emailAddress, onChanged: bloc.changeEmail, decoration: InputDecoration( labelText: 'Email address', hintText: 'you@example.com', errorText: snap.error, icon: Icon(Icons.email), ), ); }, );

Widget password(LoginBloc bloc) => StreamBuilder( stream: bloc.password, builder:(context, snap) { return TextFormField( obscureText: true, onChanged: bloc.changePassword, decoration: InputDecoration( labelText: 'Password', hintText: 'Password', errorText: snap.error, icon: Icon(Icons.lock) ), ); } );

Widget login(LoginBloc bloc) => StreamBuilder( stream: bloc.submitValid, builder: (context, snap) { return RaisedButton( // onPressed: (!snap.hasData) ? null : bloc.submit(context), onPressed: () async { // Scaffold.of(context).showSnackBar( // new SnackBar(duration: new Duration(seconds: 3), content: // new Row( // children: [ // new CircularProgressIndicator(), // new Text('signing-in...') // ], // ), // ));

      // if (snap.hasData) {
      //   var response = await bloc.submit();
      //   if (response.statusCode == 200) {
      //     print('success');
      //     //Navigator.pushNamed(context, 'registration', arguments: posts[index]);
      //     Navigator.pushNamed(context, 'registration');
      //   }
      // }
       Navigator.pushNamed(context, 'registration');
    },
    child: Text('Login', style: TextStyle(color: Colors.white)),
    color: Colors.blue,
  );
},

);

Widget register(LoginBloc bloc) => StreamBuilder( builder: (context, snap) { return FlatButton( child: new Text('Register'), onPressed: () { Navigator.pushNamed(context, 'event-page'); }, ); }, ); }

felangel commented 5 years ago

Hi @lmartinez10 👋 I'm sorry but this repository is reserved for questions regarding the bloc library.

lmartinez10 commented 5 years ago

yup, i know. but can someone help me please with regards to my problem. the problem is when navagation with bloc and bloc provider

felangel commented 5 years ago

@lmartinez10 I would recommend posting on stack overflow if you have questions that aren't specific to this library. It'd be hard to help since you're using a custom bloc and bloc provider implementation.

ishuacharis commented 5 years ago

this works fine

void main(){ Repository repository = Repository(); runApp( BlocProvider( builder: (context) { return AuthenticationBloc(repository: repository) ..dispatch(AppStarted()); }, child: MyApp(repository: repository), )); }

this works fine too class MyApp extends StatelessWidget { final Repository repository;

MyApp({this.repository}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.pink, ), onGenerateRoute: RouteGenerator.genarateRoute, initialRoute: homeRoute, ); } } this also works fine Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home'), backgroundColor: primaryColor, ), body: Center( child: Text("wale"), ), floatingActionButton: FloatingActionButton( backgroundColor: primaryColor, onPressed: () { Navigator.of(context).pushNamed(signUpRoute, arguments: repo); }, child: Icon(Icons.arrow_forward_ios), ), ); }

this is where the error is thrown when i tap _onSignUpFormButtonPressed to naviagte i get BlocProvider.of() called with a context that does not contain a Bloc of type SignUpBloc Widget build(BuildContext context) { final _navBloc = BlocProvider.of(context);

_onSignUpFormButtonPressed(){
   _navBloc.dispatch(EventB());
}

return BlocListener( bloc: _navBloc, listener: (BuildContext context, SignUpState state) { if (state is StateB) { Navigator.of(context).pushNamed(signUpFormRoute); } }, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: _onSignUpFormButtonPressed , child: Container( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.featured_video), Text("Sign up with Email", style: TextStyle( color: Colors.pink, fontWeight: FontWeight.bold, fontSize: 18.0), ) ], ), decoration: BoxDecoration( color: Colors.white, border: Border.all( width: 2.0, color: Colors.pink ), borderRadius: BorderRadius.all( Radius.circular(50.0) ) ), ), ), ], ), ) ); }

ishuacharis commented 5 years ago

this works fine

void main(){ Repository repository = Repository(); runApp( BlocProvider( builder: (context) { return AuthenticationBloc(repository: repository) ..dispatch(AppStarted()); }, child: MyApp(repository: repository), )); }

this works fine too class MyApp extends StatelessWidget { final Repository repository;

MyApp({this.repository}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.pink, ), onGenerateRoute: RouteGenerator.genarateRoute, initialRoute: homeRoute, ); } } this also works fine Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home'), backgroundColor: primaryColor, ), body: Center( child: Text("wale"), ), floatingActionButton: FloatingActionButton( backgroundColor: primaryColor, onPressed: () { Navigator.of(context).pushNamed(signUpRoute, arguments: repo); }, child: Icon(Icons.arrow_forward_ios), ), ); }

this is where the error is thrown when i tap _onSignUpFormButtonPressed to naviagte i get BlocProvider.of() called with a context that does not contain a Bloc of type SignUpBloc Widget build(BuildContext context) { final _navBloc = BlocProvider.of(context);

_onSignUpFormButtonPressed(){
   _navBloc.dispatch(EventB());
}

return BlocListener( bloc: _navBloc, listener: (BuildContext context, SignUpState state) { if (state is StateB) { Navigator.of(context).pushNamed(signUpFormRoute); } }, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: _onSignUpFormButtonPressed , child: Container( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.featured_video), Text("Sign up with Email", style: TextStyle( color: Colors.pink, fontWeight: FontWeight.bold, fontSize: 18.0), ) ], ), decoration: BoxDecoration( color: Colors.white, border: Border.all( width: 2.0, color: Colors.pink ), borderRadius: BorderRadius.all( Radius.circular(50.0) ) ), ), ), ], ), ) ); }

i really need help on this bloc pattern has been a very good architecture just having trouble using it with naviagtion i read the Naviagtion on the flutter bloc website still got the same error

hardikdabhi2 commented 5 years ago

Does anyone give me the answer to this question?

https://stackoverflow.com/questions/57826865/data-not-being-updated-after-change-the-placeid-in-flutter-bloc/57830486#57830486

felangel commented 5 years ago

@hardikdabhi2 looks like it was already answered 👍

hardikdabhi2 commented 5 years ago

The answer which was answered is correct for data parsing but the circular progress bar will not showing at the moment.

On Fri, Sep 13, 2019 at 7:27 AM Felix Angelov notifications@github.com wrote:

@hardikdabhi2 https://github.com/hardikdabhi2 looks like it was already answered 👍

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/felangel/bloc/issues/201?email_source=notifications&email_token=AFQ35IAIMITVQOZPVABMJ4TQJLXQTA5CNFSM4HEBJJ32YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6TX3HA#issuecomment-531070364, or mute the thread https://github.com/notifications/unsubscribe-auth/AFQ35IDFGUXXTJUSBSACBBLQJLXQTANCNFSM4HEBJJ3Q .