Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/vector_graphics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.2.5

* Fixes a leak of the decoded picture when a `VectorGraphic` is removed before
loading finishes.

## 1.2.4

* Fixes an unhandled exception reported for every asynchronous loading failure,
Expand Down
12 changes: 7 additions & 5 deletions packages/vector_graphics/lib/src/vector_graphics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -421,18 +421,20 @@ class _VectorGraphicWidgetState extends State<VectorGraphic> {
try {
final _PictureData data = await _loadPicture(context, key, loader);
data.count += 1;
if (data.count == 1) {
_livePictureCache[key] = data;
}

// The widget may have changed, requesting a new vector graphic before
// this operation could complete.
if (!mounted || loader != widget.loader) {
_maybeReleasePicture(data);
// Other widgets awaiting the same load resume after this one, all in the
// same microtask. Defer the release to a later microtask so they can claim
// the picture first; otherwise it could be disposed while they still need it.
scheduleMicrotask(() => _maybeReleasePicture(data));
return;
}

if (data.count == 1) {
_livePictureCache[key] = data;
}

setState(() {
_maybeReleasePicture(_pictureData);
_pictureData = data;
Expand Down
2 changes: 1 addition & 1 deletion packages/vector_graphics/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: vector_graphics
description: A vector graphics rendering package for Flutter using a binary encoding.
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
version: 1.2.4
version: 1.2.5

environment:
sdk: ^3.11.0
Expand Down
117 changes: 116 additions & 1 deletion packages/vector_graphics/test/caching_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_graphics/src/render_vector_graphic.dart';
import 'package:vector_graphics/src/vector_graphics.dart';
import 'package:vector_graphics_codec/vector_graphics_codec.dart';

Expand Down Expand Up @@ -249,6 +251,105 @@ void main() {

expect(testBundle.loadKeys, <String>['bar.svg', 'foo.svg', 'foo.svg']);
});

group('Pictures decoded after their VectorGraphic is removed', () {
// Collects the pictures created during a test.
Set<ui.Picture> trackPictures() {
final created = <ui.Picture>{};
void listener(ObjectEvent event) {
final Object object = event.object;
if (event is ObjectCreated && object is ui.Picture) {
created.add(object);
}
}

FlutterMemoryAllocations.instance.addListener(listener);
addTearDown(() => FlutterMemoryAllocations.instance.removeListener(listener));
return created;
}

testWidgets('are disposed', (WidgetTester tester) async {
final loader = CompleterBytesLoader();
final Set<ui.Picture> created = trackPictures();

await tester.pumpWidget(VectorGraphic(loader: loader));
await tester.pumpWidget(const SizedBox());
loader.complete();
await tester.pumpAndSettle();

expect(created, hasLength(1));
expect(created.single.debugDisposed, isTrue);
});

testWidgets('are disposed once when every widget sharing the load is removed', (
WidgetTester tester,
) async {
final loader = CompleterBytesLoader();
final Set<ui.Picture> created = trackPictures();

await tester.pumpWidget(
Column(
children: <Widget>[
VectorGraphic(loader: loader),
VectorGraphic(loader: loader),
],
),
);
await tester.pumpWidget(const SizedBox());
loader.complete();
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
expect(created, hasLength(1));
expect(created.single.debugDisposed, isTrue);
});

testWidgets('stay alive while another widget sharing the load shows them', (
WidgetTester tester,
) async {
final loader = CompleterBytesLoader();

await tester.pumpWidget(
Column(
children: <Widget>[
VectorGraphic(key: GlobalKey(), loader: loader),
VectorGraphic(loader: loader),
],
),
);
await tester.pumpWidget(Column(children: <Widget>[VectorGraphic(loader: loader)]));
loader.complete();
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
final ui.Picture picture = tester.allRenderObjects
.whereType<RenderVectorGraphic>()
.single
.pictureInfo
.picture;
expect(picture.debugDisposed, isFalse);

await tester.pumpWidget(const SizedBox());
expect(picture.debugDisposed, isTrue);
});

testWidgets('are disposed when the loader changes before decoding finishes', (
WidgetTester tester,
) async {
final first = CompleterBytesLoader();
final second = CompleterBytesLoader();

final Set<ui.Picture> created = trackPictures();
await tester.pumpWidget(VectorGraphic(loader: first));
await tester.pumpWidget(VectorGraphic(loader: second));
first.complete();
await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
expect(created, hasLength(1));
expect(created.single.debugDisposed, isTrue);
});
});
}

class TestAssetBundle extends Fake implements AssetBundle {
Expand Down Expand Up @@ -296,3 +397,17 @@ class TestWidgetsLocalizations extends DefaultWidgetsLocalizations {
@override
TextDirection get textDirection => TextDirection.ltr;
}

class CompleterBytesLoader extends BytesLoader {
final Completer<ByteData> _completer = Completer<ByteData>();

/// Finishes loading with a small, empty vector graphic.
void complete() {
final buffer = VectorGraphicsBuffer();
codec.writeSize(buffer, 10, 10);
_completer.complete(buffer.done());
}

@override
Future<ByteData> loadBytes(BuildContext? context) => _completer.future;
}
Loading