1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:file/memory.dart';
6import 'package:flutter_tools/src/artifacts.dart';
7import 'package:flutter_tools/src/base/file_system.dart';
8import 'package:flutter_tools/src/base/logger.dart';
9import 'package:flutter_tools/src/build_info.dart';
10import 'package:flutter_tools/src/build_system/build_system.dart';
11import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart';
12import 'package:flutter_tools/src/devfs.dart';
13
14import '../../../src/common.dart';
15import '../../../src/fake_process_manager.dart';
16import '../../../src/fakes.dart';
17
18const List<int> _kTtfHeaderBytes = <int>[0, 1, 0, 0, 0, 15, 0, 128, 0, 3, 0, 112];
19
20const String inputPath = '/input/fonts/MaterialIcons-Regular.otf';
21const String outputPath = '/output/fonts/MaterialIcons-Regular.otf';
22const String relativePath = 'fonts/MaterialIcons-Regular.otf';
23
24final RegExp whitespace = RegExp(r'\s+');
25
26void main() {
27 late BufferLogger logger;
28 late MemoryFileSystem fileSystem;
29 late FakeProcessManager processManager;
30 late Artifacts artifacts;
31 late DevFSStringContent fontManifestContent;
32
33 late String dartPath;
34 late String constFinderPath;
35 late String fontSubsetPath;
36 late List<String> fontSubsetArgs;
37
38 List<String> getConstFinderArgs(String appDillPath) => <String>[
39 dartPath,
40 constFinderPath,
41 '--kernel-file',
42 appDillPath,
43 '--class-library-uri',
44 'package:flutter/src/widgets/icon_data.dart',
45 '--class-name',
46 'IconData',
47 '--annotation-class-name',
48 '_StaticIconProvider',
49 '--annotation-class-library-uri',
50 'package:flutter/src/widgets/icon_data.dart',
51 ];
52
53 void addConstFinderInvocation(
54 String appDillPath, {
55 int exitCode = 0,
56 String stdout = '',
57 String stderr = '',
58 }) {
59 processManager.addCommand(
60 FakeCommand(
61 command: getConstFinderArgs(appDillPath),
62 exitCode: exitCode,
63 stdout: stdout,
64 stderr: stderr,
65 ),
66 );
67 }
68
69 void resetFontSubsetInvocation({
70 int exitCode = 0,
71 String stdout = '',
72 String stderr = '',
73 required CompleterIOSink stdinSink,
74 }) {
75 stdinSink.clear();
76 processManager.addCommand(
77 FakeCommand(
78 command: fontSubsetArgs,
79 exitCode: exitCode,
80 stdout: stdout,
81 stderr: stderr,
82 stdin: stdinSink,
83 ),
84 );
85 }
86
87 setUp(() {
88 processManager = FakeProcessManager.empty();
89 fontManifestContent = DevFSStringContent(validFontManifestJson);
90 artifacts = Artifacts.test();
91 fileSystem = MemoryFileSystem.test();
92 logger = BufferLogger.test();
93 dartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
94 constFinderPath = artifacts.getArtifactPath(Artifact.constFinder);
95 fontSubsetPath = artifacts.getArtifactPath(Artifact.fontSubset);
96
97 fontSubsetArgs = <String>[fontSubsetPath, outputPath, inputPath];
98
99 fileSystem.file(constFinderPath).createSync(recursive: true);
100 fileSystem.file(dartPath).createSync(recursive: true);
101 fileSystem.file(fontSubsetPath).createSync(recursive: true);
102 fileSystem.file(inputPath)
103 ..createSync(recursive: true)
104 ..writeAsBytesSync(_kTtfHeaderBytes);
105 });
106
107 Environment createEnvironment(Map<String, String> defines) {
108 return Environment.test(
109 fileSystem.directory('/icon_test')..createSync(recursive: true),
110 defines: defines,
111 artifacts: artifacts,
112 processManager: FakeProcessManager.any(),
113 fileSystem: fileSystem,
114 logger: BufferLogger.test(),
115 );
116 }
117
118 testWithoutContext('Prints error in debug mode environment', () async {
119 final Environment environment = createEnvironment(<String, String>{
120 kIconTreeShakerFlag: 'true',
121 kBuildMode: 'debug',
122 });
123
124 final IconTreeShaker iconTreeShaker = IconTreeShaker(
125 environment,
126 fontManifestContent,
127 logger: logger,
128 processManager: processManager,
129 fileSystem: fileSystem,
130 artifacts: artifacts,
131 targetPlatform: TargetPlatform.android,
132 );
133
134 expect(
135 logger.errorText,
136 'Font subsetting is not supported in debug mode. The --tree-shake-icons'
137 ' flag will be ignored.\n',
138 );
139 expect(iconTreeShaker.enabled, false);
140
141 final bool subsets = await iconTreeShaker.subsetFont(
142 input: fileSystem.file(inputPath),
143 outputPath: outputPath,
144 relativePath: relativePath,
145 );
146 expect(subsets, false);
147 expect(processManager, hasNoRemainingExpectations);
148 });
149
150 testWithoutContext('Does not get enabled without font manifest', () {
151 final Environment environment = createEnvironment(<String, String>{
152 kIconTreeShakerFlag: 'true',
153 kBuildMode: 'release',
154 });
155
156 final IconTreeShaker iconTreeShaker = IconTreeShaker(
157 environment,
158 null,
159 logger: logger,
160 processManager: processManager,
161 fileSystem: fileSystem,
162 artifacts: artifacts,
163 targetPlatform: TargetPlatform.android,
164 );
165
166 expect(logger.errorText, isEmpty);
167 expect(iconTreeShaker.enabled, false);
168 expect(processManager, hasNoRemainingExpectations);
169 });
170
171 testWithoutContext('Gets enabled', () {
172 final Environment environment = createEnvironment(<String, String>{
173 kIconTreeShakerFlag: 'true',
174 kBuildMode: 'release',
175 });
176
177 final IconTreeShaker iconTreeShaker = IconTreeShaker(
178 environment,
179 fontManifestContent,
180 logger: logger,
181 processManager: processManager,
182 fileSystem: fileSystem,
183 artifacts: artifacts,
184 targetPlatform: TargetPlatform.android,
185 );
186
187 expect(logger.errorText, isEmpty);
188 expect(iconTreeShaker.enabled, true);
189 expect(processManager, hasNoRemainingExpectations);
190 });
191
192 test('No app.dill throws exception', () async {
193 final Environment environment = createEnvironment(<String, String>{
194 kIconTreeShakerFlag: 'true',
195 kBuildMode: 'release',
196 });
197
198 final IconTreeShaker iconTreeShaker = IconTreeShaker(
199 environment,
200 fontManifestContent,
201 logger: logger,
202 processManager: processManager,
203 fileSystem: fileSystem,
204 artifacts: artifacts,
205 targetPlatform: TargetPlatform.android,
206 );
207
208 expect(
209 () async => iconTreeShaker.subsetFont(
210 input: fileSystem.file(inputPath),
211 outputPath: outputPath,
212 relativePath: relativePath,
213 ),
214 throwsA(isA<IconTreeShakerException>()),
215 );
216 expect(processManager, hasNoRemainingExpectations);
217 });
218
219 testWithoutContext('Can subset a font', () async {
220 final Environment environment = createEnvironment(<String, String>{
221 kIconTreeShakerFlag: 'true',
222 kBuildMode: 'release',
223 });
224 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
225
226 final IconTreeShaker iconTreeShaker = IconTreeShaker(
227 environment,
228 fontManifestContent,
229 logger: logger,
230 processManager: processManager,
231 fileSystem: fileSystem,
232 artifacts: artifacts,
233 targetPlatform: TargetPlatform.android,
234 );
235 final CompleterIOSink stdinSink = CompleterIOSink();
236 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
237 resetFontSubsetInvocation(stdinSink: stdinSink);
238 // Font starts out 2500 bytes long
239 final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0));
240 // after subsetting, font is 1200 bytes long
241 fileSystem.file(outputPath)
242 ..createSync(recursive: true)
243 ..writeAsBytesSync(List<int>.filled(1200, 0));
244 bool subsetted = await iconTreeShaker.subsetFont(
245 input: inputFont,
246 outputPath: outputPath,
247 relativePath: relativePath,
248 );
249 expect(stdinSink.getAndClear(), '59470\n');
250 resetFontSubsetInvocation(stdinSink: stdinSink);
251
252 expect(subsetted, true);
253 subsetted = await iconTreeShaker.subsetFont(
254 input: fileSystem.file(inputPath),
255 outputPath: outputPath,
256 relativePath: relativePath,
257 );
258 expect(subsetted, true);
259 expect(stdinSink.getAndClear(), '59470\n');
260 expect(processManager, hasNoRemainingExpectations);
261 expect(
262 logger.statusText,
263 contains(
264 'Font asset "MaterialIcons-Regular.otf" was tree-shaken, reducing it from 2500 to 1200 bytes (52.0% reduction). Tree-shaking can be disabled by providing the --no-tree-shake-icons flag when building your app.',
265 ),
266 );
267 });
268
269 testWithoutContext('Does not subset a non-supported font', () async {
270 final Environment environment = createEnvironment(<String, String>{
271 kIconTreeShakerFlag: 'true',
272 kBuildMode: 'release',
273 });
274 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
275
276 final IconTreeShaker iconTreeShaker = IconTreeShaker(
277 environment,
278 fontManifestContent,
279 logger: logger,
280 processManager: processManager,
281 fileSystem: fileSystem,
282 artifacts: artifacts,
283 targetPlatform: TargetPlatform.android,
284 );
285
286 final CompleterIOSink stdinSink = CompleterIOSink();
287 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
288 resetFontSubsetInvocation(stdinSink: stdinSink);
289
290 final File notAFont =
291 fileSystem.file('input/foo/bar.txt')
292 ..createSync(recursive: true)
293 ..writeAsStringSync('I could not think of a better string');
294 final bool subsetted = await iconTreeShaker.subsetFont(
295 input: notAFont,
296 outputPath: outputPath,
297 relativePath: relativePath,
298 );
299 expect(subsetted, false);
300 });
301
302 testWithoutContext('Does not subset an invalid ttf font', () async {
303 final Environment environment = createEnvironment(<String, String>{
304 kIconTreeShakerFlag: 'true',
305 kBuildMode: 'release',
306 });
307 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
308
309 final IconTreeShaker iconTreeShaker = IconTreeShaker(
310 environment,
311 fontManifestContent,
312 logger: logger,
313 processManager: processManager,
314 fileSystem: fileSystem,
315 artifacts: artifacts,
316 targetPlatform: TargetPlatform.android,
317 );
318
319 final CompleterIOSink stdinSink = CompleterIOSink();
320 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
321 resetFontSubsetInvocation(stdinSink: stdinSink);
322
323 final File notAFont = fileSystem.file(inputPath)..writeAsBytesSync(<int>[0, 1, 2]);
324 final bool subsetted = await iconTreeShaker.subsetFont(
325 input: notAFont,
326 outputPath: outputPath,
327 relativePath: relativePath,
328 );
329
330 expect(subsetted, false);
331 });
332
333 for (final TargetPlatform platform in <TargetPlatform>[
334 TargetPlatform.android_arm,
335 TargetPlatform.web_javascript,
336 ]) {
337 testWithoutContext('Non-constant instances $platform', () async {
338 final Environment environment = createEnvironment(<String, String>{
339 kIconTreeShakerFlag: 'true',
340 kBuildMode: 'release',
341 });
342 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
343
344 final IconTreeShaker iconTreeShaker = IconTreeShaker(
345 environment,
346 fontManifestContent,
347 logger: logger,
348 processManager: processManager,
349 fileSystem: fileSystem,
350 artifacts: artifacts,
351 targetPlatform: platform,
352 );
353
354 addConstFinderInvocation(appDill.path, stdout: constFinderResultWithInvalid);
355
356 await expectLater(
357 () => iconTreeShaker.subsetFont(
358 input: fileSystem.file(inputPath),
359 outputPath: outputPath,
360 relativePath: relativePath,
361 ),
362 throwsToolExit(
363 message:
364 'Avoid non-constant invocations of IconData or try to build'
365 ' again with --no-tree-shake-icons.',
366 ),
367 );
368 expect(processManager, hasNoRemainingExpectations);
369 });
370 }
371
372 testWithoutContext('Does not add 0x32 for non-web builds', () async {
373 final Environment environment = createEnvironment(<String, String>{
374 kIconTreeShakerFlag: 'true',
375 kBuildMode: 'release',
376 });
377 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
378
379 final IconTreeShaker iconTreeShaker = IconTreeShaker(
380 environment,
381 fontManifestContent,
382 logger: logger,
383 processManager: processManager,
384 fileSystem: fileSystem,
385 artifacts: artifacts,
386 targetPlatform: TargetPlatform.android_arm64,
387 );
388
389 addConstFinderInvocation(
390 appDill.path,
391 // Does not contain space char
392 stdout: validConstFinderResult,
393 );
394 final CompleterIOSink stdinSink = CompleterIOSink();
395 resetFontSubsetInvocation(stdinSink: stdinSink);
396 expect(processManager.hasRemainingExpectations, isTrue);
397 final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0));
398 fileSystem.file(outputPath)
399 ..createSync(recursive: true)
400 ..writeAsBytesSync(List<int>.filled(1200, 0));
401
402 final bool result = await iconTreeShaker.subsetFont(
403 input: inputFont,
404 outputPath: outputPath,
405 relativePath: relativePath,
406 );
407
408 expect(result, isTrue);
409 final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace);
410 expect(codePoints, isNot(contains('optional:32')));
411
412 expect(processManager, hasNoRemainingExpectations);
413 });
414
415 testWithoutContext('Ensures 0x32 is included for web builds', () async {
416 final Environment environment = createEnvironment(<String, String>{
417 kIconTreeShakerFlag: 'true',
418 kBuildMode: 'release',
419 });
420 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
421
422 final IconTreeShaker iconTreeShaker = IconTreeShaker(
423 environment,
424 fontManifestContent,
425 logger: logger,
426 processManager: processManager,
427 fileSystem: fileSystem,
428 artifacts: artifacts,
429 targetPlatform: TargetPlatform.web_javascript,
430 );
431
432 addConstFinderInvocation(
433 appDill.path,
434 // Does not contain space char
435 stdout: validConstFinderResult,
436 );
437 final CompleterIOSink stdinSink = CompleterIOSink();
438 resetFontSubsetInvocation(stdinSink: stdinSink);
439 expect(processManager.hasRemainingExpectations, isTrue);
440 final File inputFont = fileSystem.file(inputPath)..writeAsBytesSync(List<int>.filled(2500, 0));
441 fileSystem.file(outputPath)
442 ..createSync(recursive: true)
443 ..writeAsBytesSync(List<int>.filled(1200, 0));
444
445 final bool result = await iconTreeShaker.subsetFont(
446 input: inputFont,
447 outputPath: outputPath,
448 relativePath: relativePath,
449 );
450
451 expect(result, isTrue);
452 final List<String> codePoints = stdinSink.getAndClear().trim().split(whitespace);
453 expect(codePoints, containsAllInOrder(const <String>['59470', 'optional:32']));
454
455 expect(processManager, hasNoRemainingExpectations);
456 });
457
458 testWithoutContext('Non-zero font-subset exit code', () async {
459 final Environment environment = createEnvironment(<String, String>{
460 kIconTreeShakerFlag: 'true',
461 kBuildMode: 'release',
462 });
463 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
464 fileSystem.file(inputPath).createSync(recursive: true);
465
466 final IconTreeShaker iconTreeShaker = IconTreeShaker(
467 environment,
468 fontManifestContent,
469 logger: logger,
470 processManager: processManager,
471 fileSystem: fileSystem,
472 artifacts: artifacts,
473 targetPlatform: TargetPlatform.android,
474 );
475
476 final CompleterIOSink stdinSink = CompleterIOSink();
477 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
478 resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
479
480 await expectLater(
481 () => iconTreeShaker.subsetFont(
482 input: fileSystem.file(inputPath),
483 outputPath: outputPath,
484 relativePath: relativePath,
485 ),
486 throwsA(isA<IconTreeShakerException>()),
487 );
488 expect(processManager, hasNoRemainingExpectations);
489 });
490
491 testWithoutContext('font-subset throws on write to sdtin', () async {
492 final Environment environment = createEnvironment(<String, String>{
493 kIconTreeShakerFlag: 'true',
494 kBuildMode: 'release',
495 });
496 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
497
498 final IconTreeShaker iconTreeShaker = IconTreeShaker(
499 environment,
500 fontManifestContent,
501 logger: logger,
502 processManager: processManager,
503 fileSystem: fileSystem,
504 artifacts: artifacts,
505 targetPlatform: TargetPlatform.android,
506 );
507
508 final CompleterIOSink stdinSink = CompleterIOSink(throwOnAdd: true);
509 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
510 resetFontSubsetInvocation(exitCode: -1, stdinSink: stdinSink);
511
512 await expectLater(
513 () => iconTreeShaker.subsetFont(
514 input: fileSystem.file(inputPath),
515 outputPath: outputPath,
516 relativePath: relativePath,
517 ),
518 throwsA(isA<IconTreeShakerException>()),
519 );
520 expect(processManager, hasNoRemainingExpectations);
521 });
522
523 testWithoutContext('Invalid font manifest', () async {
524 final Environment environment = createEnvironment(<String, String>{
525 kIconTreeShakerFlag: 'true',
526 kBuildMode: 'release',
527 });
528 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
529
530 fontManifestContent = DevFSStringContent(invalidFontManifestJson);
531
532 final IconTreeShaker iconTreeShaker = IconTreeShaker(
533 environment,
534 fontManifestContent,
535 logger: logger,
536 processManager: processManager,
537 fileSystem: fileSystem,
538 artifacts: artifacts,
539 targetPlatform: TargetPlatform.android,
540 );
541
542 addConstFinderInvocation(appDill.path, stdout: validConstFinderResult);
543
544 await expectLater(
545 () => iconTreeShaker.subsetFont(
546 input: fileSystem.file(inputPath),
547 outputPath: outputPath,
548 relativePath: relativePath,
549 ),
550 throwsA(isA<IconTreeShakerException>()),
551 );
552 expect(processManager, hasNoRemainingExpectations);
553 });
554
555 testWithoutContext('Allow system font fallback when fontFamily is null', () async {
556 final Environment environment = createEnvironment(<String, String>{
557 kIconTreeShakerFlag: 'true',
558 kBuildMode: 'release',
559 });
560 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
561
562 // Valid manifest, just not using it.
563 fontManifestContent = DevFSStringContent(validFontManifestJson);
564
565 final IconTreeShaker iconTreeShaker = IconTreeShaker(
566 environment,
567 fontManifestContent,
568 logger: logger,
569 processManager: processManager,
570 fileSystem: fileSystem,
571 artifacts: artifacts,
572 targetPlatform: TargetPlatform.android,
573 );
574
575 addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult);
576 // Does not throw
577 await iconTreeShaker.subsetFont(
578 input: fileSystem.file(inputPath),
579 outputPath: outputPath,
580 relativePath: relativePath,
581 );
582
583 expect(
584 logger.traceText,
585 contains(
586 'Expected to find fontFamily for constant IconData with codepoint: '
587 '59470, but found fontFamily: null. This usually means '
588 'you are relying on the system font. Alternatively, font families in '
589 'an IconData class can be provided in the assets section of your '
590 'pubspec.yaml, or you are missing "uses-material-design: true".\n',
591 ),
592 );
593 expect(processManager, hasNoRemainingExpectations);
594 });
595
596 testWithoutContext(
597 'Allow system font fallback when fontFamily is null and manifest is empty',
598 () async {
599 final Environment environment = createEnvironment(<String, String>{
600 kIconTreeShakerFlag: 'true',
601 kBuildMode: 'release',
602 });
603 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
604
605 // Nothing in font manifest
606 fontManifestContent = DevFSStringContent(emptyFontManifestJson);
607
608 final IconTreeShaker iconTreeShaker = IconTreeShaker(
609 environment,
610 fontManifestContent,
611 logger: logger,
612 processManager: processManager,
613 fileSystem: fileSystem,
614 artifacts: artifacts,
615 targetPlatform: TargetPlatform.android,
616 );
617
618 addConstFinderInvocation(appDill.path, stdout: emptyConstFinderResult);
619 // Does not throw
620 await iconTreeShaker.subsetFont(
621 input: fileSystem.file(inputPath),
622 outputPath: outputPath,
623 relativePath: relativePath,
624 );
625
626 expect(
627 logger.traceText,
628 contains(
629 'Expected to find fontFamily for constant IconData with codepoint: '
630 '59470, but found fontFamily: null. This usually means '
631 'you are relying on the system font. Alternatively, font families in '
632 'an IconData class can be provided in the assets section of your '
633 'pubspec.yaml, or you are missing "uses-material-design: true".\n',
634 ),
635 );
636 expect(processManager, hasNoRemainingExpectations);
637 },
638 );
639
640 testWithoutContext('ConstFinder non-zero exit', () async {
641 final Environment environment = createEnvironment(<String, String>{
642 kIconTreeShakerFlag: 'true',
643 kBuildMode: 'release',
644 });
645 final File appDill = environment.buildDir.childFile('app.dill')..createSync(recursive: true);
646
647 fontManifestContent = DevFSStringContent(invalidFontManifestJson);
648
649 final IconTreeShaker iconTreeShaker = IconTreeShaker(
650 environment,
651 fontManifestContent,
652 logger: logger,
653 processManager: processManager,
654 fileSystem: fileSystem,
655 artifacts: artifacts,
656 targetPlatform: TargetPlatform.android,
657 );
658
659 addConstFinderInvocation(appDill.path, exitCode: -1);
660
661 await expectLater(
662 () async => iconTreeShaker.subsetFont(
663 input: fileSystem.file(inputPath),
664 outputPath: outputPath,
665 relativePath: relativePath,
666 ),
667 throwsA(isA<IconTreeShakerException>()),
668 );
669 expect(processManager, hasNoRemainingExpectations);
670 });
671}
672
673const String validConstFinderResult = '''
674{
675 "constantInstances": [
676 {
677 "codePoint": 59470,
678 "fontFamily": "MaterialIcons",
679 "fontPackage": null,
680 "matchTextDirection": false
681 }
682 ],
683 "nonConstantLocations": []
684}
685''';
686
687const String emptyConstFinderResult = '''
688{
689 "constantInstances": [
690 {
691 "codePoint": 59470,
692 "fontFamily": null,
693 "fontPackage": null,
694 "matchTextDirection": false
695 }
696 ],
697 "nonConstantLocations": []
698}
699''';
700
701const String constFinderResultWithInvalid = '''
702{
703 "constantInstances": [
704 {
705 "codePoint": 59470,
706 "fontFamily": "MaterialIcons",
707 "fontPackage": null,
708 "matchTextDirection": false
709 }
710 ],
711 "nonConstantLocations": [
712 {
713 "file": "file:///Path/to/hello_world/lib/file.dart",
714 "line": 19,
715 "column": 11
716 }
717 ]
718}
719''';
720
721const String validFontManifestJson = '''
722[
723 {
724 "family": "MaterialIcons",
725 "fonts": [
726 {
727 "asset": "fonts/MaterialIcons-Regular.otf"
728 }
729 ]
730 },
731 {
732 "family": "GalleryIcons",
733 "fonts": [
734 {
735 "asset": "packages/flutter_gallery_assets/fonts/private/gallery_icons/GalleryIcons.ttf"
736 }
737 ]
738 },
739 {
740 "family": "packages/cupertino_icons/CupertinoIcons",
741 "fonts": [
742 {
743 "asset": "packages/cupertino_icons/assets/CupertinoIcons.ttf"
744 }
745 ]
746 }
747]
748''';
749
750const String invalidFontManifestJson = '''
751{
752 "famly": "MaterialIcons",
753 "fonts": [
754 {
755 "asset": "fonts/MaterialIcons-Regular.otf"
756 }
757 ]
758}
759''';
760
761const String emptyFontManifestJson = '[]';
762

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com