felangel / bloc

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

[Help] BlocListener not reacting to state change ? #363

Closed DamienMrtl closed 5 years ago

DamienMrtl commented 5 years ago

Hi, I'm having an issue where my BlocListener is not triggering, I don't know why. Maybe I did something wrong when yielding the state or I am passing a wrong instance of my bloc to the listener, I can't find why it doesn't work. Basically when I put a breakpoint in the listener callback it doesn't get executed on state change.

Context : I'm creating a chatbot application in Flutter so I made a bloc to store the list of chat messages. From the UI I have a simulate function witch adds messages in my bloc using a timer. And again in the UI I have a AnimatedListView whitch display the messages.

Here are the 2 files concerned by the problem: (this is my first fluter app so pardon the messy code)

BLoC (dialog_bloc.dart):

import 'package:Djebots/ui/widgets/chat_message.dart';
import 'package:bloc/bloc.dart';

class DialogState {
  List<MessageGroup> messages;

  DialogState._();

  factory DialogState.initial() {
    return DialogState._()..messages = [];
  }
}

class DialogBloc extends Bloc<DialogEvent, DialogState> {
  @override
  DialogState get initialState => DialogState.initial();

  @override
  Stream<DialogState> mapEventToState(DialogEvent event) async* {
    if (event is NewMessageEvent) {
      yield addMessage(event.message);
    }
  }

  DialogState addMessage(Message msg) {
    if (currentState.messages.length > 0 &&
        currentState.messages.last.type == msg.type) {
      currentState.messages.last.messages.add(msg);
    } else {
      currentState.messages.add(MessageGroup([msg], msg.type));
    }
    return currentState;
  }

  void sendMessage(Message msg) {
    dispatch(NewMessageEvent(msg));
  }
}

abstract class DialogEvent {}

class NewMessageEvent extends DialogEvent {
  final Message message;
  NewMessageEvent(this.message);
}

class Message {
  String text;
  MessageType type;
  Message(this.text, this.type);
}

class MessageGroup {
  MessageGroup(this.messages, this.type);
  List<Message> messages;
  MessageType type;
}

UI (chatbot_page.dart)

import 'dart:async';
import 'dart:math';

import 'package:Djebots/blocs/dialog_bloc.dart';
import 'package:Djebots/ui/widgets/bot_avatar.dart';
import 'package:Djebots/ui/widgets/chat_message.dart';
import 'package:Djebots/ui/widgets/dial_pane.dart';
import 'package:Djebots/ui/widgets/dialog_list.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class BotScreen extends StatefulWidget {
  BotScreen({Key key}) : super(key: key);

  @override
  _BotScreenState createState() => _BotScreenState();
}

class _BotScreenState extends State<BotScreen> {
  DialogBloc _dialogBloc;
  final Random rnd = Random();
  final _listKey = GlobalKey<AnimatedListState>();
  final _lastListItemKey = GlobalKey<AnimatedListState>();
  final ScrollController _scrollController = ScrollController();

  List<MessageGroup> _messages;

  double _botScrollOffset;
  static const double _botPosition = 150;
  double _lastSentSize;
  int _botAnimSpeed;

