Bloc is either calling multiple times or not getting called at all - flutter

I have 2 screens
Screen A
Screen B
I go from screen A to Screen B via the below code
onTap: () {
Navigator.pushNamed(context, '/screen_b');
},
Screen B code
class ScreenB extends StatefulWidget {
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
SampleBloc sampleBloc;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
super.dispose();
sampleBloc.clear(); // If i add this, no event is firing from second time i come to the page. initState() is being called i checked so sampleBloc is not null.
}
#override
Widget build(BuildContext context) {
....
onTap: () {
sampleBloc.add(CreateSampleEvent());
},
....
}
}
When i click tap to add CreateSampleEvent to the sampleBloc in Screen B, the 'sampleBloc.stream.listen' getting fired multiples times.
Sample outcome
Step 1. First do sampleBloc.add (tap) -> sampleBloc.stream.listen
fired one time
Step 2. Go back to Screen A and come back to
Screen B Second go sampleBloc.add (tap) -> In one case i saw
the firing takes place in a pair of 2 times, and in other times the
firing takes place in pair of 4 times.
class SampleBloc extends Bloc<SampleEvent, SampleState> {
final SampleRepository sampleRepository;
SampleBloc({this.sampleRespository}) : super(null);
#override
Stream<SampleState> mapEventToState(SampleEvent event) async* {
if (event is SampleEvent) {
yield* mapSampleEventToState();
}
}
Stream<SampleEvent> mapSampleEventToState() async* {
yield SampleInProgressState();
try {
String sampleId = await sampleRepository.createSample();
if (sampleId != null) {
yield SampleCompletedState(sampleId, uid);
} else {
yield SampleFailedState();
}
} catch (e) {
print('Error: $e');
yield SampleFailedState();
}
}
Any ideas what might be going wrong ?

Since you are creating a manual subscription (by fetching the Bloc and then listening to a stream) you'll also need to manually cancel that subscription when you're done with it, otherwise, the SampleBloc will just keep getting new subscriptions and yielding events to all of them.
For that, you can either:
Save the subscription and then cancel it in the dispose() method.
SampleBloc sampleBloc;
StreamSubscription _subscription;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
_subscription = sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
You can take advantage of the BlocListener from the package which is a widget that is disposed automatically, hence, removes the subscriptions it created.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)

Related

Cubit - listener does not catching the first state transition

I'm using a Cubit in my app and I'm struggling to understand one behavior.
I have a list of products and when I open the product detail screen I want to have a "blank" screen with a loading indicator until receiving the data to populate the layout, but the loading indicator is not being triggered in the listener (only in this first call, when making a refresh in the screen it shows the loader).
I'm using a BlocConsumer and i'm making the request in the builder when catching the ApplicationInitialState (first state), in cubit I'm emitting the ApplicationLoadingState(), but this state transition is not being caught in the listener, only when the SuccessState is emitted the listener triggers and tries to remove the loader.
I know the listener does not catch the first State emitted but I was expecting it to catch the first state transition.
UI
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
Widget build(BuildContext context) {
_l10n = AppLocalizations.of(context);
return _buildConsumer();
}
_buildConsumer() {
return BlocConsumer<ProductCubit, ApplicationState>(
bloc: _productCubit,
builder: (context, state) {
if (state is ApplicationInitialState) {
_getProductDetail();
}
return Scaffold(
appBar: _buildAppbar(state),
body: _buildBodyState(state),
);
},
listener: (previous, current) async {
if (current is ApplicationLoadingState) {
_loadingIndicator.show(context);
} else {
_loadingIndicator.close(context);
}
},
);
}
Cubit
class ProductCubit extends Cubit<ApplicationState> with ErrorHandler {
final ProductUseCase _useCase;
ProductCubit({
required ProductUseCase useCase,
}) : _useCase = useCase,
super(const ApplicationInitialState());
void getProductDetail(String id) async {
try {
emit(const ApplicationLoadingState());
final Product = await _useCase.getProductDetail(id);
emit(CSDetailSuccessState(
detail: ProductDetailMapper.getDetail(Product),
));
} catch (exception) {
emit(getErrorState(exception));
}
}
}
ApplicationLoadingState
abstract class ApplicationState extends Equatable {
const ApplicationState();
#override
List<Object> get props => [];
}
class ApplicationLoadingState extends ApplicationState {
const ApplicationLoadingState();
}

