Icons are inherent to all applications. Even if you can use the Material Icons included in Flutter, you might need custom ones. I was surprised to see people still using flutter_svg or other methods to handle icon rendering.
In this tutorial, I will guide you on how to import icons in your Flutter application and render those icons in tests. Stay till the end cause there is a bonus tip on separating the icons in your UI package.
FlutterIcon
First, download all your icons in a svg
format. If you're using Figma, you can easily select multiple icons and export them in a single click.
Then go to FlutterIcon and import your svg
icons. You can then select the icons you want to use in your application to create a custom font that will only contain your icons.
Once you've selected the icons, you can click on 'Download.'
You'll find a font, a dart file, and a config file in the downloaded zip.
Use the icons in the application.
You can drop the dart file anywhere in your application. It contains comments with instructions at the top.
Add the font file in a font
folder at the root of your project and then modify your pubspec.yaml
to add the following code:
flutter:
uses-material-design: true
fonts:
- family: MyFlutterApp
fonts:
- asset: fonts/MyFlutterApp.ttf
And that's it!
You can use your new icons in any part of your code as a standard Icon:
Icon(MyFlutterApp.access_alarm)
Test the icons
Imagine that you have a component that contains an Icon, and you want to do a golden test with it:
class TestComponent extends StatelessWidget {
const TestComponent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [Icon(MyFlutterApp.access_alarm), Text('This is a test')],
);
}
}
...
testWidgets('$TestComponent should render', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: Center(child: TestComponent()))));
await expectLater(
find.byType(TestComponent),
matchesGoldenFile('test_component.png'),
);
});
The output of your golden will be something like that:
The icon is a small square, not very useful to see if everything is aligned correctly!
Let's change that and create a loadFonts
function!
My code is mainly based on the font loader present in the Flutter Gallery, so you can always check how the Flutter team did it here!
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
/// Load fonts for goldens tests.
Future<void> loadFonts() async {
await _load(_loadFontFile());
}
Map<String, List<Future<ByteData>>> _loadFontFile() {
final fontFamilyToData = <String, List<Future<ByteData>>>{};
final currentDir = path.dirname(Platform.script.path);
final fontsDirectory = path.join(
currentDir,
// If you put your font in a different folder, change it here!
'fonts',
);
for (final 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) {
final loader = FontLoader(entry.key);
for (final data in entry.value) {
loader.addFont(data);
}
waitList.add(loader.load());
}
await Future.wait(waitList);
}
You then have to use this function in you main
function:
void main() async {
await loadFonts();
testWidgets('$TestComponent should render', (tester) async {
And that's it. Your icon will render in the tests, and it's now correctly positioned!
Bonus: package your icons
If you need to reuse your icons in different packages, you might want to package them in another flutter project.
For this example, we'll have the icons in a separate project_theme
package.
If you import the MyFlutterApp
from the package, everything will work, but when you run your app:
The asset is not found!
To solve this, you have to indicate your package name in the dart
font file generated by FlutterIcon:
class MyFlutterApp {
MyFlutterApp._();
static const _kFontFam = 'MyFlutterApp';
static const String? _kFontPkg = 'project_theme'; //here
Everything should be back to normal. But we still need to fix the tests. There are now two places where we need to import the icons:
- tests of your main flutter app
project
- tests of the theme package
project_theme
Test in project
Point to the directory of the font. If the two projects are side by side, you can do something like that:
final fontsDirectory = path.join(
currentDir,
// If you put your font in a different folder, change it here!
'../project_theme/fonts',
);
And then, you have to add your package name as a prefix:
const prefix = 'packages/project_theme/';
...
final fontFamily = prefix + path.basenameWithoutExtension(file.path).split('-').first;
Test in project_theme
You only have to add the prefix for the theme package since you already have the font in the right place.
Conclusion
I hope this article will help you handle your icons more straightforwardly! I spent a lot of time figuring out why my icons weren't showing up in my tests and thought I should dive a little further to understand everything.
You can always contact me on Twitter if you have any other questions and forget to subscribe to my newsletter not to miss any of my future articles!