  @override
  void initState() {
    setState(() {
      _messages = [];
      _botScrollOffset = 0;
      _lastSentSize = 0;
      _botAnimSpeed = 200;
      _dialogBloc = DialogBloc();
    });
    super.initState();
    _simulate();
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      builder: (BuildContext context) => _dialogBloc,
      child: Scaffold(
        appBar: AppBar(
          title: Text('La Libertรฉ'),
          iconTheme: IconThemeData(color: Colors.red),
          textTheme: TextTheme(title: TextStyle(color: Colors.red)),
          backgroundColor: Colors.transparent,
          centerTitle: true,
          elevation: 0,
        ),
        body: Stack(
          children: <Widget>[
            Column(
              children: <Widget>[
                Expanded(
                    child: NotificationListener<ScrollNotification>(
                  onNotification: (scrollNotification) =>
                      _onScroll(scrollNotification),
                  child: DialogList(
                    listKey: _listKey,
                    lastListItemKey: _lastListItemKey,
                    messages: _messages,
                    scrollController: _scrollController,
                  ),
                )),
                Align(
                  alignment: Alignment.bottomCenter,
                  child: DialPane(),
                )
              ],
            ),
            BlocListener(
              bloc: _dialogBloc,
              listener: (BuildContext context, DialogState state) {
                _updateLastSentSize(state.messages);
                _updateDialogList();
                setState(() {
                  _messages = state.messages;
                });
              },
              child: AnimatedPositioned(
                duration: Duration(milliseconds: _botAnimSpeed),
                bottom: (_botPosition + _botScrollOffset + _lastSentSize) >= 75
                    ? (_botPosition + _botScrollOffset + _lastSentSize)
                    : 75,
                child: Hero(
                  tag: 'bot-lib',
                  child: BotAvatar(
                    color: Colors.red,
                  ),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  _simulate() {
    Timer(Duration(seconds: 2), () {
      MessageType type =
          rnd.nextBool() ? MessageType.recieved : MessageType.sent;
      String text = "Test ";
      for (var i = 0; i < rnd.nextInt(40); i++) {
        text += "test ";
      }
      //addMessage(Message(text, type));
      _dialogBloc.sendMessage(Message(text, type));
      _simulate();
    });
  }

  bool _onScroll(ScrollNotification e) {
    var speed = e is ScrollUpdateNotification ? 0 : 200;
    setState(() {
      _botAnimSpeed = speed;
    });

    if (e.metrics.extentBefore <= _botPosition + _lastSentSize - 70) {
      setState(() {
        _botScrollOffset = -e.metrics.extentBefore;
      });
    }
    return true;
  }

  _updateDialogList() {
    if (_listKey.currentState != null) {
      _listKey.currentState
          .insertItem(0, duration: Duration(milliseconds: 300));
    }
  }

  _updateLastSentSize(messages) {
    final keyContext = _lastListItemKey.currentContext;
    double height = 0.0;
    if (messages != null &&
        messages.length > 0 &&
        messages.last.type == MessageType.sent &&
        keyContext != null) {
      final box = keyContext.findRenderObject() as RenderBox;
      height = box.size.height;
    }
    setState(() {
      _lastSentSize = height;
    });
  }

  @override
  void dispose() {
    _dialogBloc.dispose();
    super.dispose();
  }
}

Full Flutter Project

hawkinsjb1 commented 5 years ago

To start, I don't think yielding a modified 'currentState' is the way to go for constructing a new state like in your addMessage() function. Also, the bloc listener will not fire if the new state is the same as the previous state, a state of the same type but different 'messages' list will not count as a state change.

felangel commented 5 years ago

Hi @DamienMrtl ๐Ÿ‘‹ Thanks for opening an issue!

As @hawkinsjb1 pointed out, you should not mutate and yield currentState. Instead, you should always return a new instance of state.

Check out the core concepts for more details.

Blocs will ignore duplicate states. If a Bloc yields State state where currentState == state, then no transition will occur and no change will be made to the Stream.

Hope that helps ๐Ÿ‘

DamienMrtl commented 5 years ago

Ok thanks, it seems to work now.

If it can help some future people here, I changed the mapEventToState method to this :

  Stream<DialogState> mapEventToState(DialogEvent event) async* {
    if (event is NewMessageEvent) {
      yield DialogState._()..messages = addMessage(event.message).messages;
    }
  }

I am still confused by this Youtube vidรฉo, isn't he also yielding the current state ?

felangel commented 5 years ago

@DamienMrtl that video is outdated and I left a comment to explain that you cannot mutate the state. Glad you got it working! ๐Ÿ’ฏ

basnetjiten commented 5 years ago

@felangel Hi there. I have similar problem when using BlocListener. It did not triggered when the state changed. Here what my bloc is:

@override
ProfileEditorState get initialState => ProfileEditorUnInitializedState();

@override
  Stream<ProfileEditorState> mapEventToState(
   ProfileEditorEvent event,
 ) async* {
   if (event is StageEditorEvent) {
  try {

    StageEditPost stageEditPost =
        StageEditPost(event.authToken, event.selectedStages);
    ProfileEditData data =
        await profileEditorRepo.getProfileStageEditResponse(stageEditPost);
    yield StageEditedState(message: data.message);
    print(data.message);
  } catch (error) {
    print(error.toString());
  }
}

//here is the StageEditState

  class StageEditedState extends ProfileEditorState {
  String message;

StageEditedState({this.message});
}

//I am using the Bloc Listener in the Button click:

OutlineButton(
          onPressed: () {
            selectedStageKey(
                _tempSelectedStageOptions, this.widget.menuListData);
               profileEditBloc.dispatch(StageEditorEvent(
               authToken: this.widget.authToken,
                selectedStages: requiredStageKey));

            BlocListener(
              bloc: profileEditBloc,
              // ignore: missing_return
              listener: (BuildContext context, ProfileEditorState state) {
                print('here');
                if (state is StageEditedState) {
              Navigator.pop(context,"success");
                  print("popping...");

                }
              },

            );

          },
          color: Color(0xFFfab82b),
          child: Text(
            'SAVE',
            style: TextStyle(color: Colors.blue),
          ),
        ),
basnetjiten commented 5 years ago

Is there anyone who is going to answer me ?

felangel commented 5 years ago

Hey @basnetjiten ๐Ÿ‘‹ You donโ€™t need a BlocListener if you want to do something on a button tap. You should only use a BlocListener if you want to do something in response to a state change (without user input). With the code you shared, the BlocListener is only created when the user taps the button so it will only start listening then. Hope that helps ๐Ÿ‘

basnetjiten commented 5 years ago

@felangel thankyou for clearing the concept . May I please ask you some suggestions for my app situation. I have SearchStateful page. It contains searchTextfield for keywords search. Based on the search the event is dispatched and on the builder function I am checking the state change to build the listview. That works perfectly fine. Now I also have setting logo when clicked takes to the dialog page. User make some configuration (like a advance search ) and when user finishes I dispatched the event and did Navigator.pop(context of first page). Now I have in te SearchSatefulpage. My question is how do I rebulid the list view again with new value in this page. Do I have to use condition to rebuild the Bloc . Or what's the best way to trigger the builder function so that my list view gets recreated based state change. I don't know why Bloc do not respond to state change when the event has dispatched from DialogBox page.

basnetjiten commented 5 years ago

I have tried using BlocListnener and it's child as Bloc in the FirstPage for checking state and building list view. It works fine for the keywords search but no luck for coming back from dialogBoxPage

felangel commented 5 years ago

@basnetjiten can you please share a link to a sample app which demonstrates the problem you're having? It'd be much easier for me to help if I can see your implementation.

basnetjiten commented 5 years ago

@felangel I am working in a local development environment here at the company. so data might not be available/ Is it Okay If I share you just the code?

basnetjiten commented 5 years ago

proposal_search_setting.dart page

class ProposalSearchSetting extends StatefulWidget {
 final UserProfileBloc userProfileBloc;
final ProposalSearchBloc proposalSearchBloc;
 final MenuListData menuListData;
final BuildContext context;
final Function() notifyParent;

ProposalSearchSetting({@required this.notifyParent, this.proposalSearchBloc,
this.userProfileBloc,
this.menuListData,
 this.context})
  : assert(menuListData != null),
    assert(userProfileBloc != null);

@override _ProposalSearchSettingState createState() => _ProposalSearchSettingState(); }

class _ProposalSearchSettingState extends State<ProposalSearchSetting>
 with SingleTickerProviderStateMixin {
UserProfileBloc get _userProfileBloc => widget.userProfileBloc;

ProposalSearchBloc get _proposalSearchBloc => widget.proposalSearchBloc;
List<String> selectedOptions = [];

String resultBy;

List<String> industries;
List<String> stages;
List<String> locations;
List<String> languages;
List<String> countries;
List<String> regionsValue = [];

MenuListData get _menuListData => widget.menuListData;
Animation<double> animation;
AnimationController controller;
double startingPoint;

@override
void initState() {
 super.initState();

}

@override
void dispose() {
  _userProfileBloc.dispose();
  _proposalSearchBloc.dispose();
  super.dispose();
}

@override
Widget build(BuildContext context) {
  //double startingPoint = MediaQuery.of(context).size.height;
    return MaterialApp(
    theme: ThemeData(
      buttonTheme: ButtonThemeData(
          minWidth: 200.0,
          height: 40.0,
          buttonColor: Colors.blue,
          textTheme: ButtonTextTheme.primary)),
      home: Scaffold(
      body: Padding(
      padding: const EdgeInsets.only(top: 100.0),
      child: Center(
        child: Container(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              RaisedButton(
                onPressed: () async {
                  resultBy = await showDialog(
                      context: context,
                      builder: (context) {
                        return ResultBySearchDialog(
                            userProfileBloc: _userProfileBloc,
                            menuListData: _menuListData,
                            title: 'Result By:',
                            options: _menuListData.displayBy.values.toList()
                        );
                      });
                },
                color: Colors.blue,
                child: Text(
                  'RESULT BY',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                onPressed: () async {
                  countries = await showDialog(
                      context: context,
                      builder: (context) {
                        return CountrySearchDialog(
                            userProfileBloc: _userProfileBloc,
                            menuListData: _menuListData,
                            title: 'Select Countries',
                            selectedOptions: selectedOptions,
                            onSelectedOptionListChanged: (options) {
                              selectedOptions = options;
                              print(selectedOptions);
                            });
                      });
                },
                color: Colors.blue,
                child: Text(
                  'COUNTRY',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                onPressed: () async {
                  industries = await showDialog(
                      context: context,
                      builder: (context) {
                        return IndustrySearchDialog(

                            menuListData: _menuListData,
                            title: 'Select Industries',
                            options: _menuListData.industries.values
                                .toList(),
                            selectedOptions: selectedOptions,
                            onSelectedOptionListChanged: (options) {
                              selectedOptions = options;
                              print(selectedOptions);
                            });
                      });
                },
                child: Text(
                  'INDUSTRIES',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                onPressed: () async {
                  stages = await showDialog(
                      context: context,
                      builder: (context) {
                        return StageSearchDialog(

                            menuListData: _menuListData,
                            title: 'Select Stages',
                            options:
                            _menuListData.stages.values.toList(),
                            selectedOptions: selectedOptions,
                            onSelectedOptionListChanged: (options) {
                              selectedOptions = options;
                              print(selectedOptions);
                            });
                      });
                },
                child: Text(
                  'STAGES',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                onPressed: () async {
                  languages = await showDialog(
                      context: context,
                      builder: (context) {
                        return LanguageSearchDialog(

                            menuListData: _menuListData,
                            title: 'Select Languages',
                            options: _menuListData.languages.values
                                .toList(),
                            selectedOptions: selectedOptions,
                            onSelectedOptionListChanged: (options) {
                              selectedOptions = options;
                              print(selectedOptions);
                            });
                      });
                },
                child: Text(
                  'LANGUAGES',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                onPressed: () async {
                  locations = await showDialog(
                      context: context,
                      builder: (context) {
                        return LocationSearchDialog(

                            menuListData: _menuListData,
                            title: 'Select Locations',
                            options: _menuListData.locations.values
                                .toList(),
                            selectedOptions: selectedOptions,
                            onSelectedOptionListChanged: (options) {
                              selectedOptions = options;
                              print(selectedOptions);
                            });
                      });
                },
                child: Text(
                  'LOCATIONS',
                  style: TextStyle(fontFamily: 'MyRaidPro'),
                ),
              ),
              SizedBox(
                height: 40,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  ButtonTheme(
                    textTheme: ButtonTextTheme.primary,
                    minWidth: 60,
                    child: RaisedButton(
                      onPressed: () {
                        Navigator.of(this.widget.context).pop();
                      },
                      color: Colors.blue,
                      child: Text(
                        'Cancel',
                        style: TextStyle(fontFamily: 'MyRaidPro'),
                      ),
                    ),
                  ),
                  ButtonTheme(
                    textTheme: ButtonTextTheme.primary,
                    minWidth: 60,
                    child: RaisedButton(
                      onPressed: () async{
                        _proposalSearchBloc.proposalFilterPostParam(
                            FilterProposalPost(
                              languageList: languages,
                              locationList: locations,
                              stageList: stages,
                              industryList: industries,
                              countryList: countries,
                              displayName: resultBy,
                              page: 1,
                              limit: 5,
                              offset: 5,
                              max_investment: "",
                              min_investment: "",
                              authtoken:
                           "eyJ0eXAiOiJzZWxmIiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VyX2lkIjoiNTM2NDA1Iiwic2l0ZV9pZCI6IlBLIiwiZXhwaXJlX3RpbWUiOjE1Njk5MDYyOTMsImdyb3VwIjoiMTAwIn0.RwDM0GNOJ61MoyCINBK5i4FEDyxyVJAErZ1NiYreemA",
                              keyword: "fire",
                              siteId: "PK",
                            ));

                        widget.notifyParent();
                        Navigator.pop(this.widget.context);

                        print(("value from dialog" +
                            industries.toString()));

                        print(("value from dialog" +
                            stages.toString()));

                        print(("value from dialog" +
                            locations.toString()));

                        print(("value from dialog" +
                            languages.toString()));
                      },
                      color: Colors.blue,
                      child: Text(
                        'Apply',
                        style: TextStyle(fontFamily: 'MyRaidPro'),
                      ),
                    ),
                  )
                ],
              )
            ],
          ),
        ),
      ),
     ),
     ),
    );
    }
 }

======================================== search_proposal_page

 class ProposalSearchPage extends StatefulWidget {
 final UserProfileBloc userProfileBloc;
 final MenuBloc menuBloc;

 ProposalSearchPage({this.userProfileBloc, this.menuBloc})
   : assert(menuBloc != null),
     assert(userProfileBloc != null);

  @override
 _ProposalSearchPageState createState() => _ProposalSearchPageState();
 }

class _ProposalSearchPageState extends State { UserProfileBloc get _userProfileBloc => widget.userProfileBloc; List filteredProposal = [];

MenuBloc get _menuBloc => widget.menuBloc;
ProposalSearchBloc _proposalSearchBloc;
ScrollController _scrollController = ScrollController();
String searchedKeyword = "";
int searchProposalPage = 1;

@override
void initState() {
  _proposalSearchBloc =
    ProposalSearchBloc(proposalRepository: ProposalListingRepo());
  _scrollController.addListener(_searchScrollListener);
  _menuBloc.dispatch(MenuResponseFetchedEvent());
  super.initState();
  }

  @override
  void dispose() {
   _proposalSearchBloc.dispose();

    _scrollController.dispose();
    super.dispose();
  }

@override
Widget build(BuildContext context) {
 return CupertinoApp(
  localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
    DefaultMaterialLocalizations.delegate,
    DefaultWidgetsLocalizations.delegate,
  ],
   theme: new CupertinoThemeData(
    textTheme: CupertinoTextThemeData(primaryColor: Colors.white),
    brightness: Brightness.light,
    primaryColor: Colors.white,
    //Changing this will change the color of the TabBar
    primaryContrastingColor: Colors.blue[600],
  ),
  home: Scaffold(
    appBar: AppBar(
      leading: IconButton(
        icon: Icon(Icons.chevron_left),
        onPressed: () {
          Navigator.of(context).pop();
        },
      ),
       actions: <Widget>[
        BlocBuilder(
          bloc: _menuBloc,
          // ignore: missing_return
          builder: (context, state) {
            if (state is MenuResponseFetchedState) {
              return new IconButton(
                icon: new Icon(CupertinoIcons.gear_big),
                onPressed: () async {
                  await showDialog<FilterProposalPost>(
                      context: context,
                      builder: (context) {
                        return ProposalSearchSetting(
                            notifyParent: refresh,
                            proposalSearchBloc: _proposalSearchBloc,
                            menuListData: state.menuListData,
                            userProfileBloc: _userProfileBloc,
                            context: context);
                      });
                },
              );
            }
          },
         ),
       ],
      title: Center(
        child: Container(
          width: 250.0,
          height: 35.0,
          decoration: BoxDecoration(
              color: Colors.black12,
              borderRadius: BorderRadius.all(Radius.circular(7.0))),
            child: CupertinoTextField(
            placeholder: 'search here.',
            style: TextStyle(
              color: Colors.white,
            ),
            onSubmitted: (keyword) {
              print(keyword);
              searchedKeyword = keyword;
              FilterProposalPost filterProposalPost =
              _buildSearchQueryParameter(keyword);
              // print(query);
              _proposalSearchBloc
                  .proposalFilterPostParam(filterProposalPost);
             },
           ),
         ),
       ),
     ),
    body: BlocBuilder(

      bloc: _proposalSearchBloc,
      // ignore: missing_return
       builder: (context, state) {
        if (state is ProposalSearchFetchingState) {
          return Center(
            child: CircularProgressIndicator(),
           );
         } else if (state is ProposalSearchFetchedState) {
          filteredProposal = state.filteredProposal;
          return _buildSearchProposalList(filteredProposal);
         }
       },
     ),
     ),

    );
 }

 refresh() {
   setState(() {

    });
  }

  Widget _buildSearchProposalList(List searchedProposals) {
   return ListView.builder(
    controller: _scrollController,
     itemCount: searchedProposals.length + 1,
    itemBuilder: (context, position) {
      return position >= searchedProposals.length
          ? _buildLoaderListItem()
          : ProposalCardFactory.createProposalCard(
          context, searchedProposals[position]);
       });
  }

 Widget _buildLoaderListItem() {
   return Center(child: CircularProgressIndicator());
 }

  FilterProposalPost _buildSearchQueryParameter(String keyword) {
  return FilterProposalPost(
  languageList: null,
  locationList: null,
  stageList: null,
   industryList: null,
   countryList: null,
   displayName: "default",
   page: 1,
   limit: 5,
   offset: 5,
   max_investment: "",
   min_investment: "",
   authtoken:
  "eyJ0eXAiOiJzZWxmIiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VyX2lkIjoiNTM2NDA1Iiwic2l0ZV9pZCI6IlBLIiwiZXhwaXJlX3RpbWUiOjE1Njk5MDYyOTMsImdyb3VwIjoiMTAwIn0.RwDM0GNOJ61MoyCINBK5i4FEDyxyVJAErZ1NiYreemA",
   keyword: keyword,
   siteId: "PK",
   );
 }

 void _searchScrollListener() {
 final currentScroll = _scrollController.position.pixels;
 final maxScroll = _scrollController.position.maxScrollExtent;

  if (currentScroll == maxScroll) {
  ++searchProposalPage;
  _proposalSearchBloc.proposalFilterPostParam(FilterProposalPost(
    languageList: null,
    locationList: null,
    stageList: null,
    industryList: null,
    countryList: null,
    displayName: "default",
    page: searchProposalPage,
    limit: 5,
    offset: 5,
    max_investment: "",
    min_investment: "",
    authtoken:
    "eyJ0eXAiOiJzZWxmIiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VyX2lkIjoiNTM2NDA1Iiwic2l0ZV9pZCI6IlBLIiwiZXhwaXJlX3RpbWUiOjE1Njk5MDYyOTMsImdyb3VwIjoiMTAwIn0.RwDM0GNOJ61MoyCINBK5i4FEDyxyVJAErZ1NiYreemA",
      keyword: searchedKeyword,
      siteId: "PK",
      ));
   }
 }

 onAfterBuild(BuildContext context) {

 }
}

========================= proposal_search_bloc

 class ProposalSearchBloc extends Bloc<ProposalSearchEvent, ProposalSearchState> {
   final ProposalListingRepo proposalRepository;
  List keywordSearchedProposalList = List();
  List filteredProposalList = List();
  ProposalSearchBloc({this.proposalRepository});

 void proposalFilterPostParam(FilterProposalPost filterProposalPost){

   dispatch(ProposalSearchFetchEvent(filterProposalPost: filterProposalPost));
     }

  @override
  ProposalSearchState get initialState => ProposalSearchFetchingState();

  @override
  Stream<ProposalSearchState> mapEventToState(event) async* {
   if (event is ProposalSearchFetchEvent) {
   try {
    print("proposal search even fired first time");

    final filteredProposal =
    await proposalRepository.filterProposal(event.filterProposalPost);
    filteredProposalList.addAll(filteredProposal);
    yield ProposalSearchFetchedState(filteredProposal: filteredProposalList);
    } catch (_) {
    //print(error.toString());
    yield ProposalSearchErrorState();
  }
  }

  }
 }

========================== event

import 'package:ainflutter/model/proposal/request/post/all_listing.dart';
import 'package:ainflutter/model/proposal/request/post/filtered.dart';

abstract class ProposalSearchEvent{}
class ProposalSearchFetchingEvent extends ProposalSearchEvent{}
class ProposalSearchFetchEvent extends ProposalSearchEvent{

 final FilterProposalPost filterProposalPost;

 ProposalSearchFetchEvent({this.filterProposalPost}):assert(filterProposalPost!=null);

}

class ProposalSearchFetchedEvent extends ProposalSearchEvent{
 }
class ProposalSearchErrorEvent extends ProposalSearchEvent{}
class ProposalSearchEmptyEvent extends ProposalSearchEvent{}

================== state

 abstract class ProposalSearchState  {
 }

class ProposalSearchFetchingState extends ProposalSearchState {}

 class ProposalSearchFetchedState extends ProposalSearchState {
List filteredProposal;

 ProposalSearchFetchedState({this.filteredProposal});

  @override
   String toString() =>
   'ProposalFetchedState { ProposalFetchedCount: ${filteredProposal.length}}';
 }

 class ProposalSearchErrorState extends ProposalSearchState {}

 class ProposalSearchEmptyState extends ProposalSearchState {}
felangel commented 5 years ago

@basnetjiten would it be possible for you to put together a simple sample app that illustrates the problem you're having and provide a link to the github repo? It would be way easier for me to help ๐Ÿ‘

kelidon commented 4 years ago

Also, the bloc listener will not fire if the new state is the same as the previous state, a state of the same type but different 'messages' list will not count as a state change.

Hey there! I got a situation when I do need my BlocListener to fire on repeating yield value. How can I perform this?

deepjyotk commented 3 years ago

@kelidon I am facing similar issue, where bloc listener gets triggered only once, how to trigger the bloc listener when a button is clicked?

kelidon commented 3 years ago

@DEEPJYOTSINGHKAPOOR I've kinda resolve it by passing null to bloc, and then I've passed the necessary value

felangel commented 3 years ago

@DEEPJYOTSINGHKAPOOR BlocListener gets triggers once for each state change. Please refer to the FAQ for more information on state not updating ๐Ÿ‘

thesmalleyes commented 3 years ago

Hi @felangel How to trigger Bloc Listener with the same state but different data?. Let's say I have NameState({'John'}) and after that I emit a new NameState(Andrew).

But I can't listen it in bloclistener

kelidon commented 3 years ago

Hey, @bayuramadeza, if you're handling some particular event in your bloc in order to yield new NameState, something like if(event is ParticularEvent) yield NameState('Andrew');

then it should work. So could your provide your bloc code? It would be very helpful.

thesmalleyes commented 3 years ago

Hi @kelidon , Yes if i yield or emit something, it will changes data in blocbuilder, but the state didn't trigger in BlocListener.

This is my code

class PositionCubit extends Cubit<PositionState> {
  PositionCubit() : super(PositionInitial());

  getPostion(){
    if(state is PositionLoaded){

    }
    else{
      determinePosition().then((value) => emit(PositionLoaded(position: value, status: true)));
    }
  }
}

And I listened it in a widget class

BlocListener<PositionCubit, PositionState>(
   listener: (context, state){
       if(state is PositionLoaded)
           //toast Position Loaded
    }
}

The state is always changes. from PositionLoaded, nextstate PositionLoaded

but it didn't trigger in bloclistener state condition

thesmalleyes commented 3 years ago

Hi, May someone help me to solve the issue above? Why bloc listener doesn't listen any state if I do this step:

  1. emit(LoadingState)
  2. emit(LoadedState(data))

But, when I did this step the listener will listen my state changes.

  1. emit(LoadingState)
  2. Asynchronous request data from API
  3. emit(LoadedState(data))

Why this happened? and when exactly listener will listen state changes?

This is My State

abstract class ArticleState extends Equatable {
  const ArticleState();

  @override
  List<Object> get props => [];
}

class ArticleInitial extends ArticleState {}

class ArticleLoading extends ArticleState {}

class ArticleLoaded extends ArticleState {
  final Article article;

  ArticleLoaded(this.article);

  @override
  List<Object> get props => [this.article];
}

class ArticleError extends ArticleState {
  final String message;

  ArticleError(this.message);

  @override
  List<Object> get props => [this.message];
}

And this is my cubit class

class ArticleCubit extends Cubit<ArticleState> {
  ArticleCubit() : super(ArticleInitial());

  final provider = TipsKarirProvider();

  fetchArticle(Article article)async{
    emit(ArticleLoading());
    if(article.vid!=null && article.articleTitle!=null)
      return emit(ArticleLoaded(article));
    try{
      final result = await provider.fetchArticle(article.permalink);
      if(result.statusCode==200)
        emit(ArticleLoaded(result.data));
      else
        emit(ArticleError('Artikel tidak ditemukan'));
    } catch(e){
      emit(ArticleError('Artikel tidak ditemukan'));
    }
  }
}

And when I called listener it didn't listen any state

@override
  void initState() {
    Article article = widget.article;
    BlocProvider.of<ArticleCubit>(context).fetchArticle(article);
    super.initState();
  }

Widget build(BuildContext context) {

    return BlocConsumer<ArticleCubit, ArticleState>(
      listener: (context, _articleState){
        print("Listening");
        if(_articleState is ArticleLoaded){
          BlocProvider.of<SearchContentCubit>(context).searchRelatedContentFromArticle(_articleState.article, categoryId: _articleState.article.articleGroup??'');
        }
      },
      builder: (context, _articleState) {
      }
biniyam112 commented 3 years ago

Hey @basnetjiten You donโ€™t need a BlocListener if you want to do something on a button tap. You should only use a BlocListener if you want to do something in response to a state change (without user input). With the code you shared, the BlocListener is only created when the user taps the button so it will only start listening then. Hope that helps

Thanks very much for this

tran-nam-long commented 2 years ago

Hello @bayuramadeza

I have the same issue as you. I emit two states UserInProgress and UserSuccess, but BlocListener only receives the UserSuccess. However, the BlocBuilder can catch both :).

@felangel Could you please have a look at this issue?

PS. "flutter_bloc: ^8.0.1" and Cubit are being used.

Thank you!

Gene-Dana commented 2 years ago

Hi there ๐Ÿ‘‹๐Ÿผ Do you have a repo to share?

tran-nam-long commented 2 years ago

Hi @Gene-Dana
Thank you for the quick reply. I will share with you a simple repository after I finish it because the issue is happening on the company's project. Thank you again!

arutkayb commented 2 years ago

Hi @DamienMrtl ๐Ÿ‘‹ Thanks for opening an issue!

As @hawkinsjb1 pointed out, you should not mutate and yield currentState. Instead, you should always return a new instance of state.

Check out the core concepts for more details.

Blocs will ignore duplicate states. If a Bloc yields State state where currentState == state, then no transition will occur and no change will be made to the Stream.

Hope that helps ๐Ÿ‘

I extended my state from Equatable and I made sure that hashCode changes but that variable change in the state doesn't trigger onChange of my Cubit. I understand the concept of using immutable States but am still curious about the reason of this behavior here. Can anyone explain that?

My state

class TripListState extends Equatable {
  List<Trip> _trips;

  TripListState(this._trips) : super();

  List<Trip> get trips => _trips;

  set trips(List<Trip> trips) {
    _trips = trips;
  }

  @override
  List<Object> get props {
    return [_trips];
  }
}

Snippet from my Cubit:

var trips = await _useCaseTrip.retrieveTrips();
state.trips = trips;
emit(state);
Gene-Dana commented 2 years ago

hi there @arutkayb ๐Ÿ‘‹๐Ÿผ Bloc will not update the state because it looks at state.trips as the same as it was before. The contents may be different because you mutated the state, but when the framework compares the old version vs the new version, it sees two objects with the same hashcode (as if nothing changed).

Equatable is made specifically to address this problem - allows you to simply define how to distinguish between custom objects. You can learn more about this in the equatable package

The suggested approach is to use a copyWith method.

Checkout the Todos example https://github.com/felangel/bloc/blob/aef33799107ef285622b8bc0ea8015eb2536095a/examples/flutter_todos/lib/edit_todo/bloc/edit_todo_bloc.dart#L27-L32

The copyWith essentially makes a complete new copy of the state with the updated information which the framework will recognize as new and subsequently update once it's emitted.

You can see here in the shopping cart example an entirely new Cart object is supplied to the bloc in order to update the state https://github.com/felangel/bloc/blob/aef33799107ef285622b8bc0ea8015eb2536095a/examples/flutter_shopping_cart/lib/cart/bloc/cart_bloc.dart#L30-L43

If you have further questions like this you can open up a new issue or ask in the Discord ! https://discord.gg/bloc