Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
0b3c5d12
Unverified
Commit
0b3c5d12
authored
Apr 29, 2022
by
LongCatIsLooong
Committed by
GitHub
Apr 29, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Defer the OverlayEntry listenable disposal until its widget is unmounted (#102794)
parent
cdb4831d
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
167 additions
and
13 deletions
+167
-13
overlay.dart
packages/flutter/lib/src/widgets/overlay.dart
+58
-13
overlay_test.dart
packages/flutter/test/widgets/overlay_test.dart
+109
-0
No files found.
packages/flutter/lib/src/widgets/overlay.dart
View file @
0b3c5d12
...
...
@@ -45,9 +45,12 @@ import 'ticker_provider.dart';
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
/// call [State.setState], the user's battery will be drained unnecessarily.
///
/// [OverlayEntry] is a [
ChangeNotifier
] that notifies when the widget built by
/// [OverlayEntry] is a [
Listenable
] that notifies when the widget built by
/// [builder] is mounted or unmounted, whose exact state can be queried by
/// [mounted].
/// [mounted]. After the owner of the [OverlayEntry] calls [remove] and then
/// [dispose], the widget may not be immediately removed from the widget tree.
/// As a result listeners of the [OverlayEntry] can get notified for one last
/// time after the [dispose] call, when the widget is eventually unmounted.
///
/// See also:
///
...
...
@@ -55,7 +58,7 @@ import 'ticker_provider.dart';
/// * [OverlayState]
/// * [WidgetsApp]
/// * [MaterialApp]
class
OverlayEntry
extends
ChangeNotifier
{
class
OverlayEntry
implements
Listenable
{
/// Creates an overlay entry.
///
/// To insert the entry into an [Overlay], first find the overlay using
...
...
@@ -86,6 +89,7 @@ class OverlayEntry extends ChangeNotifier {
bool
get
opaque
=>
_opaque
;
bool
_opaque
;
set
opaque
(
bool
value
)
{
assert
(!
_disposedByOwner
);
if
(
_opaque
==
value
)
return
;
_opaque
=
value
;
...
...
@@ -109,6 +113,7 @@ class OverlayEntry extends ChangeNotifier {
bool
get
maintainState
=>
_maintainState
;
bool
_maintainState
;
set
maintainState
(
bool
value
)
{
assert
(!
_disposedByOwner
);
assert
(
_maintainState
!=
null
);
if
(
_maintainState
==
value
)
return
;
...
...
@@ -120,14 +125,21 @@ class OverlayEntry extends ChangeNotifier {
/// Whether the [OverlayEntry] is currently mounted in the widget tree.
///
/// The [OverlayEntry] notifies its listeners when this value changes.
bool
get
mounted
=>
_mounted
;
bool
_mounted
=
false
;
void
_updateMounted
(
bool
value
)
{
if
(
value
==
_mounted
)
{
return
;
}
_mounted
=
value
;
notifyListeners
();
bool
get
mounted
=>
_overlayStateMounted
.
value
;
/// Whether the `_OverlayState`s built using this [OverlayEntry] is currently
/// mounted.
final
ValueNotifier
<
bool
>
_overlayStateMounted
=
ValueNotifier
<
bool
>(
false
);
@override
void
addListener
(
VoidCallback
listener
)
{
assert
(!
_disposedByOwner
);
_overlayStateMounted
.
addListener
(
listener
);
}
@override
void
removeListener
(
VoidCallback
listener
)
{
_overlayStateMounted
.
removeListener
(
listener
);
}
OverlayState
?
_overlay
;
...
...
@@ -145,6 +157,7 @@ class OverlayEntry extends ChangeNotifier {
/// until the next frame (i.e. many milliseconds later).
void
remove
()
{
assert
(
_overlay
!=
null
);
assert
(!
_disposedByOwner
);
final
OverlayState
overlay
=
_overlay
!;
_overlay
=
null
;
if
(!
overlay
.
mounted
)
...
...
@@ -164,9 +177,40 @@ class OverlayEntry extends ChangeNotifier {
///
/// You need to call this function if the output of [builder] has changed.
void
markNeedsBuild
()
{
assert
(!
_disposedByOwner
);
_key
.
currentState
?.
_markNeedsBuild
();
}
void
_didUnmount
()
{
assert
(!
mounted
);
if
(
_disposedByOwner
)
{
_overlayStateMounted
.
dispose
();
}
}
bool
_disposedByOwner
=
false
;
/// Discards any resources used by this [OverlayEntry].
///
/// This method must be called after [remove] if the [OverlayEntry] is
/// inserted into an [Overlay].
///
/// After this is called, the object is not in a usable state and should be
/// discarded (calls to [addListener] will throw after the object is disposed).
/// However, the listeners registered may not be immediately released until
/// the widget built using this [OverlayEntry] is unmounted from the widget
/// tree.
///
/// This method should only be called by the object's owner.
void
dispose
()
{
assert
(!
_disposedByOwner
);
assert
(
_overlay
==
null
,
'An OverlayEntry must first be removed from the Overlay before dispose is called.'
);
_disposedByOwner
=
true
;
if
(!
mounted
)
{
_overlayStateMounted
.
dispose
();
}
}
@override
String
toString
()
=>
'
${describeIdentity(this)}
(opaque:
$opaque
; maintainState:
$maintainState
)'
;
}
...
...
@@ -192,12 +236,13 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
@override
void
initState
()
{
super
.
initState
();
widget
.
entry
.
_
updateMounted
(
true
)
;
widget
.
entry
.
_
overlayStateMounted
.
value
=
true
;
}
@override
void
dispose
()
{
widget
.
entry
.
_updateMounted
(
false
);
widget
.
entry
.
_overlayStateMounted
.
value
=
false
;
widget
.
entry
.
_didUnmount
();
super
.
dispose
();
}
...
...
packages/flutter/test/widgets/overlay_test.dart
View file @
0b3c5d12
...
...
@@ -1057,6 +1057,115 @@ void main() {
expect
(
renderObject
.
clipBehavior
,
clip
);
}
});
group
(
'OverlayEntry listenable'
,
()
{
final
GlobalKey
overlayKey
=
GlobalKey
();
final
Widget
emptyOverlay
=
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Overlay
(
key:
overlayKey
),
);
testWidgets
(
'mounted state can be listened'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
emptyOverlay
);
final
OverlayState
overlay
=
overlayKey
.
currentState
!
as
OverlayState
;
final
List
<
bool
>
mountedLog
=
<
bool
>[];
final
OverlayEntry
entry
=
OverlayEntry
(
builder:
(
BuildContext
context
)
=>
Container
(),
);
entry
.
addListener
(()
{
mountedLog
.
add
(
entry
.
mounted
);
});
overlay
.
insert
(
entry
);
expect
(
mountedLog
,
isEmpty
);
// Pump a frame. The Overlay entry will be mounted.
await
tester
.
pump
();
expect
(
mountedLog
,
<
bool
>[
true
]);
entry
.
remove
();
expect
(
mountedLog
,
<
bool
>[
true
]);
await
tester
.
pump
();
expect
(
mountedLog
,
<
bool
>[
true
,
false
]);
// Insert & remove again.
overlay
.
insert
(
entry
);
await
tester
.
pump
();
entry
.
remove
();
await
tester
.
pump
();
expect
(
mountedLog
,
<
bool
>[
true
,
false
,
true
,
false
]);
});
testWidgets
(
'throw if disposed before removal'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
emptyOverlay
);
final
OverlayState
overlay
=
overlayKey
.
currentState
!
as
OverlayState
;
final
OverlayEntry
entry
=
OverlayEntry
(
builder:
(
BuildContext
context
)
=>
Container
(),
);
overlay
.
insert
(
entry
);
Object
?
error
;
try
{
entry
.
dispose
();
}
catch
(
e
)
{
error
=
e
;
}
expect
(
error
,
isAssertionError
);
});
test
(
'dispose works'
,
()
{
final
OverlayEntry
entry
=
OverlayEntry
(
builder:
(
BuildContext
context
)
=>
Container
(),
);
entry
.
dispose
();
Object
?
error
;
try
{
entry
.
addListener
(()
{
});
}
catch
(
e
)
{
error
=
e
;
}
expect
(
error
,
isAssertionError
);
});
testWidgets
(
'delayed dispose'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
emptyOverlay
);
final
OverlayState
overlay
=
overlayKey
.
currentState
!
as
OverlayState
;
final
List
<
bool
>
mountedLog
=
<
bool
>[];
final
OverlayEntry
entry
=
OverlayEntry
(
builder:
(
BuildContext
context
)
=>
Container
(),
);
entry
.
addListener
(()
{
mountedLog
.
add
(
entry
.
mounted
);
});
overlay
.
insert
(
entry
);
await
tester
.
pump
();
expect
(
mountedLog
,
<
bool
>[
true
]);
entry
.
remove
();
// Call dispose on the entry. The listeners should be notified for one
// last time after this.
entry
.
dispose
();
expect
(
mountedLog
,
<
bool
>[
true
]);
await
tester
.
pump
();
expect
(
mountedLog
,
<
bool
>[
true
,
false
]);
expect
(
tester
.
takeException
(),
isNull
);
// The entry is no longer usable.
Object
?
error
;
try
{
entry
.
addListener
(()
{
});
}
catch
(
e
)
{
error
=
e
;
}
expect
(
error
,
isAssertionError
);
});
});
}
class
StatefulTestWidget
extends
StatefulWidget
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment