Bloc: Event dispatched hit the Bloc once and no more

Created on 22 Mar 2019  路  27Comments  路  Source: felangel/bloc

Hi there, first of all let me thank you for the awesome libraby.
Im facing an issue with my bloc.The issue is that when the app loads i am able to dispatch an event to the Bloc once,and subsequent calls to dispatch just do nothing.
the app uses the hidden_drawer_menu plugin which i forked and tweaked a bit to allow navigation.It is the same principle as the hidden drawer menu from the fluttery challenges.
I dispatch events to the bloc to notify other parts of the app when there is a screen change thus i can change the drawer menu icon to an arrow_back.

here is the code.

NavigationBloc.dart

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import './events/navigation_events.dart';
import './states/navigation_state.dart';
import 'dart:async';

class NavigationBloc extends Bloc<NavigationEvents,NavigationState>{
@override
  // TODO: implement initialState
  NavigationState get initialState => HomeRouteState();
  @override
  Stream<NavigationState> mapEventToState(NavigationState currentState, NavigationEvents event) async*{
    debugPrint("out event:$event");
    if(event is GoHomeEvent){
      debugPrint('event:$event');
      yield HomeRouteState();
    }else if(event is GoLoginEvent){
      debugPrint('event:$event');
      yield LoginRouteState();
    }
  }
}

NavigationEvents.dart

import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

abstract class NavigationEvents extends Equatable{
  NavigationEvents([List props=const[]]):super(props);
}

class GoHomeEvent extends NavigationEvents{
  @override
  String toString() => 'Go home';
}

class GoLoginEvent extends NavigationEvents{
  @override
  String toString()=>'Go Login Chap Chap';
}

//class

NavigationState.dart

import 'package:equatable/equatable.dart';

 abstract class NavigationState extends Equatable{}

 class HomeRouteState extends NavigationState{
   @override
  String toString() {
    // TODO: implement toString
    return 'home';
  }
 }
  class LoginRouteState extends NavigationState{
   @override
  String toString() {
    // TODO: implement toString
    return 'loginchapchap';
  }
 }

main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import './src/app.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class SimpleBlocDelegate extends BlocDelegate{
  @override
  void onTransition(Transition transition) {
    print(transition);
    super.onTransition(transition);
  }
}
void main(){
  BlocSupervisor().delegate=SimpleBlocDelegate();
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_){
    runApp(App());
  });
}

ScreenNavigator.dart the folowing class has a static method which accept events and the navigation bloc to switch screens

import 'package:flutter/material.dart';
import 'package:hidden_drawer_menu/simple_hidden_drawer/provider/simple_hidden_drawer_provider.dart';
import 'package:hidden_drawer_menu/menu/item_hidden_menu.dart';
import 'package:hidden_drawer_menu/hidden_drawer/screen_hidden_drawer.dart';
import 'package:nsiaviemobile/src/blocs/events/navigation_events.dart';
import 'package:nsiaviemobile/src/blocs/navigation_bloc.dart';
import 'package:nsiaviemobile/src/screens/chapchap_loginScreen.dart';
import 'package:nsiaviemobile/src/screens/homeScreen.dart';
import 'package:nsiaviemobile/src/utils/app_assets.dart';

class ScreenNavigator {
  NavigationBloc bloc=NavigationBloc();
  static List<ScreenHiddenDrawer> drawerItems = [
    ScreenHiddenDrawer(
        ItemHiddenMenu(
          name: "Accueil",
          colorTextSelected: Colors.white,
          colorLineSelected: NsiaAssets.jaune,
        ),
        HomeScreen()),
    ScreenHiddenDrawer(
        ItemHiddenMenu(
          name: "Nsia Chap Chap",
          colorTextSelected: Colors.white,
          colorLineSelected: NsiaAssets.jaune,
        ),
        LoginScreen()),
  ];
  static void goTo(BuildContext context, String route, NavigationBloc bloc,
      NavigationEvents event) {
    switch (route) {
      case "home":
        bloc.dispatch(event);
        SimpleHiddenDrawerProvider.of(context).setScreenByIndex(0, false);
        debugPrint('$event dispatched $bloc');
        break;
      case "login":
        bloc.dispatch(event);
        SimpleHiddenDrawerProvider.of(context).setScreenByIndex(1, false);
        debugPrint('$event dispatched $bloc');
        break;
    }
  }
}

