Repository navigation
Calling dispose() on OverlayEntry throws an assertion error #102718
Description
An assertion error is thrown when dispose() is used after remove().
_overlayEntry?.remove();
_overlayEntry?.dispose();I think this is because _markDirty() is called from remove() in the next frame when dispose() has already been executed.
flutter/packages/flutter/lib/src/widgets/overlay.dart
Lines 154 to 158 in d873157
| if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { | |
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | |
| overlay._markDirty(); | |
| }); | |
| } else { |
The error is avoided if dispose() is also delayed to the next frame manually like below, whether it is good or not.
_overlayEntry?.remove();
SchedulerBinding.instance.addPostFrameCallback((_) => _overlayEntry?.dispose());I guess that Flutter devs probably think they should use dispose() when an OverlayEntry becomes no longer necessary as they usually do when they finish using a ChangeNotifier. The unexpected error has been confusing me. Should dispose() not used? (Is it enough to use only remove()?) If so, I think it should be documented.
Steps to Reproduce
- Execute
flutter runon the code sample below. - Press the FAB to show an overlay.
- Press the FAB again to hide the overlay.
Expected results:
No error.
Actual results:
The last step above throws an error, showing A OverlayEntry was used after being disposed.
Full output
======== Exception caught by widgets library =======================================================
The following assertion was thrown while finalizing the widget tree:
A OverlayEntry was used after being disposed.
Once you have called dispose() on a OverlayEntry, it can no longer be used.
When the exception was thrown, this was the stack:
#0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure> (package:flutter/src/foundation/change_notifier.dart:114:9)
#1 ChangeNotifier._debugAssertNotDisposed (package:flutter/src/foundation/change_notifier.dart:120:6)
#2 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:288:12)
#3 OverlayEntry._updateMounted (package:flutter/src/widgets/overlay.dart:130:5)
#4 _OverlayEntryWidgetState.dispose (package:flutter/src/widgets/overlay.dart:200:18)
#5 StatefulElement.unmount (package:flutter/src/widgets/framework.dart:4983:11)
#6 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:1926:13)
#7 ListIterable.forEach (dart:_internal/iterable.dart:39:13)
#8 _InactiveElements._unmountAll (package:flutter/src/widgets/framework.dart:1935:25)
#9 BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2519:15)
#10 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:2932:7)
#11 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:884:19)
#12 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#13 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#14 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#15 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:995:5)
#19 _invoke (dart:ui/hooks.dart:151:10)
#20 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#21 _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
====================================================================================================
Code sample
import 'package:flutter/material.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
OverlayEntry? _overlayEntry;
@override
void dispose() {
_hideOverlay();
super.dispose();
}
void _hideOverlay() {
_overlayEntry?.remove();
_overlayEntry?.dispose();
setState(() => _overlayEntry = null);
}
void _showOverlay() {
final entry = OverlayEntry(
builder: (_) => const Center(child: Text('Overlay')),
);
Overlay.of(context)!.insert(entry);
setState(() => _overlayEntry = entry);
}
@override
Widget build(BuildContext context) {
final isShown = _overlayEntry != null;
return Scaffold(
floatingActionButton: isShown
? FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: _hideOverlay,
)
: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: _showOverlay,
),
);
}
}Logs
PS D:\bug> flutter analyze
Analyzing bug...
No issues found! (ran in 1.4s)PS D:\bug> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.10.5, on Microsoft Windows [Version 10.0.22000.613], locale ja-JP)
[√] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe)
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.1.3)
[!] Android Studio (not installed)
[√] IntelliJ IDEA Ultimate Edition (version 2022.1)
[√] VS Code (version 1.66.2)
[√] Connected device (2 available)
[√] HTTP Host Availability
! Doctor found issues in 2 categories.
Activity
darshankawar commentedon Apr 28, 2022
@kaboc
I see two things here.
remove()beforedispose()which probably is causing the error you are seeing.Once you have called dispose() on a OverlayEntry, it can no longer be used., perremove()method:After calling
remove(), the UI will not update until next frame, but then callingdispose()which permanently removes the object from tree.Although not exactly similar, but can you try something like referred here and see updating it accordingly in your use case helps ? rrousselGit/provider#168 (comment)
kaboc commentedon Apr 28, 2022
@darshankawar
Thanks, but I already know the reason why the error occurs. I filed the issue because it seemed (almost) like a bug that just calling
dispose()in an ordinary way caused an error, and also because the documentation could be improved. I mean, this is not a request for help but a bug report. It is error-prone thatremove()anddispose()can be executed in reverse order.darshankawar commentedon Apr 28, 2022
Thanks for the feedback. I see that the usage of
remove()anddispose()could be documented to avoid the scenario, or maybe a better error message to indicate usage of both.console error log
Keeping it open and labeling to get further insights from the team on correct usage of both methods and their order and whether the same need to be documented as well or not.
stable, master flutter doctor -v
github-actions commentedon May 16, 2022
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.