Why is Bloc skipping this emit command? - flutter

I have an bloc that receives an event called OpenTourStop that invokes a function whose first line of code invokes emit(Waiting()); and then proceeds to execute some logic before emitting a different state. In the UI, the BlocConsumer is supposed to print out a message to the console as soon as state equals Waiting, but it NEVER does. The bloc does not emit the Waiting state, but does emit the other states that result from completing the function. What am I doing wrong?
Below are the relevant sections of code for the bloc and UI
Bloc code:
class QuiztourManagerBloc
extends Bloc<QuiztourManagerEvent, QuiztourManagerState> {
final QuiztourRemoteData _repo;
QuiztourManagerBloc({QuiztourRemoteData repo})
: _repo = repo,
super(QuiztourManagerInitial()) {
on<OpenTourStop>(_openTourStop);
}
_openTourStop(event, emit) {
emit(Waiting()); // Why doesn't the Waiting state show up in the UI?
final _tourStopIndex = event.tourStopIndex;
// section of code removed for clarity
if (_quizPlayDoc.seenRules && tourStopGameResults.isEmpty) {
emit(ShowQuizQuestionViewManager(
quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
// emit(ShowQuizQuestions(quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
} else if (tourStopGameResults.length > 0) {
emit(ShowQuizTourStopScreen(
tour: event.tour,
tourStopIndex: event.tourStopIndex,
quizPlayDoc: _quizPlayDoc,
maxTourStopPoints: _maxTourStopPoints.toString(),
pointsEarned: _tourStopScore.toString(),
));
} else {
emit(ShowQuizRules(_quizPlayDoc));
}
}
}
UI code (from class QuizTourStopViewManager) :
#override
Widget build(BuildContext context) {
return BlocConsumer<QuiztourManagerBloc, QuiztourManagerState>(
builder: (context, state) {
if (state is Waiting) {
print('!!!!!!!!!!!!!!!!!!!!! Waiting '); // Why does this line never get executed?
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
else if (state is ShowQuizTourStopScreen) {
return QuizTourStop( );
}
},
listener: (_, state) {},
);
}
The UI that triggers the event is a button. The code associated with that button is below:
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (tourType == "quizTour") {
BlocProvider.of<QuiztourManagerBloc>(context)
.add(OpenTourStop(
tour: tour,
tourStopIndex: selectedTourStopIndex,
));
return QuizTourStopViewManager(
tour: tour,
// game: widget.game,
selectedTourStopIndex: selectedTourStopIndex,
);

I guess that when you send 'OpenTourStop' event at 'onTap' method, 'QuizTourStopViewManager' page is not builded.
So would you try to change event call position after 'OpenTourStop' page is builded?
At initState() method inside.
or
At 'bloc' parameter in BlocConsumer method.

Related

Is there any easy way to use a Future (which performs an http connection) inside a stateful widget without having it reconnect on every screen build?

Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.

BlocListener not being executed after Cubit function call

So, I have a cubit with a function that POSTs some data. I have three states, DataLoading, DataError and DataLoaded for my cubit.
When the user taps on a button, I call the function in the cubit. After that, I have a BlocListener to wait until the Cubit emits the DataLoaded state. The issue is that the listener is reacting to the state changes.
Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
);
}
),
I've tried using await on the read() call but that didn't work. How do I react to the state changes here? Thanks.
This BlocListener isn't listening because you have added the listener inside a function instead of adding it in widget tree. Wrap your button inside BlocConsumer widget and it will works fine. Have a look into below code.
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
builder: (context, state) {
return Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
});
},
),

Flutter Bloc consumer states overlap