Flutter Trigger bloc event again

On my first screen, I fill out the form and then click the next button it added SubmitDataEvent() to the bloc. Then, the BolcListner listing and when it comes to SuccessSate it navigate to the next screen.
on the second screen, when I click the back button it navigates to the previous screen. After that, when I change the user-input data on the form and again click the next button now SubmitDataEvent() is not added.
I preferred some resources related to this and I understand the problem is that the state is in SuccessSate and it doesn't change to InitialState. So in dispose() I used bloc.close();
#override
void dispose() {
bloc.close();
super.dispose();
}
But still, it's not working. Also, I try with this code
#override
void dispose() {
bloc.emit(InitialState);
bloc.close();
super.dispose();
}
still, it's not working.
I used this to navigate between screens:
Navigator.popAndPushNamed()
What I want to do is:
On the first screen, when clicking on the next button SubmitDataEvent() added to the bloc and it in SuccessState it navigate to the next screen. When I click the back button on the second page it navigates again to the first screen. Now when I click the next button on the first screen I want to run all bloc process again.
There are no dependencies with the first and second screens.
first screen code:
...
#override
void initState() {
super.initState();
bloc = injection<SubmitPersonalDetailsBloc>();
EasyLoading.addStatusCallback((status) {
print('EasyLoading Status $status');
if (status == EasyLoadingStatus.dismiss) {
_timer?.cancel();
}
});
}
#override
void dispose() {
_scrollController.dispose();
bloc.close();
super.dispose();
}
#override
Widget buildView(BuildContext context) {
return Scaffold(
body: BlocProvider<SubmitPersonalDetailsBloc>(
create: (_) => bloc,
child: BlocListener<SubmitPersonalDetailsBloc,
BaseState<PersonalDetailsState>>(
listener: (context, state) {
if (state is LoadingSubmitPersonalDetailsState) {
EasyLoading.show(status: 'Submitting Data');
}
if (state is SubmitPersonalDetailsSuccessState) {
setState(() {
submitPersonalDetailsResponseEntity =
state.submitPersonalDetailsResponseEntity;
});
if (submitPersonalDetailsResponseEntity!.responseCode == "00") {
EasyLoading.showSuccess('Done!');
//Navigate next screen
EasyLoading.dismiss();
}
} else if (state is SubmitPersonalDetailsFailedState) {
EasyLoading.showError(state.error); }
},
....
The problem is on Dependency Injection, Once it creates an instance the parameters don't change. So when navigating to the next screen have to reset that instance.
#override
void dispose() {
_scrollController.dispose();
bloc.close();
injection.resetLazySingleton<SubmitPersonalDetailsBloc>(); // here reset the instance
super.dispose();
}
You can try with this snippet
if (result.responseCode == APIResponse.RESPONSE_SUCCESS) {
yield SubmitPersonalDetailsSuccessState(
submitPersonalDetailsResponseEntity: r);
} else
yield SubmitPersonalDetailsFailedState(error: r.responseMsg);
}

initialize data once in initState and call the setState when data is ready causes exception

Since flutter calls the build method many times in different condition, to avoid getting the data many times, I initialize the data in initState.
I want to re-build the widget when the data is ready.
Here is my code :
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
Data data;
bool dataReady = false;
#override
void initState() {
super.initState();
getData(context).then((Data data) async {
setState(() {
dataReady= true;
});
});
}
#override
Widget build(BuildContext context) {
if (dataReady) {
return createMainContent(context);
} else {
return new Container();
}
}
}
However, it results in following exception :
inheritFromWidgetOfExactType(_InheritedProvider) or inheritFromElement() was called before _TestState.initState() completed.
May I know am I doing something wrong here?
When I add the following line to implementation of getData(context)
await Future.delayed(new Duration(milliseconds: 300));
the exception does not happen.
For everyone coming here at a later point
It is best to use the #override void didChangeDependencies () method of the State class.
From the docs
This method is also called immediately after initState. It is safe to call BuildContext.inheritFromWidgetOfExactType from this method.
But make sure to check if you have already performed your initialization
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (bloc == null) { // or else you end up creating multiple instances in this case.
bloc = BlocProvider<MyBloc>.of(context);
}
}
Edit: Better answer below.
Apparently, you cannot access getData(context) during initState (more concrete: before it completed).
The reason, so I believe, is that getData tries to look up an InheritedWidget ancestor up in the tree, but the tree is just now being built (your widget is created during the parent widget's build).
The obvious solution would be to delay getData's lookup to a later point in time. There are several ways to achieve that:
Delay the lookup to a later time. scheduleMicrotask should work fine.
Look it up during the first build call. You could have an isInitialized field set to false and in you build, something like:
if (!isInitialized) {
isInitialized = true;
// TODO: do the getData(...) stuff
}
an alternative is to put it inside PostFrameCallback which is between initState and Build.
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) => getData());
super.initState();
}
getData() async {
}
I moved my code to my build method from initState and it worked
class _TestState extends State<Test> {
Data data;
bool dataReady = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
getData(context).then((Data data) async {
setState(() {
dataReady= true;
});
});
if (dataReady) {
return createMainContent(context);
} else {
return new Container();
}
}
}

