🖋️

【Flutter】フォントを変更する

2024/12/06に公開

初めに

アプリに使用されるフォントは、アプリ全体のイメージに直結する重要な要素であるため、サービスのイメージに合わせたフォントを選択することでより統一感のあるデザインになります。今回は Flutter のアプリでカスタムのフォントを使用する方法について簡単にまとめていきたいと思います。

記事の対象者

  • Flutter 学習者
  • Flutter アプリでフォントを変更したい方

目的

今回の目的はアプリに使用するフォントをカスタムする方法をまとめることです。
カスタムしたフォントを導入した際に起こりがちな問題の対処についてもまとめています。

実装

実装は以下の3ステップで進めていきたいと思います。
フォントをどのように変更するかのみを知りたい方はステップ1のみでも良いかなと思います。

  1. フォントの変更実装
  2. Slack クローンのUI実装
  3. フォントを変更した際の表示が崩れる問題の解消

1. フォントの変更実装

まずはどのようにフォントを変更するかをまとめていきます。
実装は以下の手順で進めていきます。

  1. フォントのダウンロード
  2. pubspeck.yaml の編集
  3. main.dart の編集

1. フォントのダウンロード

まずは使用するフォントのダウンロードを行います。
フォントは Google Fonts などでダウンロードできます。

今回は次の章の Slack のクローンで使用する Lato というフォントを以下からダウンロードしてきます。
https://fonts.google.com/specimen/Lato

ダウンロードが完了したら Zip ファイルを解凍して、Flutter プロジェクトの assets/fonts のパスに追加します。フォントを追加すると以下のようになっているかと思います。

これでフォントのダウンロードは完了です。

2. pubspeck.yaml の編集

次にダウンロードしたフォントを読み込むために pubspec.yaml を編集していきます。
- assets にフォントが含まれるディレクトリのパスを追加します。
- family にはフォントが含まれるディレクトリの名前を追加します。
fonts ではそれぞれのフォントがどの FontWeight に対応するかを定義していきます。
FontWeight の割り当ては公式ドキュメントの FontWeight class をもとに決めると良いかと思います。

pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/fonts/

  fonts:
    - family: Lato
      fonts:
        - asset: assets/fonts/Lato/Lato-Thin.ttf
          weight: 100
        - asset: assets/fonts/Lato/Lato-Light.ttf
          weight: 300
        - asset: assets/fonts/Lato/Lato-Regular.ttf
          weight: 400
        - asset: assets/fonts/Lato/Lato-Bold.ttf
          weight: 700
        - asset: assets/fonts/Lato/Lato-Black.ttf
          weight: 900

pubspec.yaml の編集が終わったら flutter pub get を実行して設定は完了です。

3. main.dart の編集

先程追加した Loto フォントを特定の箇所のみで使用したい場合は以下のように fontFamily に割り当てることでそれぞれのテキストで使用することができます。

Text(
  'Loto のテキスト',
  style: TextStyle(
    fontFamily: 'Lato',
  ),
),

一方で、追加したカスタムフォントをアプリ全体で使用したい場合は main.dart で以下のコードのように MaterialApptheme にカスタムフォントを割り当てると、アプリ全体でそのフォントを使用するようになります。

lib/main.dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
+       fontFamily: 'Lato',  // デフォルトで適用
      ),
      home: const FontSampleScreen(),
    );
  }
}

これでフォントの変更は完了です。

2. Slack クローンのUI実装

次にカスタムしたフォントをもとにアプリの画面を作ってみたいと思います。
今回は、フォントをカスタムしたアプリとして Slack を例に取り上げて実装してみたいと思います。

作成するのは一つの画面のみであるため、コードを以下に提示します。

lib/screens/slack_sample_screen.dart
import 'package:flutter/material.dart';