I'm having some troubles with the bloc pattern, specifically with BlocConsumer reactions to the emitted states which lead to certain overlaps.
The main character is an object list, for semplicity let's say an object with a String and int parameters.
On the first page I'm adding an event to the bloc to immediately perform the list fetch:
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc()..add(FetchNewList());
}
The layout, a grid view holding buttons with the string param as label follow by the int value, it's build by a BlocConsumer once the list is fetched:
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadingState) {
return CircularProgressIndicator();
} else if (state is ListLoadedState) {
return GridView.count(
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: state.exampleList
.map((e) => FlatButton(
onPressed: () => _openSecondScreen(),
child: Text('${e.label} ${e.value}'),
))
.toList());
} else if (state is ListFetchErrorState) {
return FlatButton(
onPressed: () =>
_exampleBloc = ExampleBloc()..add(FetchNewList()),
child: Text('try again'));
} else {
return Container();
}
},
listener: (context, state) {
//will be used for other purpose
},
),
Non-blocking errors (for problems on list edit operations basically) are shown in a snackbar.
Clicking on a button open a second page, where the same list is loaded as a ListView and some actions allows making changes on list items (for simplicity sake a tap increment the int value of 1 while a long click delete the item from the list):
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadedState) {
return ListView.builder(
itemCount: state.exampleList.length,
itemBuilder: (BuildContext context, int index) {
var item = state.exampleList[index];
return ListTile(
onTap: () => _exampleBloc.add(UpdateItemEvent(
Object(item.label, item.value + 1))),
onLongPress: () =>
_exampleBloc.add(DeleteItemEvent(item)),
title: Text(
'${item.label} + value: ${item.value.toString()}'),
);
});
} else {
return Container();
}
},
listener: (context, state) {
if (state is ListUpdateErrorState) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
} else if (state is ListUpdateInProgressState) {
_showCircularProgressIndicator();
}
},
));
First problem: as the list is the same for both pages, I thought it would be ok to use the same bloc but, even if the last state on the first page is the "ListLoadedState", once the second page is loaded the bloc builder is not building the corresponding widget. As workaround I thought to store the fetched list in a variable in the bloc class and adding an event (GetListAlreadyLoaded) in the initState of the second page to force the bloc to emit once again the ListLoadedState holding the list. Is there a better way to retrieve the last state of the bloc?
Second problem: if any error occurs while performing an update/delete operations on the second page, I would like to simply show a snackbar with an error message. So in the bloc class I have something like this:
#override
Stream<ExampleState> mapEventToState(ExampleEvent event) async* {
//more events...
} else if (event is PerformListUpdate) {
yield ListUpdateInProgressState();
//performing remote update
var updateResult = await _repository.performUpdate(event.objectToUpdate)
//updating local list
if (updateResult.isSuccessful) {
var index = exampleList.indexWhere(
(element) => element.label == event.objectToUpdate.label);
exampleList[index] = event.objectToUpdate;
//triggering the bloc builder with the updated list
yield ListLoadedState(exampleList);
} else {
//emit a state in order to show the error message
yield ListUpdateErrorState(
"An error occurred while performing update");
}
}
}
}
The problem is that if the ListUpdateErrorState is emitted the snackbar is shown, but the bloc builder is triggered by the new state and it rebuilds the widget in the else branch, which is an empty container. As a workaround I thought to first emit the ListUpdateErrorState to allow the listener function react and show the snackbar then, soon after, emit again the ListLoadedState with the last list value in order to trigger also the builder and show again the list view. Is that okay or there's a better way to show errors?
Third problem (basically the same as the second): while performing an asynchronous operation on the second page I would like to show some CircularProgressIndicator without "losing" the list view which could be for example in the appbar, at the bottom of the list or in the middle of the screen above the list. Emitting the "ListUpdateInProgressState" while starting the operation and reacting to it in the bloc listener however triggers the builder function which "destroys" the List view. How can I show the loading indicator without losing the list view?

Navigate while Widget state build is being executed

I'm building a simple Flutter app. Its launch screen determines if the user if logged in or not, and depending on that redirects to the login or main/home screen afterwards.
My Launch screen is a StatefulWidget, its state is shown below. It uses a ViewModel class that extends ChangeNotifier (its code is irrelevant, so I didn't include it).
class _LaunchPageState extends State<LaunchPage> {
LaunchViewModel _viewModel = LaunchViewModel();
#override
void initState() {
super.initState();
_viewModel.checkSessionStatus();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<LaunchViewModel>(
builder: (_) => _viewModel,
child: Scaffold(
body: Consumer<LaunchViewModel>(
builder: (context, viewModel, _) {
if (viewModel.state is LaunchInitial) {
return CircularProgressIndicator();
}
if (viewModel.state is LaunchLoginPage) {
Navigator.pushNamed(context, "login");
}
if (viewModel.state is LaunchMainPage) {
Navigator.pushNamed(context, "main");
}
return Container();
},
),
),
);
}
}
The ViewModel emits one of 3 states:
LaunchInitial: Default state.
LaunchLoginPage: Indicates that the Login page should be displayed.
LaunchMainPage: Indicates that the Main page should be displayed.
The LaunchInitial state is handled fine, and a progress bar is displayed on the screen. But the other 2 states cause the app to crash. The following error is thrown:
This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets
It seems that trying to redirect to another screen while the Consumer's build method is being executed is causing this issue. What's the correct way to do this?
Thanks!
You can't directly call Navigator within widget tree. If you have event-state builder, so better change the widget tree you are rendering:
builder: (context, viewModel, _) {
if (viewModel.state is LaunchInitial) {
return CircularProgressIndicator();
}
if (viewModel.state is LaunchLoginPage) {
return LoginPage();
}
if (viewModel.state is LaunchMainPage) {
return MainPage();
}
return Container();
},
You have to return Widget with each child inside build method.
Alternatively, you can do this with Navigation:
#override
void didChangeDependencies() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (viewModel.state is LaunchLoginPage) {
Navigator.pushNamed(context, "login");
}
if (viewModel.state is LaunchMainPage) {
Navigator.pushNamed(context, "main");
}
});
super.didChangeDependencies();
}
addPostFrameCallback method will be called right after the build method completed and you can navigate inside.
Be sure your provider don't have lifecycle issue.