Android onResume() method equivalent in Flutter

I am working on a Flutter app and need to pop the screen. I tried initState() method but no luck. initState() gets called when I open a class for the first time.
Do we have an equivalent of Android onResume() method in Flutter?
Any ideas?
You can use the WidgetsBindingObserver and check the AppLifeCycleState like this example:
class YourWidgetState extends State<YourWidget> with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance?.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
//do your stuff
}
}
}
Take in mind that It will called every time you open the app or go the background and return to the app. (if your widget is active)
If you just want a listener when your Widget is loaded for first time, you can listen using addPostFrameCallback, like this example:
class YourWidgetState extends State<YourWidget> {
_onLayoutDone(_) {
//do your stuff
}
#override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback(_onLayoutDone);
super.initState();
}
}
Info : https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver-class.html
Update: Null safety compliance
If you go to another page, then is called when you comeback
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
).then((value) {
_refreshFirstPage();
});
You can accomplish this by registering a didChangeAppLifecycleState observer:
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(final AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
// ...your code goes here...
});
}
}
#override
Widget build(final BuildContext context) {
// ...your code goes here...
}
}
See WidgetsBindingObserver for more information.
Use focus_detector more information can see visibility_detector
Get notified every time your widget appears or disappears from the screen.
Similar to onResume()/onPause() on Android and viewDidAppear()/viewDidDisappear() on iOS.
Focus Detector fires callbacks for you whenever something happens to take or give your widget focus. Such an event might be, for instance, the user:
Navigating to/from another screen;
Turning the device’s screen on/off while your widget is visible;
Switching to/from another app while your widget is visible;
Scrolling your widget in/out the screen;
#override
Widget build(BuildContext context) =>
FocusDetector(
onFocusLost: () {
logger.i(
'Focus Lost.'
'\nTriggered when either [onVisibilityLost] or [onForegroundLost] '
'is called.'
'\nEquivalent to onPause() on Android or viewDidDisappear() on iOS.',
);
},
onFocusGained: () {
logger.i(
'Focus Gained.'
'\nTriggered when either [onVisibilityGained] or [onForegroundGained] '
'is called.'
'\nEquivalent to onResume() on Android or viewDidAppear() on iOS.',
);
},
onVisibilityLost: () {
logger.i(
'Visibility Lost.'
'\nIt means the widget is no longer visible within your app.',
);
},
onVisibilityGained: () {
logger.i(
'Visibility Gained.'
'\nIt means the widget is now visible within your app.',
);
},
onForegroundLost: () {
logger.i(
'Foreground Lost.'
'\nIt means, for example, that the user sent your app to the background by opening '
'another app or turned off the device\'s screen while your '
'widget was visible.',
);
},
onForegroundGained: () {
logger.i(
'Foreground Gained.'
'\nIt means, for example, that the user switched back to your app or turned the '
'device\'s screen back on while your widget was visible.',
);
},
child: Container(),
);

