Skip to content

Commit 75b746e

Browse files
committed
publish to 5.0.0 and fix issues #280 #282 #284 #286 #288
- fix: avoid calling OverlayEntry.markNeedsBuild after dispose (#282) - fix: guard toast RenderBox access to prevent "RenderBox was not laid out" (#280) - fix: correct dialog onBack interception logic (#284) - fix: harden showAttach/bindWidget lifecycle and geometry checks (#288) - test: add regression test for bindPage close on route replace (#286) - chore: update CHANGELOG for 5.0.0 with issue links BREAKING CHANGE: adjust dialog onBack interception behavior to match expected handling in #284. Fixes #280 Fixes #282 Fixes #284 Fixes #286 Fixes #288
1 parent 0ff8c2f commit 75b746e

File tree

7 files changed

+218
-78
lines changed

7 files changed

+218
-78
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# [5.0.0]
2+
* Breaking change: Align `onBack` interception behavior for dialog/attach. When `onBack` returns `true`, the back event is always intercepted. Fix [#284](https://github.com/fluttercandies/flutter_smart_dialog/issues/284)
3+
* Fix [#282](https://github.com/fluttercandies/flutter_smart_dialog/issues/282): avoid calling `OverlayEntry.markNeedsBuild` after the entry is disposed during rapid toast updates
4+
* Fix [#280](https://github.com/fluttercandies/flutter_smart_dialog/issues/280): harden toast layout measurement to avoid `Bad state: RenderBox was not laid out` during page transitions
5+
* Fix [#288](https://github.com/fluttercandies/flutter_smart_dialog/issues/288): harden `showAttach` + `bindWidget` lifecycle/geometry checks to avoid inactive-element renderObject errors and invalid transform matrix assertions
6+
17
# [4.9.8+x]
28
* SmartDialog.config.checkExist() adjust to SmartDialog.checkExist()
39
* Fix [#209](https://github.com/fluttercandies/flutter_smart_dialog/issues/209)
@@ -275,4 +281,4 @@
275281
# [0.0.x]
276282

277283
* dealing with the problem of package name
278-
* init
284+
* init

lib/src/helper/monitor_widget_helper.dart

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,17 @@ class MonitorWidgetHelper {
3030
prohibitMonitor = true;
3131
var removeList = <DialogInfo>[];
3232
for (var item in monitorDialogQueue) {
33-
try {
34-
var context = item.bindWidget;
35-
if (context == null) {
36-
throw Error();
37-
}
38-
_calculate(context, item);
39-
} catch (e) {
33+
final context = item.bindWidget;
34+
if (!_isContextMounted(context)) {
4035
removeList.add(item);
4136
SmartLog.d(
4237
"The element(hashcode: ${item.bindWidget.hashCode}) is recycled and"
4338
" the 'bindWidget' dialog dismiss automatically",
4439
);
40+
continue;
4541
}
42+
43+
_calculate(context!, item);
4644
}
4745
for (var i = removeList.length; i > 0; i--) {
4846
DialogProxy.instance.dismiss(
@@ -55,9 +53,10 @@ class MonitorWidgetHelper {
5553
}
5654

5755
void _calculate(BuildContext context, DialogInfo item) {
58-
var renderObject = context.findRenderObject() as RenderBox?;
56+
var renderObject = _safeRenderBox(context);
5957
if (renderObject == null) {
60-
throw Error();
58+
item.dialog.hide();
59+
return;
6160
}
6261

6362
if (RouteRecord.curRoute == item.route) {
@@ -69,28 +68,46 @@ class MonitorWidgetHelper {
6968
_handleDialog(renderObject, item);
7069
}
7170
}
72-
// var viewport = RenderAbstractViewport.of(renderObject);
73-
// var revealedOffset = viewport?.getOffsetToReveal(renderObject, 0.0);
74-
// if (revealedOffset != null) {
75-
// // NonPage Scene
76-
// handleDialog();
77-
// } else {
78-
// // Page Scene
79-
// if (!item.bindPage) {
80-
// handleDialog();
81-
// }
82-
// }
8371
}
8472

85-
_handleDialog(RenderBox renderObject, DialogInfo item) {
86-
var selfOffset = renderObject.localToGlobal(Offset.zero);
87-
if (selfOffset.dx < 0 ||
88-
selfOffset.dy < 0 ||
89-
selfOffset.dx.isNaN ||
90-
selfOffset.dy.isNaN) {
73+
void _handleDialog(RenderBox renderObject, DialogInfo item) {
74+
try {
75+
var selfOffset = renderObject.localToGlobal(Offset.zero);
76+
if (selfOffset.dx < 0 ||
77+
selfOffset.dy < 0 ||
78+
selfOffset.dx.isNaN ||
79+
selfOffset.dy.isNaN ||
80+
selfOffset.dx.isInfinite ||
81+
selfOffset.dy.isInfinite) {
82+
item.dialog.hide();
83+
} else {
84+
item.dialog.appear();
85+
}
86+
} catch (_) {
9187
item.dialog.hide();
92-
} else {
93-
item.dialog.appear();
9488
}
9589
}
90+
91+
bool _isContextMounted(BuildContext? context) {
92+
if (context == null) {
93+
return false;
94+
}
95+
if (context is Element) {
96+
return context.mounted;
97+
}
98+
return true;
99+
}
100+
101+
RenderBox? _safeRenderBox(BuildContext context) {
102+
try {
103+
var renderObject = context.findRenderObject();
104+
if (renderObject is RenderBox &&
105+
renderObject.attached &&
106+
renderObject.hasSize) {
107+
return renderObject;
108+
}
109+
} catch (_) {}
110+
111+
return null;
112+
}
96113
}

lib/src/helper/pop_monitor/monitor_pop_route.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ class MonitorPopRoute with WidgetsBindingObserver {
6969
// handle contain system dialog and common condition
7070
if (MonitorPopRoute.handleSmartDialog()) {
7171
var lastDialog = DialogProxy.instance.dialogQueue.last;
72-
if (lastDialog.backType == SmartBackType.normal &&
73-
await lastDialog.onBack?.call() != true) {
72+
if (await lastDialog.onBack?.call() == true) {
73+
return true;
74+
}
75+
if (lastDialog.backType == SmartBackType.normal) {
7476
DialogProxy.instance.dismiss(
7577
status: SmartStatus.dialog,
7678
closeType: CloseType.back,

lib/src/widget/helper/attach_widget.dart

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ class AttachWidget extends StatefulWidget {
3434
this.aboveBuilder,
3535
}) : super(key: key);
3636

37-
/// 目标widget
37+
/// target widget
3838
final BuildContext? targetContext;
3939

40-
/// 自定义坐标点
40+
/// custom target point
4141
final TargetBuilder? targetBuilder;
4242

4343
final BeforeBuilder beforeBuilder;
@@ -58,11 +58,11 @@ class _AttachWidgetState extends State<AttachWidget>
5858
with WidgetsBindingObserver {
5959
late double _postFrameOpacity;
6060

61-
//offset size
62-
late Offset targetOffset;
63-
late Size targetSize;
61+
// offset & size
62+
Offset targetOffset = Offset.zero;
63+
Size targetSize = Size.zero;
6464

65-
//target info
65+
// target info
6666
RectInfo? _targetRect;
6767
BuildContext? _childContext;
6868
late Widget _originChild;
@@ -97,8 +97,11 @@ class _AttachWidgetState extends State<AttachWidget>
9797
@override
9898
void didChangeMetrics() {
9999
super.didChangeMetrics();
100-
// The latency threshold is approximately 8 milliseconds
100+
// The latency threshold is approximately 8 milliseconds.
101101
Future.delayed(const Duration(milliseconds: 30), () {
102+
if (!mounted) {
103+
return;
104+
}
102105
_resetState();
103106
});
104107
}
@@ -115,53 +118,74 @@ class _AttachWidgetState extends State<AttachWidget>
115118
List<Widget> above =
116119
widget.aboveBuilder?.call(targetOffset, targetSize) ?? [];
117120
return Stack(children: [
118-
//blow
119121
for (var belowWidget in below) belowWidget,
120-
121-
//target
122122
Positioned(
123123
left: _targetRect?.left,
124124
right: _targetRect?.right,
125125
top: _targetRect?.top,
126126
bottom: _targetRect?.bottom,
127127
child: widget.builder(child, _adjustParam),
128128
),
129-
130-
//above
131129
for (var aboveWidget in above) aboveWidget,
132130
]);
133131
}
134132

135133
void _resetState() {
134+
if (!mounted) {
135+
return;
136+
}
137+
136138
_originChild = widget.originChild;
137139
_postFrameOpacity = 0;
140+
_targetRect = null;
138141

139-
final renderBox = widget.targetContext?.findRenderObject() as RenderBox?;
140-
if (renderBox != null) {
141-
targetOffset = renderBox.localToGlobal(Offset.zero);
142-
targetSize = renderBox.size;
143-
}
142+
final hasTargetInfo = _tryUpdateTargetInfo();
144143
if (widget.targetBuilder != null) {
145-
targetOffset = widget.targetContext != null
146-
? widget.targetBuilder!(
147-
targetOffset,
148-
Size(targetSize.width, targetSize.height),
149-
)
150-
: widget.targetBuilder!(Offset.zero, Size.zero);
151-
targetSize = widget.targetContext != null ? targetSize : Size.zero;
144+
final customOffset = widget.targetBuilder!(
145+
targetOffset,
146+
Size(targetSize.width, targetSize.height),
147+
);
148+
if (_isValidOffset(customOffset)) {
149+
targetOffset = customOffset;
150+
}
151+
152+
// targetBuilder can work without targetContext.
153+
if (widget.targetContext == null && !hasTargetInfo) {
154+
final fallbackOffset = widget.targetBuilder!(Offset.zero, Size.zero);
155+
targetOffset =
156+
_isValidOffset(fallbackOffset) ? fallbackOffset : Offset.zero;
157+
targetSize = Size.zero;
158+
}
159+
}
160+
161+
// targetContext exists but currently unavailable (inactive/not laid out).
162+
if (!hasTargetInfo &&
163+
widget.targetContext != null &&
164+
widget.targetBuilder == null) {
165+
return;
152166
}
153167

154168
ViewUtils.addSafeUse(() {
155-
if (mounted) _handleLocation();
169+
if (mounted) {
170+
_handleLocation();
171+
}
156172
});
157173
}
158174

159-
/// 处理: 方向及其位置
160175
void _handleLocation() {
161-
final selfSize = (_childContext!.findRenderObject() as RenderBox).size;
176+
final selfRenderBox = _safeRenderBox(_childContext);
177+
if (selfRenderBox == null ||
178+
!_isValidOffset(targetOffset) ||
179+
!_isValidSize(targetSize)) {
180+
return;
181+
}
182+
183+
final selfSize = selfRenderBox.size;
162184
final screen = MediaQuery.of(context).size;
185+
if (!_isValidSize(selfSize) || !_isValidSize(screen)) {
186+
return;
187+
}
163188

164-
//动画方向及其位置
165189
final alignment = _alignment;
166190

167191
if (alignment == Alignment.topLeft) {
@@ -244,12 +268,13 @@ class _AttachWidgetState extends State<AttachWidget>
244268
return;
245269
}
246270

247-
//第一帧后恢复透明度,同时重置位置信息
248271
_postFrameOpacity = 1;
272+
if (!mounted) {
273+
return;
274+
}
249275
setState(() {});
250276
}
251277

252-
/// 计算attach alignment类型的偏移量
253278
double _calculateDx(Alignment alignment, Size selfSize) {
254279
double offset = 0;
255280
var type = SmartDialog.config.attach.attachAlignmentType;
@@ -284,11 +309,18 @@ class _AttachWidgetState extends State<AttachWidget>
284309
bool fixedHorizontal = false,
285310
bool fixedVertical = false,
286311
}) {
287-
final childSize = (_childContext!.findRenderObject() as RenderBox).size;
312+
final childRenderBox = _safeRenderBox(_childContext);
288313
final screen = MediaQuery.of(context).size;
289314
var rectInfo = RectInfo(left: left, right: right, top: top, bottom: bottom);
315+
if (childRenderBox == null || !_isValidSize(screen)) {
316+
return rectInfo;
317+
}
318+
319+
final childSize = childRenderBox.size;
320+
if (!_isValidSize(childSize)) {
321+
return rectInfo;
322+
}
290323

291-
//处理左右边界问题
292324
if (!fixedHorizontal && left != null) {
293325
if (left < 0) {
294326
rectInfo.left = 0;
@@ -302,7 +334,6 @@ class _AttachWidgetState extends State<AttachWidget>
302334
}
303335
}
304336

305-
//处理上下边界问题
306337
if (!fixedVertical && top != null) {
307338
if (top < 0) {
308339
rectInfo.top = 0;
@@ -318,6 +349,50 @@ class _AttachWidgetState extends State<AttachWidget>
318349

319350
return rectInfo;
320351
}
352+
353+
bool _tryUpdateTargetInfo() {
354+
final renderBox = _safeRenderBox(widget.targetContext);
355+
if (renderBox == null) {
356+
return false;
357+
}
358+
359+
try {
360+
final offset = renderBox.localToGlobal(Offset.zero);
361+
final size = renderBox.size;
362+
if (!_isValidOffset(offset) || !_isValidSize(size)) {
363+
return false;
364+
}
365+
targetOffset = offset;
366+
targetSize = size;
367+
return true;
368+
} catch (_) {
369+
return false;
370+
}
371+
}
372+
373+
RenderBox? _safeRenderBox(BuildContext? context) {
374+
if (context == null) {
375+
return null;
376+
}
377+
378+
try {
379+
final renderObject = context.findRenderObject();
380+
if (renderObject is RenderBox &&
381+
renderObject.attached &&
382+
renderObject.hasSize) {
383+
return renderObject;
384+
}
385+
} catch (_) {}
386+
return null;
387+
}
388+
389+
bool _isValidOffset(Offset offset) {
390+
return offset.dx.isFinite && offset.dy.isFinite;
391+
}
392+
393+
bool _isValidSize(Size size) {
394+
return size.width.isFinite && size.height.isFinite;
395+
}
321396
}
322397

323398
class AdaptBuilder extends StatelessWidget {

lib/src/widget/helper/smart_overlay_entry.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,24 @@ class SmartOverlayEntry extends OverlayEntry {
88
bool maintainState = false,
99
}) : super(builder: builder, opaque: opaque, maintainState: maintainState);
1010

11+
bool _disposedByOwner = false;
12+
1113
@override
1214
void markNeedsBuild() {
13-
ViewUtils.addSafeUse(() => super.markNeedsBuild());
15+
ViewUtils.addSafeUse(() {
16+
if (_disposedByOwner || !mounted) {
17+
return;
18+
}
19+
super.markNeedsBuild();
20+
});
1421
}
1522

1623
@override
1724
void remove() {
18-
if (!mounted) {
25+
if (_disposedByOwner || !mounted) {
1926
return;
2027
}
28+
_disposedByOwner = true;
2129
super.remove();
2230
super.dispose();
2331
}

0 commit comments

Comments
 (0)