Closed DamienMrtl closed 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.
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 ๐
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 ?
@DamienMrtl that video is outdated and I left a comment to explain that you cannot mutate the state. Glad you got it working! ๐ฏ
@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),
),
),
Is there anyone who is going to answer me ?
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 ๐
@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.
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
@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.
@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?
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
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 {}
@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 ๐
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?
@kelidon I am facing similar issue, where bloc listener gets triggered only once, how to trigger the bloc listener when a button is clicked?
@DEEPJYOTSINGHKAPOOR I've kinda resolve it by passing null to bloc, and then I've passed the necessary value
@DEEPJYOTSINGHKAPOOR BlocListener gets triggers once for each state change. Please refer to the FAQ for more information on state not updating ๐
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
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.
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
Hi, May someone help me to solve the issue above? Why bloc listener doesn't listen any state if I do this step:
But, when I did this step the listener will listen my state changes.
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) {
}
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
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!
Hi there ๐๐ผ Do you have a repo to share?
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!
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 ofstate
.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);
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
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):
UI (chatbot_page.dart)
Full Flutter Project