Skip to content

Commit d50ee50

Browse files
committed
Fix: NOverlayImage.fromWidget cause Memory Leak by undisposed.
1 parent 0004b29 commit d50ee50

File tree

2 files changed

+142
-16
lines changed

2 files changed

+142
-16
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import "package:flutter/material.dart";
2+
import "package:flutter_naver_map/flutter_naver_map.dart";
3+
import "package:flutter_naver_map/src/util/widget_to_image.dart";
4+
import "package:flutter_test/flutter_test.dart";
5+
6+
void main() {
7+
testWidgets("NOverlayImage.fromWidget MemoryLeak test", (tester) async {
8+
const myAppPageKey = Key("MyApp");
9+
const stfulWidgetKey = Key("stfulA");
10+
await tester.pumpWidget(
11+
const _MyApp(key: myAppPageKey, stfulWidgetKey: stfulWidgetKey));
12+
13+
await tester.pump();
14+
15+
final stfulWidgetFinder = find.byKey(stfulWidgetKey);
16+
final _StFulTestWidgetState statefulWidgetState =
17+
tester.state(stfulWidgetFinder);
18+
19+
expect(statefulWidgetState.count, 0);
20+
21+
// dispose test : case normal attach to widgetTree
22+
final _MyAppState myAppState = tester.state(find.byKey(myAppPageKey));
23+
myAppState.disposeStfulTestWidget();
24+
await tester.pump();
25+
26+
print(statefulWidgetState.mounted);
27+
expect(statefulWidgetState.mounted, false); // : isDisposed
28+
29+
// dispose test : case attach to NOverlayImage.fromWidget inner canvas (virtual canvas)
30+
final buildContext = myAppState.context;
31+
final widgetToImageBytes = await WidgetToImageUtil.widgetToImageByte(
32+
const StFulTestWidget(),
33+
size: const Size(40, 80),
34+
context: buildContext);
35+
print(widgetToImageBytes.length);
36+
expect(widgetToImageBytes.length > 100, true); // successful created test
37+
38+
print("test end");
39+
});
40+
}
41+
42+
class _MyApp extends StatefulWidget {
43+
final Key stfulWidgetKey;
44+
45+
const _MyApp({super.key, required this.stfulWidgetKey});
46+
47+
@override
48+
State<_MyApp> createState() => _MyAppState();
49+
}
50+
51+
class _MyAppState extends State<_MyApp> {
52+
@override
53+
Widget build(BuildContext context) {
54+
return MaterialApp(
55+
home: Scaffold(
56+
body: disposeSwitchFlag
57+
? const SizedBox()
58+
: StFulTestWidget(key: widget.stfulWidgetKey),
59+
));
60+
}
61+
62+
bool disposeSwitchFlag = false;
63+
64+
void disposeStfulTestWidget() {
65+
disposeSwitchFlag = true;
66+
setState(() {});
67+
}
68+
69+
@override
70+
void dispose() {
71+
super.dispose();
72+
print("_MyApp dispose");
73+
}
74+
}
75+
76+
class StFulTestWidget extends StatefulWidget {
77+
const StFulTestWidget({super.key});
78+
79+
@override
80+
State<StFulTestWidget> createState() => _StFulTestWidgetState();
81+
}
82+
83+
class _StFulTestWidgetState extends State<StFulTestWidget> {
84+
final count = 0;
85+
86+
@override
87+
Widget build(BuildContext context) {
88+
return const Placeholder();
89+
}
90+
91+
@override
92+
void initState() {
93+
print("initState: $this");
94+
super.initState();
95+
}
96+
97+
@override
98+
void dispose() {
99+
print("dispose: $this");
100+
super.dispose();
101+
}
102+
}

lib/src/util/widget_to_image.dart

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
import "dart:typed_data" show Uint8List;
2-
import "dart:ui" show ImageByteFormat;
2+
import "dart:ui" show FlutterView, ImageByteFormat;
33

4+
import "package:flutter/material.dart";
45
import "package:flutter/rendering.dart"
56
show
67
PipelineOwner,
78
RenderPositionedBox,
89
RenderRepaintBoundary,
910
RenderView,
1011
ViewConfiguration;
11-
import "package:flutter/widgets.dart";
1212

1313
class WidgetToImageUtil {
14-
static Widget _setSizeAndTextDirection(Widget widget, Size size) {
14+
static Widget _setSizeAndTextDirection(
15+
Widget widget, Size size, BuildContext context, FlutterView view) {
1516
return SizedBox(
1617
width: size.width,
1718
height: size.height,
18-
child: Directionality(
19-
textDirection: TextDirection.ltr,
20-
child: widget,
21-
),
19+
child: MediaQuery(
20+
data: MediaQueryData.fromView(view),
21+
child: Theme(
22+
data: Theme.of(context),
23+
child: Directionality(
24+
textDirection: TextDirection.ltr,
25+
child: widget,
26+
))),
2227
);
2328
}
2429

@@ -29,33 +34,52 @@ class WidgetToImageUtil {
2934
}) async {
3035
final renderBox = RenderRepaintBoundary();
3136
final view = View.of(context);
37+
38+
final renderPositionedBox =
39+
RenderPositionedBox(alignment: Alignment.center, child: renderBox);
3240
final renderView = RenderView(
3341
view: view,
3442
configuration: ViewConfiguration(
3543
size: size, devicePixelRatio: view.devicePixelRatio),
36-
child:
37-
RenderPositionedBox(alignment: Alignment.center, child: renderBox));
44+
child: renderPositionedBox);
3845

3946
final pipelineOwner = PipelineOwner()..rootNode = renderView;
4047
renderView.prepareInitialFrame();
4148

4249
final buildOwner = BuildOwner(focusManager: FocusManager());
43-
final renderToWidget = RenderObjectToWidgetAdapter(
44-
container: renderBox, child: _setSizeAndTextDirection(widget, size))
50+
final rootElement = RenderObjectToWidgetAdapter(
51+
container: renderBox,
52+
child: _setSizeAndTextDirection(widget, size, context, view))
4553
.attachToRenderTree(buildOwner);
4654
buildOwner
47-
..buildScope(renderToWidget)
55+
..buildScope(rootElement)
4856
..finalizeTree();
4957

5058
pipelineOwner
5159
..flushLayout()
5260
..flushCompositingBits()
5361
..flushPaint();
62+
try {
63+
final image = await renderBox.toImage(pixelRatio: view.devicePixelRatio);
64+
65+
final rawImage = await image
66+
.toByteData(format: ImageByteFormat.png)
67+
.then((b) => b!.buffer.asUint8List());
5468

55-
final image = await renderBox.toImage(pixelRatio: view.devicePixelRatio);
56-
return image
57-
.toByteData(format: ImageByteFormat.png)
58-
.then((b) => b!.buffer.asUint8List());
69+
return rawImage;
70+
} finally {
71+
final emptyRenderToWidgetAdapter =
72+
RenderObjectToWidgetAdapter(container: renderBox);
73+
rootElement.update(emptyRenderToWidgetAdapter); // renderbox child = null
74+
buildOwner.finalizeTree();
75+
renderView
76+
..detach()
77+
..dispose();
78+
rootElement
79+
..detachRenderObject()
80+
..deactivate();
81+
buildOwner.finalizeTree();
82+
}
5983
}
6084

6185
WidgetToImageUtil._();

0 commit comments

Comments
 (0)