main_drawer.dart is the hidden_drawer itself and is the root widget of the app,i use the bloc here to change the menu icon to a back button as switching away from the root screen which is homeScreen.dart suppose you are on a sub page

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hidden_drawer_menu/hidden_drawer/hidden_drawer_menu.dart';
import 'package:nsiaviemobile/src/blocs/events/navigation_events.dart';
import 'package:nsiaviemobile/src/blocs/navigation_bloc.dart';
import 'package:nsiaviemobile/src/blocs/states/navigation_state.dart';
import 'package:nsiaviemobile/src/utils/screen_navigator.dart';
import '../screens/pubscreen.dart';
import '../utils/app_assets.dart';

class RootDrawer extends StatefulWidget {
  final Widget child;

  RootDrawer({Key key, this.child}) : super(key: key);

  _RootDrawerState createState() => _RootDrawerState();
}

class _RootDrawerState extends State<RootDrawer>
    with SingleTickerProviderStateMixin {
  NavigationBloc routeBloc;
  Animation<double> drawerIconAnimation;
  AnimationController drawerCtrl;
  void initState() {
    drawerCtrl =
        AnimationController(duration: Duration(milliseconds: 350), vsync: this);
    drawerIconAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
        CurvedAnimation(parent: drawerCtrl, curve: Curves.fastOutSlowIn));
    super.initState();
  }

  Widget _buildMenuIcon(bool isRoot,BuildContext context,NavigationBloc bloc,String screen,NavigationEvents event) {
    if (isRoot) {
      debugPrint('in root');
      return AnimatedBuilder(
        animation: drawerIconAnimation,
        builder: (context, child) {
          return Stack(
            children: <Widget>[
              Transform.scale(
                scale: drawerIconAnimation.value - 1.0,
                child: Icon(
                  Icons.arrow_back,
                  color: NsiaAssets.bleu,
                ),
              ),
              Transform.scale(
                scale: drawerIconAnimation.value,
                child: Icon(
                  Icons.menu,
                  color: NsiaAssets.bleu,
                ),
              ),
            ],
          );
        },
      );
    }else{
      return AnimatedBuilder(
        animation: drawerIconAnimation,
        builder: (context, child) {
          return Stack(
            children: <Widget>[
              Transform.scale(
                scale: drawerIconAnimation.value - 1.0,
                child: Icon(
                  Icons.menu,
                  color: NsiaAssets.bleu,
                ),
              ),
              Transform.scale(
                scale: drawerIconAnimation.value,
                child: IconButton(
                  onPressed:(){
                    //override default vehavior of hidden_drawer_navigator
                    ScreenNavigator.goTo(context, screen, bloc,event);
                    debugPrint('back pressed');

                  },
                  icon: Icon(
                    Icons.arrow_back,
                    color: NsiaAssets.bleu,
                  ),
                ),
              ),
            ],
          );
        });
    }

  }

  @override
  Widget build(BuildContext context) {
    routeBloc = BlocProvider.of<NavigationBloc>(context);
    return Stack(
      children: <Widget>[
        HiddenDrawerMenu(
          transparentAppBar: true,
          initPositionSelected: 0,
          screens: ScreenNavigator.drawerItems,
          enablePerspective: true,
          whithAutoTittleName: false,
          backgroundColorMenu: NsiaAssets.bleu,
          iconMenuAppBar: BlocBuilder<NavigationEvents, NavigationState>(
              bloc: routeBloc,
              builder: (BuildContext context, NavigationState state) {
                debugPrint('state is $state');
                if (state is HomeRouteState) {
                  return _buildMenuIcon(true,context,routeBloc,"null",null);
                } else if(state is LoginRouteState){
                  return _buildMenuIcon(false,context,routeBloc,"home",GoHomeEvent());
                }
              },
              ),
          backgroundColorAppBar: Colors.transparent,
          backgroundMenu: DecorationImage(
            fit: BoxFit.cover,
            image: AssetImage(NsiaAssets.drawerBackgroundPath),
          ),
          elevationAppBar: 0.0,
        ),
        PubScreen()
      ],
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    //routeBloc.dispose();
    drawerCtrl.dispose();
    super.dispose();
  }
}

here is the HomeScreen.dart class

import 'package:flutter/material.dart';
import '../routes/routes.dart';
import '../utils/app_assets.dart';
import '../widgets/radial_menu.dart';
import '../widgets/blinking_text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/events/navigation_events.dart';
import '../blocs/navigation_bloc.dart';
import '../utils/screen_navigator.dart';

class HomeScreen extends StatefulWidget {
  final Widget child;

