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
5/// @docImport 'dart:io';
6library;
7
8import 'dart:async' show FutureOr;
9import 'dart:io' as io show HttpClient, OSError, SocketException;
10
11import 'package:file/file.dart';
12import 'package:file/local.dart';
13import 'package:flutter/foundation.dart';
14import 'package:flutter_test/flutter_test.dart';
15import 'package:platform/platform.dart';
16import 'package:process/process.dart';
17
18import 'skia_client.dart';
19export 'skia_client.dart';
20
21// If you are here trying to figure out how to use golden files in the Flutter
22// repo itself, consider reading this wiki page:
23// https://github.com/flutter/flutter/blob/main/docs/contributing/testing/Writing-a-golden-file-test-for-package-flutter.md
24
25// If you are trying to debug this package, you may like to use the golden test
26// titled "Inconsequential golden test" in this file:
27// /packages/flutter/test/widgets/basic_test.dart
28
29// TODO(ianh): sort the parameters and arguments in this file so they use a consistent order throughout.
30
31const String _kFlutterRootKey = 'FLUTTER_ROOT';
32
33bool _isMainBranch(String? branch) {
34 return branch == 'main'
35 || branch == 'master';
36}
37
38
39/// Main method that can be used in a `flutter_test_config.dart` file to set
40/// [goldenFileComparator] to an instance of [FlutterGoldenFileComparator] that
41/// works for the current test. _Which_ [FlutterGoldenFileComparator] is
42/// instantiated is based on the current testing environment.
43///
44/// When set, the `namePrefix` is prepended to the names of all gold images.
45///
46/// This function assumes the [goldenFileComparator] has been set to a
47/// [LocalFileComparator], which happens in the bootstrap code used when running
48/// tests using `flutter test`. This should not be called when running a test
49/// using `flutter run`, as in that environment, the [goldenFileComparator] is a
50/// [TrivialComparator].
51///
52/// An [HttpClient] is created when this method is called. That client is used
53/// to communicate with the Skia Gold servers. Any [HttpOverrides] set in this
54/// will affect whether this is effective or not. For example, if the current
55/// override provides a mock client that always fails, then all calls to gold
56/// comparison functions will fail.
57Future<void> testExecutable(FutureOr<void> Function() testMain, {String? namePrefix}) async {
58 assert(
59 goldenFileComparator is LocalFileComparator,
60 'The flutter_goldens package should be used from a flutter_test_config.dart '
61 'file, which is only invoked when using "flutter test". The "flutter test" '
62 'bootstrap logic sets "goldenFileComparator" to a LocalFileComparator. It '
63 'appears in this instance however that the "goldenFileComparator" is a '
64 '${goldenFileComparator.runtimeType}.\n'
65 'See also: https://flutter.dev/to/flutter-test-docs',
66 );
67 const Platform platform = LocalPlatform();
68 const FileSystem fs = LocalFileSystem();
69 const ProcessManager process = LocalProcessManager();
70 final io.HttpClient httpClient = io.HttpClient();
71 if (FlutterPostSubmitFileComparator.isForEnvironment(platform)) {
72 goldenFileComparator = await FlutterPostSubmitFileComparator.fromLocalFileComparator(
73 localFileComparator: goldenFileComparator as LocalFileComparator,
74 platform: platform,
75 namePrefix: namePrefix,
76 log: print,
77 fs: fs,
78 process: process,
79 httpClient: httpClient,
80 );
81 } else if (FlutterPreSubmitFileComparator.isForEnvironment(platform)) {
82 goldenFileComparator = await FlutterPreSubmitFileComparator.fromLocalFileComparator(
83 localFileComparator: goldenFileComparator as LocalFileComparator,
84 platform: platform,
85 namePrefix: namePrefix,
86 log: print,
87 fs: fs,
88 process: process,
89 httpClient: httpClient,
90 );
91 } else if (FlutterSkippingFileComparator.isForEnvironment(platform)) {
92 goldenFileComparator = FlutterSkippingFileComparator.fromLocalFileComparator(
93 localFileComparator: goldenFileComparator as LocalFileComparator,
94 'Golden file testing is not executed on Cirrus, or LUCI environments '
95 'outside of flutter/flutter, or in test shards that are not configured '
96 'for using goldctl.',
97 platform: platform,
98 namePrefix: namePrefix,
99 log: print,
100 fs: fs,
101 process: process,
102 httpClient: httpClient,
103 );
104 } else {
105 goldenFileComparator = await FlutterLocalFileComparator.fromLocalFileComparator(
106 localFileComparator: goldenFileComparator as LocalFileComparator,
107 platform: platform,
108 log: print,
109 fs: fs,
110 process: process,
111 httpClient: httpClient,
112 );
113 }
114 await testMain();
115}
116
117/// Abstract base class golden file comparator specific to the `flutter/flutter`
118/// repository.
119///
120/// Golden file testing for the `flutter/flutter` repository is handled by three
121/// different [FlutterGoldenFileComparator]s, depending on the current testing
122/// environment.
123///
124/// * The [FlutterPostSubmitFileComparator] is utilized during post-submit
125/// testing, after a pull request has landed on the master branch. This
126/// comparator uses the [SkiaGoldClient] and the `goldctl` tool to upload
127/// tests to the [Flutter Gold dashboard](https://flutter-gold.skia.org).
128/// Flutter Gold manages the master golden files for the `flutter/flutter`
129/// repository.
130///
131/// * The [FlutterPreSubmitFileComparator] is utilized in pre-submit testing,
132/// before a pull request lands on the master branch. This
133/// comparator uses the [SkiaGoldClient] to execute tryjobs, allowing
134/// contributors to view and check in visual differences before landing the
135/// change.
136///
137/// * The [FlutterLocalFileComparator] is used for local development testing.
138/// This comparator will use the [SkiaGoldClient] to request baseline images
139/// from [Flutter Gold](https://flutter-gold.skia.org) and manually compare
140/// pixels. If a difference is detected, this comparator will
141/// generate failure output illustrating the found difference. If a baseline
142/// is not found for a given test image, it will consider it a new test and
143/// output the new image for verification.
144///
145/// The [FlutterSkippingFileComparator] is utilized to skip tests outside
146/// of the appropriate environments described above. Currently, some Luci
147/// environments do not execute golden file testing, and as such do not require
148/// a comparator. This comparator is also used when an internet connection is
149/// unavailable.
150abstract class FlutterGoldenFileComparator extends GoldenFileComparator {
151 /// Creates a [FlutterGoldenFileComparator] that will resolve golden file
152 /// URIs relative to the specified [basedir], and retrieve golden baselines
153 /// using the [skiaClient]. The [basedir] is used for writing and accessing
154 /// information and files for interacting with the [skiaClient]. When testing
155 /// locally, the [basedir] will also contain any diffs from failed tests, or
156 /// goldens generated from newly introduced tests.
157 @visibleForTesting
158 FlutterGoldenFileComparator(
159 this.basedir,
160 this.skiaClient, {
161 required this.fs,
162 required this.platform,
163 this.namePrefix,
164 required this.log,
165 });
166
167 /// The directory to which golden file URIs will be resolved in [compare] and
168 /// [update].
169 final Uri basedir;
170
171 /// A client for uploading image tests and making baseline requests to the
172 /// Flutter Gold Dashboard.
173 final SkiaGoldClient skiaClient;
174
175 /// The file system used to perform file access.
176 final FileSystem fs;
177
178 /// The environment (current working directory, identity of the OS,
179 /// environment variables, etc).
180 final Platform platform;
181
182 /// The prefix that is added to all golden names.
183 final String? namePrefix;
184
185 /// The logging function to use when reporting messages to the console.
186 final LogCallback log;
187
188 @override
189 Future<void> update(Uri golden, Uint8List imageBytes) async {
190 final File goldenFile = getGoldenFile(golden);
191 await goldenFile.parent.create(recursive: true);
192 await goldenFile.writeAsBytes(imageBytes, flush: true);
193 }
194
195 @override
196 Uri getTestUri(Uri key, int? version) => key;
197
198 /// Calculate the appropriate basedir for the current test context.
199 ///
200 /// The optional [suffix] argument is used by the
201 /// [FlutterPostSubmitFileComparator] and the [FlutterPreSubmitFileComparator].
202 /// These [FlutterGoldenFileComparator]s randomize their base directories to
203 /// maintain thread safety while using the `goldctl` tool.
204 @protected
205 @visibleForTesting
206 static Directory getBaseDirectory(
207 LocalFileComparator defaultComparator, {
208 required Platform platform,
209 String? suffix,
210 required FileSystem fs,
211 }) {
212 final Directory flutterRoot = fs.directory(platform.environment[_kFlutterRootKey]);
213 final Directory comparisonRoot = switch (suffix) {
214 null => flutterRoot.childDirectory(fs.path.join('bin', 'cache', 'pkg', 'skia_goldens')),
215 _ => fs.systemTempDirectory.createTempSync(suffix),
216 };
217
218 final String testPath = fs.directory(defaultComparator.basedir).path;
219 return comparisonRoot.childDirectory(
220 fs.path.relative(testPath, from: flutterRoot.path),
221 );
222 }
223
224 /// Returns the golden [File] identified by the given [Uri].
225 @protected
226 File getGoldenFile(Uri uri) {
227 final File goldenFile = fs.directory(basedir).childFile(fs.file(uri).path);
228 return goldenFile;
229 }
230
231 /// Prepends the golden URL with the library name that encloses the current
232 /// test.
233 Uri _addPrefix(Uri golden) {
234 // Ensure the Uri ends in .png as the SkiaClient expects
235 assert(
236 golden.toString().split('.').last == 'png',
237 'Golden files in the Flutter framework must end with the file extension '
238 '.png.'
239 );
240 return Uri.parse(<String>[
241 if (namePrefix != null)
242 namePrefix!,
243 basedir.pathSegments[basedir.pathSegments.length - 2],
244 golden.toString(),
245 ].join('.'));
246 }
247}
248
249/// A [FlutterGoldenFileComparator] for testing golden images with Skia Gold in
250/// post-submit.
251///
252/// For testing across all platforms, the [SkiaGoldClient] is used to upload
253/// images for framework-related golden tests and process results.
254///
255/// See also:
256///
257/// * [GoldenFileComparator], the abstract class that
258/// [FlutterGoldenFileComparator] implements.
259/// * [FlutterPreSubmitFileComparator], another
260/// [FlutterGoldenFileComparator] that tests golden images before changes are
261/// merged into the master branch.
262/// * [FlutterLocalFileComparator], another
263/// [FlutterGoldenFileComparator] that tests golden images locally on your
264/// current machine.
265class FlutterPostSubmitFileComparator extends FlutterGoldenFileComparator {
266 /// Creates a [FlutterPostSubmitFileComparator] that will test golden file
267 /// images against Skia Gold.
268 ///
269 /// The [fs] parameter is useful in tests, where the default
270 /// file system can be replaced by mock instances.
271 FlutterPostSubmitFileComparator(
272 super.basedir,
273 super.skiaClient, {
274 required super.fs,
275 required super.platform,
276 super.namePrefix,
277 required super.log,
278 });
279
280 /// Creates a new [FlutterPostSubmitFileComparator] that mirrors the relative
281 /// path resolution of the provided `localFileComparator`.
282 ///
283 /// The [goldens] parameter is visible for testing purposes only.
284 static Future<FlutterPostSubmitFileComparator> fromLocalFileComparator({
285 SkiaGoldClient? goldens,
286 required LocalFileComparator localFileComparator,
287 required Platform platform,
288 String? namePrefix,
289 required LogCallback log,
290 required FileSystem fs,
291 required ProcessManager process,
292 required io.HttpClient httpClient,
293 }) async {
294 final Directory baseDirectory = FlutterGoldenFileComparator.getBaseDirectory(
295 localFileComparator,
296 platform: platform,
297 suffix: 'flutter_goldens_postsubmit.',
298 fs: fs,
299 );
300 baseDirectory.createSync(recursive: true);
301
302 goldens ??= SkiaGoldClient(
303 baseDirectory,
304 log: log,
305 platform: platform,
306 fs: fs,
307 process: process,
308 httpClient: httpClient,
309 );
310 await goldens.auth();
311 return FlutterPostSubmitFileComparator(
312 baseDirectory.uri,
313 goldens,
314 platform: platform,
315 namePrefix: namePrefix,
316 log: log,
317 fs: fs,
318 );
319 }
320
321 @override
322 Future<bool> compare(Uint8List imageBytes, Uri golden) async {
323 await skiaClient.imgtestInit();
324 golden = _addPrefix(golden);
325 await update(golden, imageBytes);
326 final File goldenFile = getGoldenFile(golden);
327 return skiaClient.imgtestAdd(golden.path, goldenFile);
328 }
329
330 /// Decides based on the current environment if goldens tests should be
331 /// executed through Skia Gold.
332 static bool isForEnvironment(Platform platform) {
333 final bool luciPostSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
334 && platform.environment.containsKey('GOLDCTL')
335 // Luci tryjob environments contain this value to inform the [FlutterPreSubmitComparator].
336 && !platform.environment.containsKey('GOLD_TRYJOB')
337 // Only run on main branch.
338 && _isMainBranch(platform.environment['GIT_BRANCH']);
339 return luciPostSubmit;
340 }
341}
342
343/// A [FlutterGoldenFileComparator] for testing golden images before changes are
344/// merged into the master branch. The comparator executes tryjobs using the
345/// [SkiaGoldClient].
346///
347/// See also:
348///
349/// * [GoldenFileComparator], the abstract class that
350/// [FlutterGoldenFileComparator] implements.
351/// * [FlutterPostSubmitFileComparator], another
352/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
353/// dashboard in post-submit.
354/// * [FlutterLocalFileComparator], another
355/// [FlutterGoldenFileComparator] that tests golden images locally on your
356/// current machine.
357class FlutterPreSubmitFileComparator extends FlutterGoldenFileComparator {
358 /// Creates a [FlutterPreSubmitFileComparator] that will test golden file
359 /// images against baselines requested from Flutter Gold.
360 ///
361 /// The [fs] parameter is useful in tests, where the default
362 /// file system can be replaced by mock instances.
363 FlutterPreSubmitFileComparator(
364 super.basedir,
365 super.skiaClient, {
366 required super.fs,
367 required super.platform,
368 super.namePrefix,
369 required super.log,
370 });
371
372 /// Creates a new [FlutterPreSubmitFileComparator] that mirrors the
373 /// relative path resolution of the default [goldenFileComparator].
374 ///
375 /// The [goldens] parameter is visible for testing purposes only.
376 static Future<FlutterGoldenFileComparator> fromLocalFileComparator({
377 SkiaGoldClient? goldens,
378 required LocalFileComparator localFileComparator,
379 required Platform platform,
380 Directory? testBasedir,
381 String? namePrefix,
382 required LogCallback log,
383 required FileSystem fs,
384 required ProcessManager process,
385 required io.HttpClient httpClient,
386 }) async {
387 final Directory baseDirectory = testBasedir ?? FlutterGoldenFileComparator.getBaseDirectory(
388 localFileComparator,
389 platform: platform,
390 suffix: 'flutter_goldens_presubmit.',
391 fs: fs,
392 );
393
394 if (!baseDirectory.existsSync()) {
395 baseDirectory.createSync(recursive: true);
396 }
397
398 goldens ??= SkiaGoldClient(
399 baseDirectory,
400 platform: platform,
401 log: log,
402 fs: fs,
403 process: process,
404 httpClient: httpClient,
405 );
406
407 await goldens.auth();
408 return FlutterPreSubmitFileComparator(
409 baseDirectory.uri,
410 goldens,
411 platform: platform,
412 namePrefix: namePrefix,
413 log: log,
414 fs: fs,
415 );
416 }
417
418 @override
419 Future<bool> compare(Uint8List imageBytes, Uri golden) async {
420 await skiaClient.tryjobInit();
421 golden = _addPrefix(golden);
422 await update(golden, imageBytes);
423 final File goldenFile = getGoldenFile(golden);
424
425 await skiaClient.tryjobAdd(golden.path, goldenFile);
426
427 // This will always return true since golden file test failures are managed
428 // in pre-submit checks by the flutter-gold status check.
429 return true;
430 }
431
432 /// Decides based on the current environment if goldens tests should be
433 /// executed as pre-submit tests with Skia Gold.
434 static bool isForEnvironment(Platform platform) {
435 final bool luciPreSubmit = platform.environment.containsKey('SWARMING_TASK_ID')
436 && platform.environment.containsKey('GOLDCTL')
437 && platform.environment.containsKey('GOLD_TRYJOB')
438 // Only run on the main branch
439 && _isMainBranch(platform.environment['GIT_BRANCH']);
440 return luciPreSubmit;
441 }
442}
443
444/// A [FlutterGoldenFileComparator] for testing conditions that do not execute
445/// golden file tests.
446///
447/// Currently, this comparator is used on Cirrus, or in Luci environments when executing tests
448/// outside of the flutter/flutter repository.
449///
450/// See also:
451///
452/// * [FlutterPostSubmitFileComparator], another [FlutterGoldenFileComparator]
453/// that tests golden images through Skia Gold.
454/// * [FlutterPreSubmitFileComparator], another
455/// [FlutterGoldenFileComparator] that tests golden images before changes are
456/// merged into the master branch.
457/// * [FlutterLocalFileComparator], another
458/// [FlutterGoldenFileComparator] that tests golden images locally on your
459/// current machine.
460class FlutterSkippingFileComparator extends FlutterGoldenFileComparator {
461 /// Creates a [FlutterSkippingFileComparator] that will skip tests that
462 /// are not in the right environment for golden file testing.
463 FlutterSkippingFileComparator(
464 super.basedir,
465 super.skiaClient,
466 this.reason, {
467 super.namePrefix,
468 required super.platform,
469 required super.log,
470 required super.fs,
471 });
472
473 /// Describes the reason for using the [FlutterSkippingFileComparator].
474 final String reason;
475
476 /// Creates a new [FlutterSkippingFileComparator] that mirrors the
477 /// relative path resolution of the given [localFileComparator].
478 static FlutterSkippingFileComparator fromLocalFileComparator(
479 String reason, {
480 required LocalFileComparator localFileComparator,
481 String? namePrefix,
482 required Platform platform,
483 required LogCallback log,
484 required FileSystem fs,
485 required ProcessManager process,
486 required io.HttpClient httpClient,
487 }) {
488 final Uri basedir = localFileComparator.basedir;
489 final SkiaGoldClient skiaClient = SkiaGoldClient(
490 fs.directory(basedir),
491 platform: platform,
492 log: log,
493 fs: fs,
494 process: process,
495 httpClient: httpClient,
496 );
497 return FlutterSkippingFileComparator(
498 basedir,
499 skiaClient,
500 reason,
501 namePrefix: namePrefix,
502 platform: platform,
503 log: log,
504 fs: fs,
505 );
506 }
507
508 @override
509 Future<bool> compare(Uint8List imageBytes, Uri golden) async {
510 log('Skipping "$golden" test: $reason');
511 return true;
512 }
513
514 @override
515 Future<void> update(Uri golden, Uint8List imageBytes) async {}
516
517 /// Decides, based on the current environment, if this comparator should be
518 /// used.
519 ///
520 /// If we are in a CI environment, LUCI or Cirrus, but are not using the other
521 /// comparators, we skip. Otherwise we would fallback to the local comparator,
522 /// for which failures cannot be resolved in a CI environment.
523 static bool isForEnvironment(Platform platform) {
524 return platform.environment.containsKey('SWARMING_TASK_ID')
525 // Some builds are still being run on Cirrus, we should skip these.
526 || platform.environment.containsKey('CIRRUS_CI');
527 }
528}
529
530/// A [FlutterGoldenFileComparator] for testing golden images locally on your
531/// current machine.
532///
533/// This comparator utilizes the [SkiaGoldClient] to request baseline images for
534/// the given device under test for comparison. This comparator is initialized
535/// when conditions for all other [FlutterGoldenFileComparator]s have not been
536/// met, see the `isForEnvironment` method for each one listed below.
537///
538/// The [FlutterLocalFileComparator] is intended to run on local machines and
539/// serve as a smoke test during development. As such, it will not be able to
540/// detect unintended changes on environments other than the currently executing
541/// machine, until they are tested using the [FlutterPreSubmitFileComparator].
542///
543/// See also:
544///
545/// * [GoldenFileComparator], the abstract class that
546/// [FlutterGoldenFileComparator] implements.
547/// * [FlutterPostSubmitFileComparator], another
548/// [FlutterGoldenFileComparator] that uploads tests to the Skia Gold
549/// dashboard.
550/// * [FlutterPreSubmitFileComparator], another
551/// [FlutterGoldenFileComparator] that tests golden images before changes are
552/// merged into the master branch.
553/// * [FlutterSkippingFileComparator], another
554/// [FlutterGoldenFileComparator] that controls post-submit testing
555/// conditions that do not execute golden file tests.
556class FlutterLocalFileComparator extends FlutterGoldenFileComparator with LocalComparisonOutput {
557 /// Creates a [FlutterLocalFileComparator] that will test golden file
558 /// images against baselines requested from Flutter Gold.
559 ///
560 /// The [fs] parameter is useful in tests, where the default
561 /// file system can be replaced by mock instances.
562 FlutterLocalFileComparator(
563 super.basedir,
564 super.skiaClient, {
565 required super.fs,
566 required super.platform,
567 required super.log,
568 });
569
570 /// Creates a new [FlutterLocalFileComparator] that mirrors the
571 /// relative path resolution of the given [localFileComparator].
572 ///
573 /// The [goldens] and [baseDirectory] parameters are
574 /// visible for testing purposes only.
575 static Future<FlutterGoldenFileComparator> fromLocalFileComparator({
576 SkiaGoldClient? goldens,
577 required LocalFileComparator localFileComparator,
578 required Platform platform,
579 Directory? baseDirectory,
580 required LogCallback log,
581 required FileSystem fs,
582 required ProcessManager process,
583 required io.HttpClient httpClient,
584 }) async {
585 baseDirectory ??= FlutterGoldenFileComparator.getBaseDirectory(
586 localFileComparator,
587 platform: platform,
588 fs: fs,
589 );
590
591 if (!baseDirectory.existsSync()) {
592 baseDirectory.createSync(recursive: true);
593 }
594
595 goldens ??= SkiaGoldClient(
596 baseDirectory,
597 platform: platform,
598 log: log,
599 fs: fs,
600 process: process,
601 httpClient: httpClient,
602 );
603 try {
604 // Check if we can reach Gold.
605 await goldens.getExpectationForTest('');
606 } on io.OSError catch (_) {
607 return FlutterSkippingFileComparator(
608 baseDirectory.uri,
609 goldens,
610 'OSError occurred, could not reach Gold. '
611 'Switching to FlutterSkippingGoldenFileComparator.',
612 platform: platform,
613 log: log,
614 fs: fs,
615 );
616 } on io.SocketException catch (_) {
617 return FlutterSkippingFileComparator(
618 baseDirectory.uri,
619 goldens,
620 'SocketException occurred, could not reach Gold. '
621 'Switching to FlutterSkippingGoldenFileComparator.',
622 platform: platform,
623 log: log,
624 fs: fs,
625 );
626 } on FormatException catch (_) {
627 return FlutterSkippingFileComparator(
628 baseDirectory.uri,
629 goldens,
630 'FormatException occurred, could not reach Gold. '
631 'Switching to FlutterSkippingGoldenFileComparator.',
632 platform: platform,
633 log: log,
634 fs: fs,
635 );
636 }
637
638 return FlutterLocalFileComparator(
639 baseDirectory.uri,
640 goldens,
641 platform: platform,
642 log: log,
643 fs: fs,
644 );
645 }
646
647 @override
648 Future<bool> compare(Uint8List imageBytes, Uri golden) async {
649 golden = _addPrefix(golden);
650 final String testName = skiaClient.cleanTestName(golden.path);
651 late String? testExpectation;
652 testExpectation = await skiaClient.getExpectationForTest(testName);
653
654 if (testExpectation == null || testExpectation.isEmpty) {
655 log(
656 'No expectations provided by Skia Gold for test: $golden. '
657 'This may be a new test. If this is an unexpected result, check '
658 'https://flutter-gold.skia.org.\n'
659 'Validate image output found at $basedir'
660 );
661 update(golden, imageBytes);
662 return true;
663 }
664
665 ComparisonResult result;
666 final List<int> goldenBytes = await skiaClient.getImageBytes(testExpectation);
667
668 result = await GoldenFileComparator.compareLists(
669 imageBytes,
670 goldenBytes,
671 );
672
673 if (result.passed) {
674 result.dispose();
675 return true;
676 }
677
678 final String error = await generateFailureOutput(result, golden, basedir);
679 result.dispose();
680 throw FlutterError(error);
681 }
682}
683

Provided by KDAB

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