📖

【Flutter】flutter_markdown で見た目をカスタマイズする

2021/12/21に公開

この記事は Flutter Advent Calendar(カレンダー2)の21日目の記事です。
https://qiita.com/advent-calendar/2021/flutter

はじめに

Flutter for Web で開発していてMarkdownを使うことになり flutter_markdownを導入して開発をしました。良い感じにMarkdownの見た目をカスタマイズするのに日本語の記事などがなかったのでメモ。

https://pub.dev/packages/flutter_markdown

デフォルトではこんな見た目

採用して使う場合にはでアプリのデザインに沿うことが多いと思います。ですがデフォルトのままでは統一感を出すことは難しいと思います。今回はこの見た目を良い感じにする方法を書いていきたいと思います。

基本の使い方

Markdownのdataという引数にマークダウンを含んだ文字列を渡してあげるだけです。

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';

class MarkdownScreen extends StatelessWidget {
  const MarkdownScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Markdown(
        data: '''
# h1 見出し
## h2 見出し
### h3 見出し
#### h4 見出し
##### h5 見出し
###### h6 見出し

# 水平
---

# 文字
 
**太字**

__太字__

*斜体のテキスト*

_斜体のテキスト_

~~取り消し線~~

# ブロッククォート

> ブロッククォート
>> ブロッククォート
>>>ブロッククォート

# リスト

- リスト
- リスト
  - リスト

1. リスト
2. リスト
3. リスト
  1. リスト
  2. リスト
  
# コード

インライン `code`

``` dart
print("Hello World");
```

      ''',
      ),
    );
  }
}

その1 h1の見た目をカスタマイズしてみる

今回はデフォルトの細字を太字になるようにしたいと思います。

手順

  1. インポートします
import 'package:markdown/markdown.dart' as md;
  1. h1が適応される際に表示したいウィジェットを用意します。
class H1 extends StatelessWidget {
  final String text;

  const H1({Key? key, required this.text}) : super(key: key);

  
  Widget build(BuildContext context) {
    return SelectableText.rich(
      TextSpan(
          text: text,
          style: const TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
          )),
    );
  }
}
  1. MarkdownElemendBuilderを継承したクラスを作成し、先ほど用意したH1クラスを返すようにします。
/// H1
class CustomHeader1Builder extends MarkdownElementBuilder {
  
  Widget visitText(md.Text text, TextStyle? preferredStyle) {
    return H1(text: text.text);
  }
}
  1. Markdownにbuilders引数を用意しMap型で渡してあげます。
Markdown(
  builders: { // 追加
    'h1': CustomHeader1Builder(), 
  },
  data: '''
# h1 見出し
## h2 見出し
```

完成


Before

After

その2 Pre(整形済みテキスト)を Zenn風にする

次はPreをZenn風にカスタマイズしていきます。さらに右上のクリップボードボタンを押すとコピーすることができる様にしたいと思います。


デフォルト


カスタマイズ後(Zennぽい)

手順

  1. Preが適応される際に表示したいウィジェットを用意します
class Pre extends StatelessWidget {
  final String text;

  const Pre({Key? key, required this.text}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        const SizedBox(height: 16),
        Stack(
          alignment: Alignment.topRight,
          children: [
            Container(
              width: double.infinity,
              decoration: BoxDecoration(
                color: Colors.black,
                borderRadius: BorderRadius.circular(4),
              ),
              child: Padding(
                padding: const EdgeInsets.fromLTRB(20, 20, 20, 4),
                child: SelectableText(
                  text,
                  style: Theme.of(context)
                      .textTheme
                      .caption
                      ?.copyWith(color: Colors.white),
                ),
              ),
            ),
            IconButton(
              onPressed: () {},
              tooltip: 'クリップボードにコピー',
              icon: const Icon(
                Icons.content_copy_outlined,
                size: 20,
                color: Colors.white,
              ),
            ),
          ],
        ),
        const SizedBox(height: 16)
      ],
    );
  }
}
  1. クリップボード機能を作成するためにインポートする
import 'package:flutter/services.dart';
  1. IconButton の onPressed 内の動作に追加する
IconButton(
  onPressed: () {
    final data = ClipboardData(text: text); // 2行を追加
    Clipboard.setData(data)
  },
  tooltip: 'クリップボードにコピー',
  icon: const Icon(
    Icons.content_copy_outlined,
    size: 20,
    color: Colors.white,
  ),
),;
  1. MarkdownElemendBuilder`を継承したクラスを作成し、先ほど用意したPreクラスを返すようにします。
/// Pre
class CustomPreBuilder extends MarkdownElementBuilder {
  
  Widget visitText(md.Text text, TextStyle? preferredStyle) {
    return Pre(text: text.text);
  }
}
  1. builders 引数に追加する
Markdown(
  selectable: true,
  builders: {
    'h1': CustomHeader1Builder(),
    'pre': CustomPreBuilder(),
  },
  data: '''
  
# コード

```
print("Hello World");
```

```

完成

シンタックスハイライトはないの?

SyntaxHighlighter を継承したクラスでパーサーを書けば出来るようです。
しかし筆者自身もそうですが、あまり理解していないので理解してからまた記事を書こうと考えています。

その3 画像をセンタリング(カスタマイズ)する

画像をセンタリングするにはimageBuilder内でWidgetを返す様にすれば出来ます。
ちなみにimageBuilder内はいろいろカスタマイズ出来るので cached_network_image を使ったキャッシュや画像を角丸にすることも出来ます。
画像表示にはLorem Picsumを利用しています。

手順

Markdown(
  selectable: true,
  builders: {
    'h1': CustomHeader1Builder(),
    'pre': CustomPreBuilder(),
    'blockquote': CustomBlockQuoteBuilder(),
  },
  imageBuilder: (uri, title, alt) {
    return Center(
      child: Image.network(
        uri.toString(),
      ),
    );
  },
  data: '''
# イメージ

![](https://picsum.photos/200/300)
''',
)

完成


Before


After

その4 リンクをタップできるようにする

Markdownはリンクを定義することができますがクリックしても何も反応がありません。

# リンク

[Zenn](https://zenn.dev)

手順

onTapLinkを内でリンクをクリックした処理を書くことで実装できます。
url_launcher ライブラリを利用しています。

import 'package:url_launcher/url_launcher.dart'; // 追加

Markdown(
  selectable: true,
  builders: {
    'h1': CustomHeader1Builder(),
    'pre': CustomPreBuilder(),
    'blockquote': CustomBlockQuoteBuilder(),
  },
  imageBuilder: (uri, title, alt) {
    return Center(
      child: Image.network(
        uri.toString(),
      ),
    );
  },
  onTapLink: (text, href, title) { // 追加
    if (href != null) {
      launch(href);
    }
  },
  data: '''
# リンク

[Zenn](https://zenn.dev)
''',
)

おわりに

flutter_markdownの基本的なカスタマイズ方法をご紹介しました。その1とその2が出来れば大体の見た目をカスタマイズすることが出来ると思います。パーサーや独自構文を追加することも出来るみたいですので、また実装して記事にしたいと思います。

Githubに今回のソースコード一式を上げています
https://github.com/K-shir0/flutter_markdown_sample

Discussion