  HomeScreen({Key key, this.child}) : super(key: key);

  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
  Animation<double> fadeIn;
  AnimationController controller;
  NavigationBloc routeBloc;
  void initState() {

    super.initState();
    controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    );
    fadeIn = Tween(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: controller, curve: Curves.easeIn));
    controller.forward();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    routeBloc.dispose();
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    routeBloc=BlocProvider.of<NavigationBloc>(context);
    double deviceHeight = MediaQuery.of(context).size.height;
    debugPrint('hauteur du device : ${MediaQuery.of(context).size.height}');
    double bar = AppBar().preferredSize.height;
    return FadeTransition(
      opacity: fadeIn,
      child: Column(
        children: <Widget>[
          SizedBox(
            height: bar,
          ),
          Expanded(
              child: Column(
            children: <Widget>[
              Container(
                width: 300.0,
                child: NsiaAssets.logoChapChapW,
              ),
              SizedBox(height: deviceHeight <= 640.0 ? 15.0 : 30.0),
              BlinkingText(
                text:
                    "Bienvenue dans notre agence num茅rique mobile.\nAppuyez sur notre sigle pour commencer",
                textColor: NsiaAssets.bleu,
                alignment: TextAlign.center,
                fontSize: 15.0,
              ),
              SizedBox(height: deviceHeight <= 640.0 ? 100 : 140.0),
              Container(
                child: buildRadialMenu(context,routeBloc),
              ),
            ],
          )),
          Container(
            height: 25,
            child: Center(
              child: Text(
                "Tel: 22 41 98 00 | [email protected]",
                style: TextStyle(color: Color(0xFF001093)),
              ),
            ),
          )
        ],
      ),
    );
  }

  Widget buildRadialMenu(BuildContext context,NavigationBloc bloc) {
    return RadialMenu(
      animationDuration: 800,
      withRotation: false,
      typeEntranceAnimation: TypeEntranceAnimation.slideInUp,
      openIcon: Container(
        width: 30.0,
        height: 30.0,
        child: NsiaAssets.sigleW,
      ),

      menus: <RadialMenuItem>[
        RadialMenuItem(
          icon: Icon(Icons.person),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "Nsia Chap Chap",
          onPressed: () {
            ScreenNavigator.goTo(context, "login", bloc,GoLoginEvent());
          },
        ),
        RadialMenuItem(
          icon: Icon(Icons.supervisor_account),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "Contactez nous",
          onPressed: () {
            debugPrint("here");
          },
        ),
        RadialMenuItem(
          icon: Icon(Icons.settings),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "R茅glages",
          onPressed: () {
            debugPrint("here");
          },
        ),
        RadialMenuItem(
          icon: Icon(Icons.room),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "Les agences Nsia",
          onPressed: () {
            debugPrint("here");
          },
        ),
        RadialMenuItem(
          icon: Icon(Icons.shopping_basket),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "notre catalogue de produits",
          onPressed: () {
            debugPrint("here");
          },
        ),
        RadialMenuItem(
          icon: Icon(Icons.flash_on),
          backgroundColor: NsiaAssets.bleu,
          tooltip: "Restez informer avec Nsia vie news",
          onPressed: () {
            debugPrint("here");
          },
        ),
      ],
    );
  }
}

I am able as you will see in the code above to dispatch an event to the bloc and go to the loginScreen, the transition shows in the console and the menu icon is changed to a back one, unfortunately for me when going from the LoginScreen back to the HomeScreen, the event is not dispatched, the switching of screens happen as intended but the bloc isnt notified of the screen change.

cqNA8RfvPG

Also i noticed that if i navigate via the drawer i no longer have access to the bloc as the following gif shows.
deuxieme

What do i do wrong ? Am i missing something ?

question

Most helpful comment

@felangel very helpful, I'm able to solve this problem right now.

All 27 comments

@zjjt thanks the positive feedback and for opening an issue!

Would it be possible for you to share a link to the repo so I can just run the app locally? It would make it much easier to debug.

Hi @felangel , thanks for answering so quick i fell asleep during my debugging.
Here's the link to the repo.Ill have many bloc talking to each other so i might maybe need help for that later.Thanks a lot

App repo:
https://github.com/zjjt/chapchap.git

you will also need to have this package built and insert it in the pubsec.yaml. on my computer it is a local package so refer to it via relative path

hidden_drawer_menu
https://github.com/zjjt/hidden_drawer_menu.git

Hi @felangel , after tweaking the code a bit, i was able to make it work by removing bloc.dispose in my widgets...Aren't we suppose to always dispose of the bloc to avoid memory leak ? Or should i only dispose of the bloc in the last widget that makes use of it ?

