hi. quick question. so based on this statement:
BlocListener is a Flutter widget which takes a BlocWidgetListener and an optional Bloc and invokes the listener in response to state changes in the bloc. It should be used for functionality that needs to occur once per state change such as navigation, showing a SnackBar, showing a Dialog, etc...
listener is only called once for each state change (NOT including initialState) unlike builder in BlocBuilder and is a void function.
What im doing is that when the widget loads, it runs a query in the api to get the list. then the BlocListener is supposed to run (one time) once the bloc state changes so i could use the list. but nothing happens. I wish to use BlocListener instead of BlocBuilder since it is only a 1 time thing.
What do you think I did wrong with my code.
String _levelFilter = '';
@override
void initState() {
super.initState();
BlocProvider.of<LevelListBloc>(context).add(LevelEvent.list);
}
@override
Widget build(BuildContext context) {
return BlocListener<LevelListBloc, List<Level>>(
condition: (previousState, state) {
print('condition block level');
return true;
},
listener: (context, list) {
print('bloc listener level');
// Re-populate level list in action bar.
setState(() { });
},
child: Scaffold(
appBar: AppBar(
title: Text(tr('app_name')),
actions: [
Center(
child: DropdownButton(
items: levels,
isDense: true,
value: _levelFilter,
onChanged: (value) {
setState(() {
_levelFilter = value;
});
},
),
) : SizedBox(),
]
),
body: Container(),
),
);
And my level bloc is
class LevelListBloc extends Bloc<LevelEvent, List<Level>> {
@override
List<Level> get initialState => List<Level>();
@override
Stream<List<Level>> mapEventToState(LevelEvent event) async* {
switch (event) {
case LevelEvent.list:
if (state.length == 0) {
Response res = await Web.getLevels();
res.data.forEach((element) => state.add(Level.fromJson(element)));
}
yield state;
break;
}
}
Level getLevel(String id) {
List<Level> list = state.where((level) => level.id == id).toList();
return list.length > 0 ? list[0] : null;
}
}
enum LevelEvent { list }
Hi @chitgoks, initState only triggers once the state has been created(or initialized from the createState method).
The Bloc Listener is now waiting for the next event to trigger a rebuild to the widget.
You can change it here
@override
Widget build(BuildContext context) {
return BlocListener<LevelListBloc, List<Level>>(
condition: (previousState, state) {
print('condition block level');
return true;
},
listener: (context, list) {
print('bloc listener level');
// Re-populate level list in action bar.
setState(() { });
},
child: Scaffold(
appBar: AppBar(
title: Text(tr('app_name')),
actions: [
Center(
child: DropdownButton(
items: levels,
isDense: true,
value: _levelFilter,
onChanged: (value) {
setState(() {
/// You might not need setState but oh well.
_levelFilter = value;
BlocProvider.of(context).add(LevelEvent.list)
});
},
),
) : SizedBox(),
]
),
body: Container(),
),
);
i see. so when initstate is run, build() is run but since the bloc getting from the api is run after, the listener is stuck. am i right?
yes in that code setState is not needed.
The listener is just waiting for state changes, basically yes. Has the solution above solved your problem?
i havent tried it yet. will give feedback.
if it runs in onChange when the widget is created, then this would work. i do not need it to access the api every time the dropdown item is changed by selection, though, so a check on the state if the list is empty, will access the api.
will update you.
@chitgoks have you tried moving the Listener above this widget because what could be happening is the event is added and the state change occurs before the Listener starts listening. It would be great if you could share a sample app which illustrates the issue and I would be happy to open a pull request with suggestions 馃憤
@felangel this is the main widget. so i cant move it above another widget. seems about right with your opinions.
i placed some prints in the widget build and in the bloc api. sure enough, the widget got built first before the api was called. but still, yield was called so BlocListener should have run. or does yield only work on BlocBuilder?
I can use BlocBuilder though, because ive put a condition that if the list action is called and if the list is not empty anymore, it will not access the api from return the state's list right away.
Can you please share a link to a sample app which illustrates the issue you鈥檙e having? Thanks 馃檹
I uploaded a bare sample app. it's weird. because it works here, im not sure why in the project it doesnt run. will look into this and try to provider a stripped down version.
i have a question on the sample app though.
the output is
I/flutter (13474): main build
I/flutter (13474): before bloc add call
I/flutter (13474): after bloc add call
I/flutter (13474): child build
I/flutter (13474): get web api
I/flutter (13474): yield String
I/flutter (13474): main build
I/flutter (13474): child build
how come main widget is rebuilt? There is no bloclistener or blocbuilder in the code.
So the cause is in the LevelListBloc action
when the list is retrieved from the api i only did state.add.
the fix is to instantiate a new List
@chitgoks you need to make sure you always create new instances instead of modifying existing instances of state because when you mutate an existing state the reference does not change so the bloc will treat it as the same as before and ignore it.