showModalBottomSheet and Unhandled Exception: setState() called after dispose() on parent widget - flutter

Context:
I have a modal bottom sheet that pops up, upon selection of Camera/Gallery acquires/selects an image XFile and returns it for processing (uploading) done with the help of image_picker.
This is done with a sample line:
ListTile(
onTap: () {
// definition: Future<XFile?> showCamera(IdPhotoOrientation orientation);
showCamera(orientation).then((value) => Navigator.of(context).pop<XFile?>(value));
},
...
),
Picking an image with showModalBottomSheet is done by returning the selected XFile and processing it on a chained function _handleFile(XFile, enum):
return showModalBottomSheet<XFile?>(
context: context,
builder: (context) {
return SingleChildScrollView(
child: ListBody(
children: [
...
ListTile(
onTap: () {
showCamera(orientation).then((value) => Navigator.of(context).pop<XFile?>(value));
},
leading: Icon(Icons.camera),
title: Text("From Camera"),
),
...
],
),
);
},
).then((value) => _handleFile(value, orientation));
What is the problem:
While processing file in _handle(XFile?, int), I need to update the state of the app to show progress bar updates, circular indicators, uploading status, etc.
Future<void> _handleFile(XFile? xfile, int orientation) {
if (xfile == null) {
return Future.value();
}
// store locally with Uploading Status
var imageService = locator<ImageService>();
setState(() { <-------- offending line (ui_partner_registration_id_photos.dart:103:5)
remoteImageStatus[xfile] = UploadStatus.Uploading;
images[orientation] = xfile;
});
// Upload and update result / error
return imageService.uploadIDPhoto(File(xfile.path), orientation).then((value) {
setState(() {
idPhotos[orientation] = value;
remoteImageStatus[xfile] = UploadStatus.Done;
});
print("Uploaded [${xfile.path}]");
}).onError((error, stackTrace) {
print("Error uploading image");
print(stackTrace);
setState(() {
remoteImageStatus[xfile] = UploadStatus.Error;
});
});
}
Why is this a problem?
setState() cannot be called on a stateful widget that is no longer visible/active/in-focus which is now the case for the showModalBottomSheet. That being said, after calling Navigator.pop() this should no longer be the case as the parent stateful widget is now in focus, this is causing my confusion.
(temporary) Solution
A temporary solution (which does not give exactly the desired result) is to add a mounted check as described here with an example here:
if (mounted) {
setState((){
// perform actions
})
}
StackTrace:
[VERBOSE-2:ui_dart_state.cc(199)] Unhandled Exception: setState() called after dispose(): _RegisterIDPhotosState#b75f9(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
#0 State.setState.<anonymous closure> (package:flutter/src/widgets/framework.dart:1052:9)
#1 State.setState (package:flutter/src/widgets/framework.dart:1087:6)
#2 _RegisterIDPhotosState._handleFile (my-awesome-app/viewcontrollers/register/partner/ui_partner_registration_id_photos.dart:103:5)
#3 _RegisterIDPhotosState.pickImageWithModalPopup.<anonymous closure> (package:my-awesome-app/viewcontrollers/register/partner/ui_partner_registration_id_photos.dart:188:23)
#4 _rootRunUnary (dart:async/zone.dart:1362:47)
#5 _CustomZone.runUnary (dart:async/zone.dart:1265:19)
<asynchronous suspension>
Question:
After selecting a file and starting the upload process, how can I call setState() as in the example of _handleFile(XFile?, int) above?

Refactor that logic to a ChangeNotifier or ValueNotifier higher up in the widget tree and make your Widgets use it to share state between them see the official docs for a more in thorough description.
The setState approach won't work because you are handling 2 different widgets there. You state:
"That being said, after calling Navigator.pop() this should no longer be the case as the parent stateful widget is now in focus, this is causing my confusion."
Whats causing your confusion is that setState is not a global callback which is executed in the currently focused Sateful Widget, setState is nothing more than executing your callback and calling markNeedsBuild for the specific widget in which the setState call was made, which in your case is no longer mounted.
That being said the docs I pointed you to is a recommended way of sharing state in a Flutter app.

Related

Memory leak when using setState to overwrite value flutter