class SlackSampleScreen extends StatelessWidget {
  const SlackSampleScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Padding(
          padding: const EdgeInsets.all(8),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(10),
            child: Container(
              padding: const EdgeInsets.all(8),
              color: Colors.purple,
              child: const Icon(
                Icons.home,
                color: Colors.white,
              ),
            ),
          ),
        ),
        title: const Text(
          'Slack Sample Group',
          style: TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
        actions: [
          Padding(
            padding: const EdgeInsets.all(8),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(10),
              child: Container(
                padding: const EdgeInsets.all(8),
                color: Colors.purple,
                child: const Icon(
                  Icons.person,
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(8),
              child: TextField(
                decoration: InputDecoration(
                  hintText: 'ジャンプまたは検索',
                  prefixIcon: const Icon(Icons.search),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8.0),
                  ),
                ),
              ),
            ),
            Row(
              children: [
                _buildCard(Icons.mail, 'キャッチアップ', '新規 16 件'),
                _buildCard(Icons.chat, 'スレッド', '新規 8 件'),
                _buildCard(Icons.headset, 'ハドルミーティ', '0 件のライブ'),
              ],
            ),
            const Divider(),
            _buildSectionTitle('メンション'),
            _buildChannel('general', '3'),
            _buildChannel('dev-all', '1'),
            const Divider(),
            _buildSectionTitle('未読'),
            _buildChannel('design'),
            _buildChannel('dev-flutter'),
            _buildChannel('random'),
            _buildChannel('times-slack'),
          ],
        ),
      ),
    );
  }

  Widget _buildCard(IconData icon, String title, String subtitle) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 8,
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(icon, size: 20, color: Colors.purple),
            Text(title, textAlign: TextAlign.center),
            Text(
              subtitle,
              style: const TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.symmetric(
        vertical: 8.0,
        horizontal: 16.0,
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            title,
            style: const TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          IconButton(
            onPressed: () {},
            icon: const Icon(
              Icons.keyboard_arrow_down,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildChannel(String name, [String? count]) {
    return ListTile(
      title: Text(
        '# $name',
        style: const TextStyle(fontWeight: FontWeight.bold),
      ),
      trailing: count != null
          ? CircleAvatar(
              backgroundColor: Colors.purple,
              radius: 12,
              child: Text(
                count,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 12,
                ),
              ),
            )
          : null,
    );
  }
}

main.dart で Loto をデフォルトのフォントに指定した状態で実行して、通常のフォントと比べてみると以下のようになります。多少印象が異なるのがわかるかと思います。

通常フォント Loto フォント

3. フォントを変更した際の表示が崩れる問題の解消

最後に、フォントを変更した際に表示が崩れる問題について触れておきたいと思います。

問題の詳細

使用するフォントを変更した場合、以下のような特殊文字が表示できなくなることがあります。

  • 𝑓𝑙𝑢𝑡𝑡𝑒𝑟
  • 𝚏𝚕𝚞𝚝𝚝𝚎𝚛
  • ᶠˡᵘᵗᵗᵉʳ

この問題が発生するかどうかは、アプリを実行しているデバイスに左右されます。
例えば、Pixel 6a API VanillaIceCream:5554 の Android Emulator で特殊文字を含む以下のコードを実行すると、以下の画像のように表示が崩れてしまいます。

class FontSampleScreen extends StatelessWidget {
  const FontSampleScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'フォントサンプル',
        ),
      ),
      body: const Padding(
        padding: EdgeInsets.all(8.0),
        child: Center(
          child: Text(
            '特殊文字: ᶠˡᵘᵗᵗᵉʳ',
            fontFamily: 'Lato',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}

実行結果

問題の原因

この問題の原因は、使用しているフォントが特殊文字をサポートしていないことです。
今回であれば fontFamily に指定している Loto のフォントが「ᶠˡᵘᵗᵗᵉʳ」の文字をサポートしていないことが原因です。

解決方法

問題の解決方法は、特殊文字を含めた多くの文字をサポートしているフォントを導入して、それをfontFamilyFallback というプロパティに新たに指定することです。

今回は以下の Noto Sans という広く使用されているフォントを新たにダウンロードして、これまで Loto のフォントで行なった手順通りに実装を進めて、プロジェクトで Noto Sans フォントも使用できるようにします。

https://fonts.google.com/noto/specimen/Noto+Sans

そして先程のコードに fontFamilyFallback を加えて以下のように編集します。

import 'package:flutter/material.dart';

class FontSampleScreen extends StatelessWidget {
  const FontSampleScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'フォントサンプル',
        ),
      ),
      body: const Padding(
        padding: EdgeInsets.all(8.0),
        child: Center(
          child: Text(
            '特殊文字: ᶠˡᵘᵗᵗᵉʳ',
            style: TextStyle(
              fontSize: 24,
              fontFamily: 'Lato',
+             fontFamilyFallback: ['NotoSans'],
              ),
          ),
        ),
      ),
    );
  }
}

すると以下の画像のように正しく表示されるようになるかと思います。

fontFamilyFallback においてリスト形式で指定されているフォントは、 fontFamily に指定されているフォントで文字や記号が表示できなかった場合にリストの初めから優先して表示されます。

今回は fontFamily に指定されている Loto フォントで「ᶠˡᵘᵗᵗᵉʳ」の文字が表示できなかったため、fontFamilyFallback に指定されている NotoSans フォントで表示されるようになっています。

ちなみに、この fontFamilyFallback もアプリ全体で適用することができ、 main.dart で以下のようにすれば、 Loto フォントで表示できなかった文字や記号が NotoSans フォントで表示されるようになります。

lib/main.dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        fontFamily: 'Lato',
+       fontFamilyFallback: const ['NotoSans'],
      ),
      home: const FontSampleScreen(),
    );
  }
}

以上です。

まとめ

最後まで読んでいただいてありがとうございました。

今回はフォントの変更方法についてまとめました。
冒頭でも述べた通り、フォントが変わるとアプリ全体のイメージも大きく変わるため、使用したいフォントがどの程度文字や記号をサポートしているかなどを慎重に見極めつつ選択できたら良いと思います。
また、3章で扱ったような対処法もしておくと良いかなと思います。

誤っている点等あればご指摘いただければ幸いです。

参考

https://zenn.dev/susatthi/articles/20220419-143426-flutter-custom-fonts

https://api.flutter.dev/flutter/dart-ui/FontWeight-class.html

https://api.flutter.dev/flutter/painting/TextStyle/fontFamilyFallback.html

Discussion