@zjjt you are supposed to dispose but you need to make sure you dispose only when you no longer need the bloc. My general recommendation is to dispose the bloc in the same widget where it is created. Does that make sense?

@felangel m Yes it does make sense, so i shouldnt pass the bloc around to other widgets / methods. If i need the bloc i can use BlocProvider.of method...Got it ill close this issue then.Thanks a lot for your time

@zjjt awesome!

Yeah I'd recommend using BlocProvider when possible 馃憤
Glad I was able to help and thanks for bringing this up! 馃挴

Hi @felangel
so, i have the same problem when i dispatch an event a second time ... nothing happens.
BlocProvider.of<ReviewsBloc>(context).dispatch(AddReview());

the first time is
BlocProvider<ReviewsBloc>( builder: (context) => ReviewsBloc(httpClient:http.Client())..dispatch(FetchReviews())

thank you

@oussemaMetoui that most likely means that the bloc thinks the state you yielded the second time is the same as the bloc's current state. Are your bloc states extending Equatable? If yes, are you making sure to pass all of the class props to super?

@felangel yes
i forgot to mention that BlocProvider.of<ReviewsBloc>(context).dispatch(AddReview()); doesn't call the method mapEventToState :confused:

`@immutable
abstract class ReviewsState extends Equatable {

ReviewsState([List props = const []]) : super(props);
}`

ReviewsEvent

@immutable
abstract class ReviewsEvent extends Equatable {
  ReviewsEvent([List props = const <dynamic>[]]) : super(props);
}
//...
class AddReview extends ReviewsEvent {
  final String gymId;
  final int value;

  AddReview({@required this.gymId, @required this.value})
      : super([gymId, value]);

  @override
  String toString() => 'AddReview { gymId : $gymId, value : $value  }';
}

ReviewsState

@immutable
abstract class ReviewsState extends Equatable {
  ReviewsState([List props = const []]) : super(props);
}
//...
class ReviewAdded extends ReviewsState{
  @override
  String toString() => 'ReviewAdded';
}
class CardDetailGym extends StatelessWidget {
//...
onRatingUpdate: (rating)
    BlocProvider.of<ReviewsBloc>(context)
         .dispatch(AddReview(
             value: rating.toInt(),
             gymId: gym.sId));
},

hope so much you could help me

@oussemaMetoui can you please provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.

@felangel
so this is the repo

got same issue @felangel , dispatch event once success and the second data didn't appear.

here the code

class LeagueBloc extends Bloc<LeagueEvent, LeagueState>{

  final GenerateData generateData;
  LeagueBloc({@required this.generateData});

  @override
  LeagueState get initialState => DefaultLeague(generateData.generateLeagueMatch(League.PREMIER));

  @override
  Stream<LeagueState> mapEventToState(LeagueEvent event) {
    print("league 11 "+event.toString());
    if(event is ChangeLeague){
      return mapLeagueToState(event);
    }else{
      return mapLeagueToState(ChangeLeague(League.PREMIER));
    }
  }

  Stream<LeagueState> mapLeagueToState(ChangeLeague changeLeague) async* {
    try {
      final matches = this.generateData.generateLeagueMatch(changeLeague.league);
      yield LeagueLoaded(matches);
      print("league 22 "+matches.toString());
    } catch (e) {
     print("error "+e.toString());
    }
  }

}

@immutable
class LeagueEvent extends Equatable{
  LeagueEvent([List props = const []]) : super(props);
}

class ChangeLeague extends LeagueEvent{
  final League league;
  ChangeLeague(this.league) : super([league]);
}


@immutable
class LeagueState extends Equatable{
  LeagueState([List props = const <dynamic>[]]) : super(props);
}

class LeagueLoaded extends LeagueState{
  final List<Match> listMatch;
  LeagueLoaded([this.listMatch = const[]]) : super(listMatch);

  @override
  String toString() => "League Loaded $listMatch.toString()";
}

class DefaultLeague extends LeagueState{
  final League league = League.PREMIER;
  final List<Match> listMatch;
  DefaultLeague([this.listMatch = const[]]) : super(listMatch);

}


The View

 BlocBuilder<LeagueBloc, LeagueState>(
         if(state is LeagueLoaded){
            print("league 33 "+state.toString());
          }

        bloc: leagueBloc,
        builder: (context, state){
        child: ListView.builder(
                          shrinkWrap: false,
                          itemBuilder: (context, pos){

                             final match = state.props[pos];
                            return ItemMatch(match: match);
                          },
                          itemCount: state.props.length,
                        )
           }
}

Step: Click and 1st dispatch changeLeague data has success changed. And than 2nd dispatcher event league 33 text didn't triggered

@oussemaMetoui the issue with your repo is that you have:

yield ReviewAdded();

so after the state is ReviewAdded() if you yield ReviewAdded() again the bloc will ignore the state because currentState == ReviewAdded() will evaluate to true.

In order to resolve this, you either need to make sure to uniquely identify the ReviewAdded states by passing a Review to the state like:

class ReviewAdded extends ReviewsState {
  final Review review;

  ReviewAdded(this.review) : super([review]);

  @override
  String toString() => 'ReviewAdded';
}

@ariefannur I'm guessing the reason why you're having this issue is because either Match or League do not extend Equatable.

still get same error @felangel here my model

class Match extends Equatable{
  final Team teamA;
  final Team teamB;
  final String stadium;
  final String date;
  final String time;
  final League league;

  Match({this.teamA, this.teamB, this.stadium, this.date, this.time, this.league}) : super([teamA, teamB, stadium, date, time, league]);

  @override
  String toString() {
    return "$teamA.toString() , $teamB.toString(), $stadium, $date, $time, $league.toString()";
  }

  @override
  List<Object> get props {
    return [teamA, teamB, stadium, date, time, league];
  }

}


enum League {
  PREMIER,
  LALIGA,
  SERIEA,
  CHAMPIONS,
  EROPA_LEAGUE
}

@ariefannur does team also extend Equatable ?

solve it's my mistake, forgot call super Equatable in model. Thanks @felangel @bigword12

@ariefannur I'm facing same problem, can you share your fixed models?

@netfirms usually you aren't passing the props to the super class if you're extending Equatable or in mapEventToState when you yield you are yielding a modified version of a previous state instead of creating a new instance. Hope that helps 馃憤

@felangel very helpful, I'm able to solve this problem right now.

I'm facing the same issue. The state isn't received the second time it's dispatched. This is my State

class RestartedAppState extends ConfigState{
  RestartedAppState():super([]);
}

and ConfigState extends Equatable

@immutable
abstract class ConfigState extends Equatable {
  ConfigState([List props = const <dynamic>[]]) : super(props);
}

Not sure what I might be doing wrong? @felangel Can you help?

@adityadroid : check your bloc class is implemented singleton or not

I use template that auto create singleton then it causes error

@adityadroid : check your bloc class is implemented singleton or not

I use template that auto create singleton then it causes error

No. Its not a singleton. I create one instance in main.dart and use BlocProvider to inject it wherever I Need it.

can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.

can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.

This is the app. The bloc is defined in lib/blocs/config. I'm trying to read the state in main.dart

can you provide a link to a sample app which illustrates the issue you're having? It would be much easier for me to help if I can run the code locally.

@nguyenhuutinh Here's a minimal reproduction of the issue
Steps to reproduce:

  1. Login -> Settings -> Logout
  2. You'll be sent back to login page. Now do Login->Settings->Logout again. This time the state won't be received.

@adityadroid the issue is you're extending Equatable for your ConfigState and after you logout the first time, the bloc's state is RestartedAppState. Then the second time you press logout internally bloc will do a check to see if the yielded state is different than the currentState. In your case the new state is RestartedAppState() and the currentState is RestartedAppState() so
nextState == currentState would evaluate to true and bloc would ignore the transition.

If you want the bloc to keep emitting the same state over and over then you should not extend Equatable like:

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

@immutable
abstract class ConfigState {}

class ConfigChangeState extends ConfigState {
  final String key;
  final bool value;
  ConfigChangeState(this.key, this.value);
}

class UnConfigState extends ConfigState {}

class UpdatingProfilePictureState extends ConfigState {}

class ProfilePictureChangedState extends ConfigState {
  final String profilePictureUrl;
  ProfilePictureChangedState(this.profilePictureUrl);

  @override
  String toString() =>
      'ProfilePictureChangedState {profilePictureUrl: $profilePictureUrl}';
}

class RestartedAppState extends ConfigState {
  RestartedAppState();
}

The real problem here though is you are changing the state when you logout but you are not changing the state when you login. If you also dispatched an event on login you wouldn't have this problem.

Hope that helps 馃憤

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Reidond picture Reidond  路  3Comments

nhwilly picture nhwilly  路  3Comments

ricktotec picture ricktotec  路  3Comments

clicksocial picture clicksocial  路  3Comments

frankrod picture frankrod  路  3Comments