skip to Main Content

I have a custom made icon font .ttf which I use within my apps as IconData, allowing for use the same way you would with Flutter’s built-in material icons.

My custom font class:

class MyIcons {
    MyIcons._();

    static const iconFontFamily = 'MyIcons';
    static const iconFontPackage = 'my_icon_package';

    /// edit_outline
    static const IconData edit_outline = IconData(0xe000, fontFamily: iconFontFamily, fontPackage: iconFontPackage);


  // etc
}

Usage:

Icon(
  MyIcons.edit_outline,
  size: 24,
)

And all works well within the app. However now I am trying to generate golden test files to ensure my icons work as expected after I’ve updated the .ttf, but the icons are always only replaced with the test Ahem font squares.

If I use Flutter’s default icons, and set uses-material-design: true in the pubspec.yaml, this allows the default icons to be rendered properly within the golden test files, however no matter what I try I cannot get my own icons to be rendered.

Other things I’ve tried and been unsuccessful with:

Is there a way to do this?

3

Answers


  1. Chosen as BEST ANSWER

    I ended up solving the gotchas with this thanks to a related question here.

    There is a way to have the best of both worlds where you can have your standalone font package and not have to declare your packaged font files in your app that is using it.

    For example, we have a company branding/typography package which we use across multiple apps that contains all our pre-configured TextStyle declarations, and another standalone package which has custom generated IconData that is stored within a *.ttf file (like FontAwesome).

    The package side:

    pubspec.yaml

    
    flutter:
      uses-material-design: true
      assets:
        - assets/fonts/
      fonts:
        - family: MyFont
          fonts:
            - asset: assets/fonts/MyFont.ttf
              weight: 400
    
        # etc
    
    

    The packaged TextStyle:

    class BrandStyles {
      static const _packageName = '<package_name>';
    
      static const headline1Style = TextStyle(
        color: Colors.black,
        fontFamily: 'MyFont',
        fontSize: 60.0,
        fontStyle: FontStyle.normal,
        fontWeight: FontWeight.w400,
        height: 1.16,
        letterSpacing: 0,
        package: _packageName,
      );
    
    
      // etc
    
    }
    

    Golden test

    void main() {
      final widget = MaterialApp(
        theme: ThemeData(
          textTheme: TextTheme(
            // use custom extension method to remove `package` value
            headline1: BrandStyles.headline1Style.trimFontPackage(),
          ),
        ),
        home: Scaffold(
          body: SafeArea(child: StylesExample()),
        ),
      );
    
      setUp(() async {
        TestWidgetsFlutterBinding.ensureInitialized();
        final file = File('path/to/packaged/asset/MyFont.ttf').readAsBytesSync();
        final bytes = Future<ByteData>.value(file.buffer.asByteData());
    
        await (FontLoader('MyFont')..addFont(bytes)).load();
      });
    
      testWidgets('Golden typography test', (WidgetTester tester) async {
        await tester.pumpWidget(widget);
        await expectLater(
            find.byType(MaterialApp), matchesGoldenFile('goldens/typography.png'));
      });
    }
    
    extension StylingExtensions on TextStyle {
      
      TextStyle trimFontPackage() {
        return TextStyle(
          inherit: inherit,
          color: color,
          backgroundColor: backgroundColor,
          fontSize: fontSize,
          fontWeight: fontWeight,
          fontStyle: fontStyle,
          letterSpacing: letterSpacing,
          wordSpacing: wordSpacing,
          textBaseline: textBaseline,
          height: height,
          locale: locale,
          foreground: foreground,
          background: background,
          shadows: shadows,
          fontFeatures: fontFeatures,
          decoration: decoration,
          decorationColor: decorationColor,
          decorationStyle: decorationStyle,
          decorationThickness: decorationThickness,
          debugLabel: debugLabel,
          /// `replaceAll` only required if loading multiple fonts, 
          /// otherwise set value to your single `fontFamily` name
          fontFamily: fontFamily.replaceAll('packages/<package_name>/', ''),
        );
      }
    }
    

    Or if like me, you have the same issue with custom icons, the same can be done within your golden test for your custom IconData with a similar extension method, removing the fontPackage value:

    extension IconExtensions on IconData {
      IconData convertToGolden() => IconData(
            this.codePoint,
            fontFamily: this.fontFamily,
          );
    }
    
    

    Your app side

    pubspec.yaml

    
    # ...
    
    dependencies:
      flutter:
        sdk: flutter
    
      <package_name>:
        git:
          url: <url_to_hosted_package>.git
          ref: <release_tag>
    
    

    main.dart

    
    class MyApp extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData.light().copyWith(
            textTheme: TextTheme(
              headline1: BrandStyles.headline1Style,
            ),
          ),
        );
      }
    
    }
    

    There is now no longer a need to declare your fonts within your apps pubspec.yaml, or even have the style package(s) within the same project/repository as your implementing app.


  2. Yes, it is possible, but you need to load the icon font manually just like any other custom font in flutter tests.

    I used parts of a solution from the Flutter Gallery demo app:
    https://github.com/flutter/gallery/blob/master/golden_test/testing/font_loader.dart

    And for material icons I used a solution found here:

    issue: https://github.com/flutter/flutter/issues/75391

    code: https://github.com/flutter/flutter/pull/74131/files

    Some gotchas are:

    • it seems like the font loading must be done outside of test function so perform it first then run the test.
    • make sure the filenames and font families are correct everywhere.

    Given that there is a correctly set up custom font called ‘Matter’ and a correctly setup custom icon font that exists in a "fonts" directory in the root of the project –>

    Demo code:

    import 'dart:io';
    import 'dart:typed_data';
    
    import 'package:[your_path_to_icon_definition_file]/custom_icons.dart';
    import 'package:file/file.dart' as f;
    import 'package:file/local.dart' as l;
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:path/path.dart' as path;
    import 'package:platform/platform.dart' as p;
    
    Future<void> main() async {
      await loadFonts();
      run();
    }
    
    void run() {
      group('mobile', () {
        testWidgets('Test font and icon loading', (tester) async {
          await tester.pumpWidget(MaterialApp(
            theme: ThemeData(
              fontFamily: 'Matter', // Define the custom font family.
            ),
            home: Scaffold(
              appBar: AppBar(
                title: Text('AppBar'),
              ),
              body: Column(
                children: [
                  Text('test'),
                  Icon(CustomIcons.camera), // Custom icon file from for example fluttericon.com
                  Icon(Icons.add), // material icons ('uses-material-design: true' Must exist in pubspec.yaml)
                ],
              ),
            ),
          ));
    
          expect(find.byType(MaterialApp), findsOneWidget);
    
          await expectLater(
            find.byType(MaterialApp),
            matchesGoldenFile('goldens/material_app.png'),
          );
        });
      });
    }
    
    /// Load fonts to make sure they show up in golden tests.
    Future<void> loadFonts() async {
      await _load(await loadFontsFromFontsDir());
      await _loadMaterialIconFont();
    }
    
    // Loads the cached material icon font.
    // Only necessary for golden tests. Relies on the tool updating cached assets before
    // running tests.
    Future<void> _loadMaterialIconFont() async {
      const f.FileSystem fs = l.LocalFileSystem();
      const p.Platform platform = p.LocalPlatform();
      final flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT']);
    
      final iconFont = flutterRoot.childFile(
        fs.path.join(
          'bin',
          'cache',
          'artifacts',
          'material_fonts',
          'MaterialIcons-Regular.otf',
        ),
      );
    
      final bytes = Future<ByteData>.value(iconFont.readAsBytesSync().buffer.asByteData());
    
      await (FontLoader('MaterialIcons')..addFont(bytes)).load();
    }
    
    /// Assumes a fonts dir in root of project
    Map<String, List<Future<ByteData>>> loadFontsFromFontsDir() {
      final fontFamilyToData = <String, List<Future<ByteData>>>{};
      final currentDir = path.dirname(Platform.script.path);
      final fontsDirectory = path.join(
        currentDir,
        'fonts',
      );
      for (var file in Directory(fontsDirectory).listSync()) {
        if (file is File) {
          final fontFamily = path.basenameWithoutExtension(file.path).split('-').first;
          (fontFamilyToData[fontFamily] ??= []).add(file.readAsBytes().then((bytes) => ByteData.view(bytes.buffer)));
        }
      }
      return fontFamilyToData;
    }
    
    Future<void> _load(Map<String, List<Future<ByteData>>> fontFamilyToData) async {
      final waitList = <Future<void>>[];
      for (final entry in fontFamilyToData.entries) {
        print('entry.key=${entry.key}');
        final loader = FontLoader(entry.key);
        for (final data in entry.value) {
          loader.addFont(data);
        }
        waitList.add(loader.load());
      }
      await Future.wait(waitList);
    }
    
    

    Resulting in:
    golden_example

    Login or Signup to reply.
  3. Thanks to the other answers i was able to solve this problem for myself and found out, that it is possible to have a shorter solution for it. Without adding extension methods and special themeData for a golden test.

    The following code snippet loads (icon-)/fonts from three different sources, a bundled asset, the material icons and fluent icons for windows.

    I hope this code will help some of you in the future to have easier handling for golden tests.

    flutter_test_config.dart

    import 'dart:async';
    
    import 'package:file/file.dart';
    import 'package:file/local.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_test/flutter_test.dart';
    import 'package:platform/platform.dart';
    
    Future<void> testExecutable(FutureOr<void> Function() testMain) async {
      setUpAll(() async {
        TestWidgetsFlutterBinding.ensureInitialized();
        // loads a font that is bundled in your package
        final fontVariable = rootBundle.load('lib/fonts/Open_Sans/OpenSans-VariableFont.ttf');
        final fontItalic = rootBundle.load('lib/fonts/Open_Sans/OpenSans-Italic-VariableFont.ttf');
    
        // here you maybe need to load more types, when you have a specific bold style as well
        final fontLoader = FontLoader('OpenSans')
          ..addFont(fontVariable)
          ..addFont(fontItalic);
    
        await fontLoader.load();
    
    
        // loads a font that is bundled in a different package, here for example from the fluent icons package (https://pub.dev/packages/fluentui_system_icons) 
    
        final fluentRegularIcon = rootBundle.load('packages/fluentui_system_icons/fonts/FluentSystemIcons-Regular.ttf');
        final fluentFilledIcon = rootBundle.load('packages/fluentui_system_icons/fonts/FluentSystemIcons-Filled.ttf');
    
        // there is no need to write extension methods or create specific themes, you only need to add the packages/package_name to the font name.
        final fluentRegularLoader = FontLoader('packages/fluentui_system_icons/FluentSystemIcons-Regular')
          ..addFont(fluentRegularIcon);
        final fluentFilledLoader = FontLoader('packages/fluentui_system_icons/FluentSystemIcons-Filled')
          ..addFont(fluentFilledIcon);
    
        await fluentRegularLoader.load();
        await fluentFilledLoader.load();
    
    
        // loads the material icon font
        final materialIcon = _loadMaterialIcon();
    
        final materialLoader = FontLoader('MaterialIcons')..addFont(materialIcon);
    
        await materialLoader.load();
      });
    
      // runs your golden tests
      await testMain();
    }
    
    // see https://github.com/flutter/flutter/pull/74131/files
    // Loads the cached material icon font.
    // Only necessary for golden tests. Relies on the tool updating cached assets before
    // running tests.
    Future<ByteData> _loadMaterialIcon() async {
      const FileSystem fs = LocalFileSystem();
      const Platform platform = LocalPlatform();
      final Directory flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT']);
    
      final File iconFont = flutterRoot.childFile(
        fs.path.join(
          'bin',
          'cache',
          'artifacts',
          'material_fonts',
          'MaterialIcons-Regular.otf',
        ),
      );
    
      final Future<ByteData> bytes = Future<ByteData>.value(
        iconFont.readAsBytesSync().buffer.asByteData(),
      );
    
      return bytes;
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search