I have some problem. When I try to overwrite a value using setState, infinite loop happening and a warning about memory leaks. I dont know why its happening, and I tried to put if(mounted), but its still not working. Any advice for this?
ListView.separated(
separatorBuilder: (BuildContext context, int i) => Divider(color: Colors.grey[400]),
itemCount: listJadwalDokter.length,
itemBuilder: (context, index) {
JadwalDokter jadwal = listJadwalDokter[index];
if(jadwal.hari < now.weekday)
for(int test = 0;test <jadwal.jadwalPraktek.length; test++) {
cutiService.cekCuti(jadwal.kodeDokter + "." + listTanggalFormatKode[jadwal.hari + 7] + jadwal.jadwalPraktek[test].jam.substring(0, 2)).then((value) {
if (value)
if(mounted)
setState(() {
//when infinite loop happened
jadwal.jadwalPraktek[test].jam =jadwal.jadwalPraktek[test].jam.substring(0, 11) + "\n(Sedang Cuti)";
});
});
}
}
)
This ListView is inside an AlertDialog that wrapped in StatefullBuilder so it can update the view when setState is called.
This is the error log
E/flutter ( 6791): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter ( 6791): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
E/flutter ( 6791): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

Flutter: A FocusNode was used after being disposed

I have a simple Flutter form inside a stateful widget with a bunch of text fields to capture credit card details. And when I show this form, tap the first field, type something in it, then tap the second field, the focus is not transferred to the second field, the cursor stays in the first one, even though both fields appear focused (border is visible on both) and when I type, it goes in the first field. The only way to type something into the second field is to long tap it as if I wanted to paste something in it. And in the console, I see this:
[VERBOSE-2:ui_dart_state.cc(199)] Unhandled Exception: A FocusNode was used after being disposed.
Once you have called dispose() on a FocusNode, it can no longer be used.
#0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure> (package:flutter/src/foundation/change_notifier.dart:117:9)
#1 ChangeNotifier._debugAssertNotDisposed (package:flutter/src/foundation/change_notifier.dart:123:6)
#2 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:234:12)
#3 FocusNode._notify (package:flutter/src/widgets/focus_manager.dart:1052:5)
#4 FocusManager._applyFocusChange (package:flutter/src/widgets/focus_manager.dart:1800:12)
#5 _rootRun (dart:async/zone.dart:1346:47)
#6 _CustomZone.run (dart:async/zone.dart:1258:19)
#7 _CustomZone.runGuarded (dart:async/zone.dart:1162:7)
The thing is that none of the fields have a focus node-set, so it seems something wrong is happening inside the Flutter form state. Has anybody seen this?
Note that for me, it occurs on the web and mobile alike, but not consistently.
Here is the code of the problematic form.
And here is a screenshot illustrating 2 fields having focus at the same time, and the cursor is stuck in the first one. And when I type something on the keyboard, it's directed at the first field. This is running on a physical iPhone device.
And here is the function that is called as the onPressed of an ElevatedButton to show this form:
Future<void> _addPaymentMethod(BuildContext context) async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
body: CardForm(),
),
),
);
}
I think the problem is using Focus widget.
You should be using FocusNode and pass this FocusNode as one of the named arguments of TextField (your CardCvcFormField).
You can attach a listener to the FocusNode and get the focus visibility.
FocusNode _cvcFocusNode;
#override
void initState() {
super.initState();
_cvcFocusNode = FocusNode();
_cvcFocusNode.addListener(_onCvcFormFieldFocusChanged);
}
void _onCvcFormFieldFocusChanged() {
setState(() => cvcHasFocus = _cvcFocusNode?.hasFocus ?? false);
}
#override
void dispose() {
_cvcFocusNode?.removeListener(_onCvcFormFieldFocusChanged);
_cvcFocusNode?.dispose();
_cvcFocusNode = null;
super.dispose();
}
Upgrade Flutter version currently its working fine with 3.7.2

Flutter: Consider canceling any active work during "dispose" when internet changes its state

I am getting the following message when internet goes off.
E/flutter (26162): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
E/flutter (26162): Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
It is showing the message from this section of my code.
#override
void initState() {
super.initState();
try {
InternetAddress.lookup('google.com').then((result) {
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
// internet conn available
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) =>
(Constants.prefsMobile.getString("mobile") == null
? Login()
// : SignupPayoutPassword(signupdata: [])),
: Home(signindata: signinData)),
));
} else {
// no conn
_showdialog();
}
}).catchError((error) {
// no conn
_showdialog();
});
} on SocketException catch (_) {
// no internet
_showdialog();
}
Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult connresult) {
if (connresult == ConnectivityResult.none) {
} else if (previous == ConnectivityResult.none) {
// internet conn
Navigator.of(context).pop();
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) =>
(Constants.prefsMobile.getString("mobile") == null
? Login()
: Home(signindata: signinData)),
));
}
previous = connresult;
});
}
I have not used any dispose method for this. If any one know please let me know how can I solve this problem. How to dispose. I am getting a crash report after my app close as follows
E/AndroidRuntime( 8064): java.lang.RuntimeException: Unable to destroy activity {com.example.aa_store/com.example.aa_store.MainActivity}: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter activity
is this crash message for the above problem? Please help.
Please use.
#override
void dispose() {
Connectivity().onConnectivityChanged.cancel();
super.dispose();
}
Better, define your stream outside the initState:
Stream _connectivityStream = Connectivity().onConnectivityChanged;
and in dispose use _connectivityStream.cancel();.
The error means that you instantiated a stream, which on changes of events, triggers build changes. This stream is setup during initState, meaning when the widget is first created. Connectivity().onConnectivityChanged.listen(....etc).
But you never tell flutter to cancel listening to this stream when the widget is disposed.
This is the role of the dispose method. Similar to how you want logic to be performed when the widget is built, you use initState, you should also tell it when you are no longer interested in these changes in logic.
Failing to do so, will result in the error you are having, aside from memory leaks also.
This is the translation of the error This widget has been unmounted, so the State no longer has a context (and should be considered defunct). which you posted. "Hey, this widget isn't in the tree anymore, its state is not mounted, I can't rebuild it, and you need to pay attention to it.
Please consider using the dispose method for these Flutter elements, not to mention all of them, but from the top of my mind:
AnimationControllers.
Timers.
Streams listeners.

