Repository navigation
Description
What package does this bug report belong to?
vector_graphics
What target platforms are you seeing this bug on?
macOS (the issue is in platform-independent Dart lifecycle code and may affect other platforms as well)
Have you already upgraded your packages?
Yes
Dependency versions
vector_graphics: 1.2.2
vector_graphics_codec: 1.1.13
The same problematic control flow is still present on the current flutter/packages main branch.
Steps to reproduce
- Create a Flutter project and add
vector_graphics: 1.2.2and
vector_graphics_codec: 1.1.13. - Add the widget test below as
test/vector_graphics_leak_test.dart. - Run
flutter test test/vector_graphics_leak_test.dart. - Observe that a
_NativePictureis created but no matching dispose event is
emitted after theVectorGraphicis removed before decoding completes.
Expected results
When asynchronous decoding finishes after the VectorGraphic has been removed,
the decoded PictureInfo.picture should be disposed immediately. Every native
picture created by this test should have a matching disposal event.
Actual results
_VectorGraphicWidgetState._loadAssetBytes increments data.count, detects that
the widget is no longer mounted, and calls _maybeReleasePicture(data).
However, _maybeReleasePicture only disposes the picture when
_livePictureCache.containsKey(data.key) is true. At this point the newly decoded
picture has never been inserted into _livePictureCache; insertion happens after
the mounted check. The count returns to zero, but the native picture is not
disposed. Leak tracking reports a non-disposed _NativePicture, with creation
through FlutterVectorGraphicsListener.toPicture and
_VectorGraphicWidgetState._loadPicture.
Relevant current implementation:
data.count += 1;
if (!mounted || loader != widget.loader) {
_maybeReleasePicture(data);
return;
}
if (data.count == 1) {
_livePictureCache[key] = data;
}void _maybeReleasePicture(_PictureData? data) {
if (data == null) {
return;
}
data.count -= 1;
if (data.count == 0 && _livePictureCache.containsKey(data.key)) {
_livePictureCache.remove(data.key);
data.pictureInfo.picture.dispose();
}
}Code sample
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_graphics/vector_graphics.dart';
import 'package:vector_graphics_codec/vector_graphics_codec.dart';
const VectorGraphicsCodec codec = VectorGraphicsCodec();
void main() {
testWidgets('disposes a picture decoded after unmount', (
WidgetTester tester,
) async {
final loader = DelayedBytesLoader();
final createdPictures = <Object>{};
final disposedPictures = <Object>{};
void trackPictures(ObjectEvent event) {
if (event.object.runtimeType.toString() != '_NativePicture') {
return;
}
if (event is ObjectCreated) {
createdPictures.add(event.object);
} else if (event is ObjectDisposed) {
disposedPictures.add(event.object);
}
}
FlutterMemoryAllocations.instance.addListener(trackPictures);
addTearDown(
() => FlutterMemoryAllocations.instance.removeListener(trackPictures),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: VectorGraphic(loader: loader),
),
);
// Unmount before asynchronous decoding finishes.
await tester.pumpWidget(const SizedBox.shrink());
final buffer = VectorGraphicsBuffer();
codec.writeSize(buffer, 10, 10);
loader.complete(buffer.done());
await tester.pumpAndSettle();
expect(createdPictures, hasLength(1));
expect(disposedPictures, containsAll(createdPictures)); // Fails on 1.2.2.
});
}
class DelayedBytesLoader extends BytesLoader {
final Completer<ByteData> _completer = Completer<ByteData>();
void complete(ByteData data) => _completer.complete(data);
@override
Future<ByteData> loadBytes(BuildContext? context) => _completer.future;
}Suggested fix
Dispose any picture whose reference count reaches zero, regardless of whether it
was inserted into the live cache. Only cache removal needs to be conditional:
void _maybeReleasePicture(_PictureData? data) {
if (data == null) {
return;
}
data.count -= 1;
if (data.count != 0) {
return;
}
if (identical(_livePictureCache[data.key], data)) {
_livePictureCache.remove(data.key);
}
data.pictureInfo.picture.dispose();
}Using identical avoids removing a newer cache entry that happens to use the same
key. The widget test above passes with this change.
Logs
leak_tracker: notDisposed: _NativePicture
FlutterMemoryAllocations._pictureOnCreate
_NativePictureRecorder.endRecording
FlutterVectorGraphicsListener.toPicture
decodeVectorGraphics
_VectorGraphicWidgetState._loadPicture
_VectorGraphicWidgetState._loadAssetBytesFlutter Doctor output
The issue was reproduced with:
Flutter 3.41.9, stable, framework revision 00b0c91f06
Dart 3.11.5
DevTools 2.54.2
macOS 26.5.2, darwin-arm64I can provide the complete flutter doctor -v output if required.
Activity
suryaKommana2662 commentedon Jul 29, 2026
@jiechic Thanks for the report. I was able to reproduce this issue locally using the provided test sample code. The test fails with the following assertion:
This indicates that a
_NativePicturewas created, but the corresponding picture was not disposed after theVectorGraphicwas unmounted before asynchronous decoding completed, matching the behavior described in the report.Tested with the provided reproduction test and
vector_graphics: 1.2.2/vector_graphics_codec: 1.1.13.flutter Doctor Output
mackings commentedon Oct 26, 2026
Hi, I'd like to work on this. I could reproduce it with your test on main, thanks for the clear report!
One thing I noticed: the suggested fix breaks when two widgets share the same load and only one of them is removed. The other one then tries to draw a picture that's already been disposed. I have a fix that handles both cases and will open a PR soon.
[vector_graphics] Dispose pictures decoded after their VectorGraphic …
github-actions commentedon Nov 12, 2026
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.