Material Symbolsを簡単にFlutterプロジェクトに組み込めるツールを作ってみた

みなさんはFlutterアプリでアイコンが必要な時、どうしていますか？Material Designなら`Icons`クラス、iOSなら`CupertinoIcons`クラスでしょうか？Flutterは便利なコンポーネントが豊富に揃っているのが良いですよね。ですが足りないものが1つあります。そう、それは[Material Symbols](https://m3.material.io/styles/icons/overview)です。残念ながらFlutterはMaterial Symbolsを標準でサポートしていません。そこで**fms（flutter-material-symbols）** というCLIツールを作りました。fmsを使えば`Icons`や`CupertinoIcons`と同じ感覚でMaterial Symbolsを使えるようになります。アイコンのリソースを手動でダウンロード・管理する必要もありません。

https://pub.dev/packages/fms



以下前置きです。本題は[ココ](#fmsの使い方)から。



## 背景



### Material Symbols：2022年に登場したMaterial Designの新しいアイコンセット

Material Designの最新版である[Material3](https://m3.material.io/)ではアイコンも刷新され、これまでの[Material Icons](https://m2.material.io/design/iconography/system-icons.html)（以下、MIcons）に代わり[Material Symbols](https://m3.material.io/styles/icons/overview)（以下、MSymbols）が新たに[導入](https://material.io/blog/introducing-symbols)されました。MIconsとMSymbolsの大きな違いは[可変フォント](https://developer.mozilla.org/ja/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide)になったことです。そのためMSymbolsのアイコン（以下、シンボル）はスタイルと4つの軸、合わせて5つのパラメータによってカスタマイズすることができます。それぞれのパラメータがどう作用するのかは[公式のギャラリーサイト](https://fonts.google.com/icons)を見るのが早いでしょう。

[![material-symbols-axes](https://lh3.googleusercontent.com/KEagCi3RJ9wwf4o5OfqRGq1oooyjVLJg_5A3aVq1XYEuOSmFqZlBvwo6ycoE2YMwYIUo1r9AUbQh_oLd4YMvzcIzz80AiKR7nOnWYxR8TCbpYo4YlYJV=w1400-v0)](https://m3.material.io/styles/icons/overview#8a94b4ec-c2c5-4dc7-b392-9f89b75904bd)



### Flutter SDKはMaterial Symbolsに未対応

現時点ではFlutter SDKの中にMSymbolsは含まれていません。`Icons`クラスは？と思う方もいるかもしれませんが、残念ながら`Icons`クラスはMIconsしかサポートしていません。

> Identifiers for the supported Material Icons.
>
> *[Icons class - material library - Dart API](https://api.flutter.dev/flutter/material/Icons-class.html)*



### 公式対応はもう少し先になりそう

GitHubでは[Issue](https://github.com/flutter/flutter/issues/102560)が上がっており、現在対応が進められている状況です。ただ昨年の9月頃からあまり動きが見られず、Stableチャンネルに実装されるのはまだ先になりそうです。

ちなみにですが、[Design Doc](https://docs.google.com/document/d/1UHRKDl8-lzl_hW_K2AHnpMwvdPo0vGPbDI7mqACWXJY/edit#)によるとMSymbolsはSDKには含まれず、別パッケージとして提供される予定だそうです。またシンボルのカスタマイズは、`Icon`Widgetのコンストラクタに4つの軸に対応した引数をそれぞれ追加することで実現されるようです。実装はまだのはずですが、`Icon`クラスのドキュメントにはすでに新しいコンストラクタ引数が追加されています。

> [Icon](https://api.flutter.dev/flutter/widgets/Icon/Icon.html)([IconData](https://api.flutter.dev/flutter/widgets/IconData-class.html)? icon, {[Key](https://api.flutter.dev/flutter/foundation/Key-class.html)? key, [double](https://api.flutter.dev/flutter/dart-core/double-class.html)? size, [double](https://api.flutter.dev/flutter/dart-core/double-class.html)? fill, [double](https://api.flutter.dev/flutter/dart-core/double-class.html)? weight, [double](https://api.flutter.dev/flutter/dart-core/double-class.html)? grade, [double](https://api.flutter.dev/flutter/dart-core/double-class.html)? opticalSize, [Color](https://api.flutter.dev/flutter/dart-ui/Color-class.html)? color, [List](https://api.flutter.dev/flutter/dart-core/List-class.html)<[Shadow](https://api.flutter.dev/flutter/dart-ui/Shadow-class.html)>? shadows, [String](https://api.flutter.dev/flutter/dart-core/String-class.html)? semanticLabel, [TextDirection](https://api.flutter.dev/flutter/dart-ui/TextDirection.html)? textDirection})
>
> Creates an icon.
>
> *[Icon constructor - Icon - widgets library - Dart API](https://api.flutter.dev/flutter/widgets/Icon/Icon.html)*



### 自前でなんとかする方法はある

じゃあMSymbolsはまだ使えない？いいえ、そんなことはありません。幸いなことにMSymbolsはOSSであり、全てのリソースがGitHubで公開されています。またシンボルを一覧できる[公式のギャラリーサイト](https://fonts.google.com/icons)からは簡単にSVGファイルが手に入りますので、あとは[flutter_svg](https://pub.dev/packages/flutter_svg)などのパッケージを使って

```dart
child: IconButton(
  icon: SvgPicture.asset('path/to/svg/file'),
),
```

とかすればすぐにプロジェクトにMSymbolsを組み込めます。

とはいえこれだとパスを書かないといけないし、IDEの補完も効きません。できれば`Icons`クラスみたいに`Icons.home`とか書きたいですよね。そのためには拾ってきたSVGを1つのアイコンフォントにまとめる必要があります。それにはnode.jsパッケージの[fantasticon](https://github.com/tancredi/fantasticon) が使えます。これはSVGからアイコンフォントを生成してくれるCLIツールです。アイコンフォントができたら、`Icons`や`CupertinoIcons`のようなラッパークラスを書きましょう。

ラッパークラスなんてどうやって書くんじゃい！安心してください、[icon_font_generator](https://github.com/ScerIO/icon_font_generator)があります。これはSVGからアイコンフォントの生成、そしてラッパークラスの生成まで全部やってくれるCLIツールです。しかもDart製なので`pub get`でインストールできます。さあ、あとは必要なSVGを拾い集めてくるだけです。



### で、fmsが生まれたってわけ

「あとは必要なSVGを拾い集めてくるだけ」の部分も自動化したのが**fms（flutter-material-symbols）** パッケージです。fmsは内部でfantasticonとicon_font_generatorを利用しているため、リスペクトの意味を込めて上記のパッケージを紹介いたしました。fmsを使えば設定ファイルからSVGの収集、アイコンフォント、ラッパークラスの生成までコマンド一発です。リソースの管理を手動でする必要はありません。


### なぜわざわざ「生成」するのか？

MIconsは全アイコンの`IconData`が`Icons`クラスにstataic変数として定義されています。一方でMSymbolsの場合シンボル自体が2500種類ほどあり、それぞれが5つのパラメータを持つためその組み合わせは途方も無い数になります。そのため各シンボルの全バリエーションをstatic変数として提供する場合、巨大なアイコンフォントをパッケージに含めることになります。そのためfmsでは必要なアイコンだけを含むフォントを生成するという方針をとりました。



## fmsの使い方

全て説明すると長くなってしまうので、ここには概要だけ記します。詳しい使い方に興味のある方は[README.md](https://github.com/fujidaiti/fms)をご覧ください。



### インストール

`pub`コマンドでPub.devからインストールできます。

```shell
$ flutter pub add --dev fms
```

fmsは内部でnode.jsパッケージのfantasticonを利用しています。バージョン11以降のnode.jsが入っていない場合は別途インストールが必要です。

```shell
$ node --version   
v18.12.1
```



### Getting started

例として[NavigationBar](https://m3.material.io/components/navigation-bar/overview)のタブに使うアイコンを用意するケースを考えます。選択状態と未選択状態を区別するために2種類のホームアイコン🏠（アウトラインのみ、塗りつぶし）を作ります。

![kyki80zd-nav-bar_navigation_3P_AdobeExpress-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112065/32288065-bb31-a813-32aa-63174d395849.gif)


1. 設定ファイルを書く

   fmsの設定ファイルはYAMLで書きます。この例では`my_symbols.yaml`という名前でプロジェクトルートに置いていますが、どこに置いても構いません。設定ファイルには生成されるアイコンフォントのファミリー名、アイコンフォントとラッパークラスの出力先、そして使用したいシンボルの情報を記述します。

   ```yaml
   # project_root/my_symbols.yaml
   
   family: MySymbols # ファミリー名
   
   output:
     flutter: lib/src/my_symbols.dart # ラッパークラス
     font: assets/my_symbols.ttf # アイコンフォント
   
   # 使用したいシンボルの情報
   symbols:
     home: Home # 未選択状態のHomeシンボル（アウトラインのみ）
     home_selected: # 選択状態のHomeシンボル
       name: Home
       fill: true # 塗りつぶし
   ```

   fmsはこの設定ファイルからラッパークラス`MySymbols`を生成し、各アイコンには`MySymbols.home_selected`のようにアクセスできるようになります。

   

2. アイコンフォントとラッパークラスを生成

   以下のコマンドは`assets/my_symbols.ttf`にアイコンフォントが、`lib/src/my_symbols.dart`にラッパークラスをそれぞれ生成します。

   ```shell
   $ flutter pub run fms build my_symbols.yaml
   ```

   実行すると`lib/src/my_symbols.dart`に下記のようなラッパークラスが生成されます。


   ```dart
    import 'package:flutter/widgets.dart';

    // GENERATED CODE - DO NOT MODIFY BY HAND
    // ignore_for_file: non_constant_identifier_names
    // ignore_for_file: constant_identifier_names
    @immutable
    class _MySymbolsData extends IconData {
      const _MySymbolsData(int codePoint, this.name)
          : super(
              codePoint,
              fontFamily: 'MySymbols',
            );

      final String name;
    }

    @immutable
    class MySymbols {
      const MySymbols._();

      static const home_focused = _MySymbolsData(0xf102, 'home_focused');
      static const home = _MySymbolsData(0xf103, 'home');

      static const all = <String, _MySymbolsData>{
        'home_focused': home_focused,
        'home': home,
      };
    }
   ```

3. アイコンフォントの情報を`pubspec.yaml`に追加

   `MySymbols`を使うためには対応するアイコンフォントをFlutterに認識させる必要があります。`pubspec.yaml`の`flutter:`セクションに生成された`my_symbols.ttf`を追加します。

   またフォントファイルを`lib/`以外の場所（例えば`assets/`）に置く場合は`assets:`セクションで[アセットとして追加する](https://docs.flutter.dev/development/ui/assets-and-images#specifying-assets)ことも忘れないでください。

   ```yaml
   ...
   
   flutter:
     assets:
       - assets/
     fonts:
       - family: MySymbols # ファミリー名
         fonts:
           - asset: assets/my_symbols.ttf # フォントファイル
   ```



4. 生成されたアイコンを使う

   あとは`Icons`や`CurpetinoIcons`と同じように`MySymbols`クラスから必要なアイコンを利用するだけです。もちろん`const`も使えますし補完も効きます。やったね！

   ```dart
   import 'package:your_package/src/my_symbols.dart';
   import 'package:flutter/material.dart';
   
   Widget homeNaviDest() {
     return NavigationDestination({
       icon: const Icon(MySymbols.home),
       selectedIcon: const Icon(MySymbols.home_selected),
     });
   }
   ```

## 終わりに

Material Symbolsでググってもあまり情報が出てこないのだけれど、もしかして認知度低いのか、、？