Close `showModalBottomSheet` after a timeout - flutter

I tried to close showModalBottomSheet widget through an event listener.
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (newcontext) {
return MyBottomDialog();
});
I tried to close this through an event listener:
class _MyBottomDialogState extends State<MyBottomDialog> {
...
...
void initState() {
_myEventStream.listen((state) {
if (state == 'timeout') {
Navigator.of(context, rootNavigator: true).pop();
}
});
super.initState();
}
TypeError: Cannot read property 'findAncestorStateOfType' of null
.
.
.
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a
reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in
the widget's didChangeDependencies() method.
How do I achieve this?

The stream class actually comes with a timeout method. You just have to feed it a duration and if it exceeds that duration it throws a TimeoutException. Wrap the execution in a try block and call the close modal sheet method from the on TimeoutException catch (e) block perhaps.

May be I didn't capture properly what the error is saying.
Even after closing the dialog the stream subscription persists. So the handler is called even if the dialog is not open.
Adding _streamSubscription.cancel() in the widget's dispose method helps solve this problem.

Related

flutter/dart: Looking up a deactivated widget's ancestor is unsafe

I have used persistent_bottom_nav_bar 5.0.2 package and from the first tab page which is homepage I navigate to further some pages and than finally I use the following navigator to come back to this page on same tab
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
(_) => false,
);
I Navigate successfully but when I click on this tab it throw this exception
════════ Exception caught by gesture ═══════════════════════════════════════════
The following assertion was thrown while handling a gesture:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
I even don't know the reason as well as solution so any help will be appreciated.
You are removing all the screens below HomePage from the stack.
pushAndRemoveUntil as name suggests pushes a new Screen in stack and removes all the other screens from stack as a result theirs no screen available to pop except the current HomePage.
Instead use only
if (mounted) {
Navigator.of(context).push(
CupertinoPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
(_) => false,
);
}

Prevent memory leak in Flutter

I was trying to make my function works with a Timer to fetch a list of actions from Database and display them without leaving the screen and re-enter again.
I tried the following :
void initState() {
super.initState();
Timer.periodic(timeDelay, (Timer t) => fetchFournisseurs());
}
It's working now , but my debug console is showing some infos :
State.setState.<anonymous closure> (package:flutter/src/widgets/fr<…>
[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: setState() called after dispose(): _listeDocumentState#c0f9f(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().
Is there a better way to make it and prevent memory leak or anything bad ?
Thank you.
All of the information is in the error message itself.
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).
For example if you navigate to a page containing this widget, and then close that page, the widget would no longer be in the widget tree, but the Timer you created is still running periodically.
This error can occur when code calls setState() from a timer or an animation callback.
Presumably fetchFournisseurs is calling setState.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback.
In other words your state class should do something along these lines.
class _ExampleState extends State<Example> {
final Duration timeDelay = ...;
// Create a variable to hold the timer.
Timer? _timer;
#override
void initState() {
super.initState();
// Assign timer to the variable.
_timer = Timer.periodic(timeDelay, (Timer t) => fetchFournisseurs());
}
#override
void dispose() {
// Cancel the timer in the dispose() callback.
_timer?.cancel();
super.dispose();
}
void fetchFournisseurs() {
// presumably fetchFournisseurs calls setState at some point.
setState(() {
...
});
}
#override
Widget build(BuildContext context) {
return ...;
}
}
Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
Which means to do the following.
void fetchFournisseurs() {
// check if the widget is mounted before proceeding
if (!mounted) return;
setState(() {
...
});
}
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().
Another reminder to cancel the Timer in dispose to avoid memory leaks.

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

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.

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.

How to invoke destructor when launching another page on flutter navigator?

Navigator.push(
context,
MaterialPageRoute(builder: (context) => MyNewPage()),
)
This loads a new page.
How can I make it such that it calls a destructor on the current page so it can clear its things properly?
I tried adding a dispose method but it isn't executed when I change pages.
To clear whole current context you can use:
Navigator.of(context)
.pushAndRemoveUntil(MaterialPageRoute(builder: (context) =>
MyNewPage()), (Route<dynamic> route) => false)
Make sure to use a RoutePredicate, that always returns false (Route<dynamic> route) => false. In this situation it removes all of the routes except for the new MyNewPage() route pushed in stack.
In case you need to execute some cleanup function
You can invoke it overriding dispose() method:
it is called when this object is removed from the tree permanently. Keep in mind, that it is an error to call setState here and make sure to end your method with a call to super.dispose() when overriding it.
#override
void dispose() {
// Your function.
super.dispose();
}
You can invoke it overriding initState() method of route you are pushing in a stack: It is called when this object is inserted into the tree. If you override this, make sure your method starts with a call to super.initState().
#override
void initState() {
super.initState();
// Your function.
}
If you need a BuildContext for your cleanup function you can use didChangeDependencies(): It is called when a dependency of this State object changes and also immediately after initState, it is safe to use BuildContext here. Subclasses rarely override this method because the framework always calls build after a dependency changes. Some subclasses do override this method because they need to do some expensive work (e.g., network fetches) when their dependencies change, and that work would be too expensive to do for every build.
#override
void didChangeDependencies() {
// Your function.
super.didChangeDependencies();
}
Getx package also has variety of ways to insert a Middleware function:
Redirect : This function will be called when the page of the called route is being searched for. It takes RouteSettings as a result to redirect to. Or give it null and there will be no redirecting.
GetPage redirect( ) {
final authService = Get.find<AuthService>();
return authService.authed.value ? null : RouteSettings(name: '/login')
}
onPageCalled : This function will be called when this Page is called before anything created you can use it to change something about the page or give it new page.
GetPage onPageCalled(GetPage page) {
final authService = Get.find<AuthService>();
return page.copyWith(title: 'Welcome ${authService.UserName}');
}
OnBindingsStart : This function will be called right before the Bindings are initialize. Here you can change Bindings for this page.
List<Bindings> onBindingsStart(List<Bindings> bindings) {
final authService = Get.find<AuthService>();
if (authService.isAdmin) {
bindings.add(AdminBinding());
}
return bindings;
}
OnPageBuildStart : This function will be called right after the Bindings are initialize. Here you can do something after that you created the bindings and before creating the page widget.
GetPageBuilder onPageBuildStart(GetPageBuilder page) {
print('bindings are ready');
return page;
}
OnPageBuilt : This function will be called right after the GetPage.page function is called and will give you the result of the function. and take the widget that will be showed.
OnPageDispose : This function will be called right after disposing all the related objects (Controllers, views, ...) of the page.
You can just use the Navigator.pushReplacement as follows:
Navigator.pushReplacement(
context, MaterialPageRoute(builder: (BuildContext context) => MyHomePage()));
this will push what you tell it and dispose the previous one once the new one finishes loading.