-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[vector_graphics] Add precacheVectorGraphic utility function #11716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -313,6 +313,244 @@ void main() { | |
|
|
||
| expect(testBundle.loadKeys, <String>['bar.svg', 'foo.svg', 'foo.svg']); | ||
| }); | ||
|
|
||
| testWidgets( | ||
| 'precacheVectorGraphic warms live cache so VectorGraphic renders without reloading', | ||
| (WidgetTester tester) async { | ||
| final testBundle = TestAssetBundle(); | ||
|
|
||
| // Precache with the same context that VectorGraphic will inherit. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: Builder( | ||
| builder: (BuildContext context) { | ||
| precacheVectorGraphic( | ||
| const AssetBytesLoader('foo.svg'), | ||
| context, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.runAsync( | ||
| () => Future<void>.delayed(Duration.zero), | ||
| ); | ||
|
|
||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
|
|
||
| // Now mount a VectorGraphic with the same key -- it should hit the cache. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: VectorGraphic( | ||
| loader: const AssetBytesLoader('foo.svg'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| // The asset was already decoded; no second load should occur. | ||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'precacheVectorGraphic keeps picture alive across widget unmount/remount', | ||
| (WidgetTester tester) async { | ||
| final testBundle = TestAssetBundle(); | ||
|
|
||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: Builder( | ||
| builder: (BuildContext context) { | ||
| precacheVectorGraphic( | ||
| const AssetBytesLoader('foo.svg'), | ||
| context, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.runAsync( | ||
| () => Future<void>.delayed(Duration.zero), | ||
| ); | ||
|
|
||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
|
|
||
| // Mount then unmount -- simulates a route transition. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: VectorGraphic( | ||
| loader: const AssetBytesLoader('foo.svg'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
| await tester.pumpWidget(const SizedBox()); | ||
|
|
||
| // Remount -- the precache reference should keep the picture alive. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: VectorGraphic( | ||
| loader: const AssetBytesLoader('foo.svg'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| // Only a single load should have happened across all mounts. | ||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'precacheVectorGraphic calls onError and does not throw on loader failure', | ||
| (WidgetTester tester) async { | ||
| Object? caughtError; | ||
|
|
||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: Builder( | ||
| builder: (BuildContext context) { | ||
| precacheVectorGraphic( | ||
| const _FailingBytesLoader(), | ||
| context, | ||
| onError: (Object error, StackTrace? _) { | ||
| caughtError = error; | ||
| }, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.runAsync( | ||
| () => Future<void>.delayed(Duration.zero), | ||
| ); | ||
|
|
||
| expect(caughtError, isNotNull); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'precacheVectorGraphic called twice for the same key does not double-count', | ||
| (WidgetTester tester) async { | ||
| final testBundle = TestAssetBundle(); | ||
|
|
||
| late BuildContext capturedContext; | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: Builder( | ||
| builder: (BuildContext context) { | ||
| capturedContext = context; | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| // Call precache twice; only one asset load should happen and the picture | ||
| // should survive an unmount/remount without a second decode. | ||
| await tester.runAsync(() async { | ||
| await precacheVectorGraphic( | ||
| const AssetBytesLoader('foo.svg'), | ||
| capturedContext, | ||
| ); | ||
| await precacheVectorGraphic( | ||
| const AssetBytesLoader('foo.svg'), | ||
| capturedContext, | ||
| ); | ||
| }); | ||
|
|
||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
|
|
||
| // Mount then fully unmount. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: VectorGraphic( | ||
| loader: const AssetBytesLoader('foo.svg'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
| await tester.pumpWidget(const SizedBox()); | ||
|
|
||
| // Remount — double-precache must not have leaked a phantom reference that | ||
| // keeps the count above what _maybeReleasePicture expects, causing the | ||
| // picture to be disposed prematurely or kept alive one extra decrement too | ||
| // many. A single load across all pumps confirms correct reference balance. | ||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: DefaultAssetBundle( | ||
| bundle: testBundle, | ||
| child: VectorGraphic( | ||
| loader: const AssetBytesLoader('foo.svg'), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| expect(testBundle.loadKeys.single, 'foo.svg'); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'precacheVectorGraphic rethrows when no onError is provided', | ||
| (WidgetTester tester) async { | ||
| late Future<void> precacheFuture; | ||
|
|
||
| await tester.pumpWidget( | ||
| Directionality( | ||
| textDirection: TextDirection.ltr, | ||
| child: Builder( | ||
| builder: (BuildContext context) { | ||
| precacheFuture = precacheVectorGraphic( | ||
| const _FailingBytesLoader(), | ||
| context, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| await tester.runAsync(() async { | ||
| await expectLater(precacheFuture, throwsException); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when I check out your PR and The tests in vector_graphics_test.dart which use ThrowingBytesLoader use |
||
| }); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| class TestAssetBundle extends Fake implements AssetBundle { | ||
|
|
@@ -361,3 +599,15 @@ class TestWidgetsLocalizations extends DefaultWidgetsLocalizations { | |
| @override | ||
| TextDirection get textDirection => TextDirection.ltr; | ||
| } | ||
|
|
||
| class _FailingBytesLoader extends BytesLoader { | ||
| const _FailingBytesLoader(); | ||
|
|
||
| @override | ||
| Future<ByteData> loadBytes(BuildContext? context) { | ||
| return Future<ByteData>.error(Exception('load failed')); | ||
| } | ||
|
|
||
| @override | ||
| Object cacheKey(BuildContext? context) => '_FailingBytesLoader'; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
almost certainly want this to be final (or at least read-only)