setState() called after dispose()

When I click the raised button, the timepicker is showing up. Now, if I wait 5 seconds, for example, and then confirm the time, this error will occur:
setState() called after dispose()
I literally see in the console how flutter is updating the parent widgets, but why? I don't do anything - I just wait 5 seconds?!
The example below will work in a normal project, however in my project which is quite more complex it won't work because Flutter is updating the states while I am waiting... What am I doing wrong? Does anyone have a guess at what it could be that Flutter is updating randomly in my more complex project and not in a simple project?
[UPDATE]
I took a second look at it and found out it is updating from the level on where my TabBar and TabBarView are.
Could it have to do something with the "with TickerProviderStateMixin" which I need for the TabBarView? Could it be that it causes the app to refresh regularly and randomly?
class DateTimeButton extends State<DateTimeButtonWidget> {
DateTime selectedDate = new DateTime.now();
Future initTimePicker() async {
final TimeOfDay picked = await showTimePicker(
context: context,
initialTime: new TimeOfDay(hour: selectedDate.hour, minute: selectedDate.minute),
);
if (picked != null) {
setState(() {
selectedDate = new DateTime(selectedDate.year, selectedDate.month, selectedDate.day, picked.hour, picked.minute);
});
}
}
#override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text("${selectedDate.hour} ${selectedDate.minute}"),
onPressed: () {
initTimePicker();
}
);
}
}
Just check boolean property mounted of the state class of your widget before calling setState().
if (this.mounted) {
setState(() {
// Your state change code goes here
});
}
Or even more clean approach
Override setState method in your StatelfulWidget class.
class DateTimeButton extends StatefulWidget {
#override
void setState(fn) {
if(mounted) {
super.setState(fn);
}
}
}
If it is an expected behavior that the Future completes when the widget already got disposed you can use
if (mounted) {
setState(() {
selectedDate = new DateTime(selectedDate.year, selectedDate.month, selectedDate.day, picked.hour, picked.minute);
});
}
Just write one line before setState()
if (!mounted) return;
and then
setState(() {
//Your code
});
I had the same problem and i solved changing the super constructor call order on initState():
Wrong code:
#override
void initState() {
foo_bar(); // call setState();
super.initState(); // then foo_bar()
}
Right code:
#override
void initState() {
super.initState();
foo_bar(); // first call super constructor then foo_bar that contains setState() call
}
To prevent the error from occurring, one can make use of the mounted property of the State class to ensure that a widget is mounted before settings its state:
// First Update data
if (!mounted) {
return;
}
setState(() { }
Try this
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text("${selectedDate.hour} ${selectedDate.minute}"),
onPressed: () async {
await initTimePicker();
}
);
}
class MountedState<T extends StatefulWidget> extends State<T> {
#override
Widget build(BuildContext context) {
return null;
}
#override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
}
Example
To prevent the error,Instead of using State use MountedState
class ExampleStatefulWidget extends StatefulWidget {
const ExampleStatefulWidget({Key key}) : super(key: key);
#override
_ExampleStatefulWidgetState createState() => _ExampleStatefulWidgetState();
}
class _ExampleStatefulWidgetState extends MountedState<ExampleStatefulWidget> {
....
}
I had this error when I mistakenly called super.initState before the variable. Check this:
#override
void initState() {
super.initState();
bloc = MainBloc();
}
Should be fixed as
#override
void initState() {
bloc = MainBloc();
super.initState();
}
The problem could occur when you have long asynchronous operation in stateful widget that could be closed/disposed before the operation finished.
Futures in Dart are not preemptive, so the only way is to check if a widget mounted before calling setState.
If you have a lot of widgets with asynchrony, adding ton of if (mounted) checks is tedious and an extension method might be useful
extension FlutterStateExt<T extends StatefulWidget> on State<T> {
void setStateIfMounted(VoidCallback fn) {
if (mounted) {
// ignore: invalid_use_of_protected_member
setState(fn);
}
}
}