Can I skip BlocBuilder render in some case?

There's a screen that have a bloc MyBloc myBloc
In the screen's build method, it's like this:
Widget build(BuildContext context) {
return MyCustomLoadingStack( //My custom widget to have the main content below the loading widget
_buildContent(context), //My main content
_buildLoading(context)); //My loading on top
}
And my 2 method:
Widget _buildContent(BuildContext context) {
return Column(children: <Widget>[
OtherWidgetOne(),
OtherWidgetTwo(),
BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyStatestate) {
switch (state.type) {
case MyStateList.doneWorking:
return MyDataWidget(); // this content cares about displaying the data only
default:
return Container(); //otherwise display nothing
}
},
)
]);
}
Widget _buildLoading(BuildContext context) {
return BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyState state) {
switch (state.type) {
case MyStateList.loading:
return LoadingView(); //This _buildLoading cares the loading only
default:
return Container(); //If it's not loading the show nothing for the loading layer
}
},
)
}
My problem is when the content is currently showing data. When I yield MyState(type: MyStateList.loading) to show the loading when doing something else (like load more for the data which is currently showing). Both BlocBuilder are called and then the _buildContent(context) show nothing because it doesn't meet the MyStateList.doneWorking condition. And of course the _buildLoading(context) shows the loading on an empty content bellow.
Is there anyway I can skip the BlocBuilder inside _buildContent(context) to keeps showing the current data and still have the loading on top?
I though about having a Widget to contains the data or empty Container() to use in the default case of the _buildContent(context) but it doesn't make sense to me because they may re-render the same widget.
Thank you for your time.
Great question! Is there a reason why you're using BlocBuilder instead of StreamBuilder?
To show nothing while the data is loading and auto populate the data once it's loaded, I typically do the following:
Widget _buildLoading(BuildContext context) {
return StreamBuilder<Model>(
stream: bloc.modelStream,
builder: (context, snapshot) {
if (snapshot.hasError) return _buildErrorWidget(snapshot.error, context);
if (snapshot.hasData) {
return DesiredWidget()
} else {
return LoadingView()
}
}
I haven't seen your BloC file but you may have to add a couple lines like the following:
final _modelFetcher = BehaviorSubject<Model>();
Stream<Model> get modelStream => _modelFetcher.stream;
Function(Model) get changeModelFetcher => _modelFetcher.sink.add;
#override
void dispose() async {
await _modelFetcher.drain();
_modelFetcher.close();
}
Let me know if this helps at all.
There is a pullrequest beeing discussed to fix this problem right now:
https://github.com/felangel/bloc/issues/315?fbclid=IwAR2x_Q1x5MIUUPE7zFRpjNkhjx5CzR0qiRx-P3IKZR_VRGEp3eqQisTthDo
For now, you can use a class for a more complex state. This can be something like:
class MyState extends Equatable {
final bool isLoading;
final List<MyData> data;
bool get hasData => data.isNotEmpty;
MyState(this.isLoading,this.data) : super([isLoading,data]);
}
I use Equatable (https://pub.dev/packages/equatable) for easier isEqual implementation.
Widget _buildContent(BuildContext context) {
return Column(children: <Widget>[
OtherWidgetOne(),
OtherWidgetTwo(),
BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyStatestate) {
if (state.hasData) {
return MyDataWidget(); // this content cares about displaying the data only
} else {
return Container(); //otherwise display nothing
}
},
)
]);
}
Widget _buildLoading(BuildContext context) {
return BlocBuilder<MyEvent, MyState>(
bloc: myBloc,
builder: (BuildContext context, MyState state) {
if (state.isLoading) {
return LoadingView(); //This _buildLoading cares the loading only
} else {
return Container(); //If it's not loading the show nothing for the loading layer
}
},
);
}
Downside of this appraoch is, that the datawidget will redraw, even tho the data doesnt change. This will be fixed with the mentioned pullrequest.