Using Navigator inside setState

I have a list of Strings (called questions).
I create a Text widget based on the current string in the list.
I have an index int that increases every time a button is pressed.
I increase the current index by 1 in the setState method.
I need to navigate to a different page when the current index reaches the length of the String list.
Otherwise, I will get an RangeError naturally.
setState(() {
this.currentIndex++;
if(this.currentIndex == questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
});
Now based on the code above, the error page appears and disappears quickly.
It is replaced by the Loser() page quickly.
Why is that?
And how can I navigate to the Loser() page without the error page showing?
Edit: As requested, the error message:
════════ Exception caught by widgets library ═══════════════════════════════════
The following RangeError was thrown building LandingPage(dirty, state: _LandingPageState#a8efe):
RangeError (index): Invalid value: Not in inclusive range 0..10: 11
The relevant error-causing widget was
LandingPage
lib/main.dart:21
When the exception was thrown, this was the stack
#0 List.[] (dart:core-patch/growable_array.dart:153:60)
#1 _LandingPageState.build
package:testing_http_package/landing_page.dart:88
#2 StatefulElement.build
package:flutter/…/widgets/framework.dart:4628
#3 ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4511
#4 StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:4684
...
════════════════════════════════════════════════════════════════════════════════
Edit: The widget I think in the build method that is causing the error:
child: Center(
child: Text(
questions[currentIndex], // This line
style: style,
textAlign: TextAlign.center,
),
),
),
Shouldn't the setState method go straight to the page before rerunning the build method?
Edit: I added the didChangeDependencies method as per #Nuts suggestion but it did not work. Now only the error page appears and it does not proceeds to the other page:
#override
void didChangeDependencies() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(this.currentIndex == questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
});
super.didChangeDependencies();
}
With setState - you are rebuilding the whole widget and while doing it - navigating. So you are trying to rebuild widgets with invalid params (in your case index)
this.currentIndex++;
if(this.currentIndex => questions.length) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Loser()),
);
}
else setState(() {}); // if currentIndex is valid, just rebuild

Flutter Navigation from Firestore Flag

Diagram of what I want to do]1
I want to navigate to Screen2 from screen1 when the value of screen2_flag changes to true (on firebase cloud firestore ) ,
I have screen 1 as shown in the diagram ,which is working perfectly fine until the value of screen2_flag is false , when I change the value of flag from false to true .
void initState() {
super.initState();
DocumentReference reference =
Firestore.instance.collection('myColection').document('myDoc');
reference.snapshots().listen((querySnapshot) {
print('got sanpshot' + querySnapshot.data['screen2_flag'].toString());
if (querySnapshot.data['screen2_flag'].toString() == 'true') {
Navigator.pushNamed(context, screen2.id);
}
});
Widget build(BuildContext context) {
// my code
...
}
}
I am able to see screen2 on my emulator. But Getting below error on Console
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: setState() callback argument returned a Future.
E/flutter (24551): The setState() method on _WaitingRoomState#25dfa was called with a closure or method that returned a Future. Maybe it is marked as "async".
E/flutter (24551): Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().
E/flutter (24551): #0 State.setState. (package:flutter/src/widgets/framework.dart:1151:9)
E/flutter (24551): #1 State.setState (package:flutter/src/widgets/framework.dart:1167:6)
The error told you the problem:
setState() callback argument returned a Future
The setState() method on _WaitingRoomState#25dfa was called with a
closure or method that returned a Future
and told you also the solution:
Instead of performing asynchronous work inside a call to setState(),
first execute the work (without updating the widget state), and then
synchronously update the state inside a call to setState()
So you do the async call,await for it until you get the flag value(as I suppose because you didn't show that part of code), then call setState() with the new values updated.