🔽

Flutter WebでCSVダウンロードする

6 min read

タイトル通り、下記gifのようなものです。

はじめに

Flutter Webは日々進化しているものの、まだ一般的なWebアプリケーションとして提供するには不足している部分が多いのが現状です。

https://zenn.dev/tsuruo/articles/773a5a7ca14924

一方で、簡単なポートフォリオサイトや管理画面のような「細かい部分は気にせずブラウザで動かしたい」要件については、十分使うことができます。今回は、管理画面を作る上で必須となる「CSVダウンロード」の機能を見ていきます。

dart:io libraryは使えない

DartはIO処理が言語レベルで整っており、iOS/Androidでのファイル操作など基本的にdart:io libraryを使えば済むことが多いです。

https://api.dart.dev/stable/2.14.3/dart-io/dart-io-library.html

しかし、ドキュメントに明記されている通りWebアプリケーション(Flutter Web)ではこのライブラリを利用することができません。

File, socket, HTTP, and other I/O support for non-web applications.
Important: Browser-based apps can't use this library. Only the following can import and use the dart:io library:

  • Servers
  • Command-line scripts
  • Flutter mobile apps
  • Flutter desktop apps

スマートフォンの場合はローカル領域にファイルを保存することができますが、Webアプリケーションの場合はブラウザにそれがないことが理由かなと思います。

AnchorElementクラス

本記事の主旨である、AnchorElementクラスです。dart:html libraryにあるクラスであり、HtmlElement class - dart:html library - Dart APIを継承しています。HtmlElementクラスを見るとわかりますが、前述のAnchorElementクラスの他に、BodyElementやTextAreaElementなどHTMLElementに対応するクラスが多数用意されています。これらクラスを用いることで、Javascriptで行なうようなDOM操作ができるわけですね。

https://api.flutter.dev/flutter/dart-html/AnchorElement-class.html

MDNのドキュメントにHTMLAnchorElementについて記載されています。確かではないですが、このWeb APIのDartラッパーがdart:html libraryであると解釈して問題無さそうです(Webと同じAPIがDartのクラスとして提供されている)。

https://developer.mozilla.org/ja/docs/Web/API/HTMLAnchorElement

つまり、このAnchorElementクラスを使いHTML属性にdownloadを付ければ想定の挙動が実現できそうだと想像がつきます。今回のファイルダウンロード機能の他にHTML属性を使用した機能を実装したい場合にも、対応したHtmlElementクラスを使って実装すれば良さそうですね。

実装する

CSVの整形についてはcsv | Dart Packageが便利です。

pubspec.yaml
dependencies:
  csv:

Model

よくある一般的なUserモデルを適当に用意しました。

user.dart
user.dart

class User {
  const User({
    required this.name,
    required this.age,
    required this.blood,
    required this.birthDate,
  });
  final String name;
  final int age;
  final Blood blood;
  final DateTime birthDate;

  List<String> toCsvFormat() => [
        name,
        '$age',
        describeEnum(blood).toUpperCase(),
        DateFormat('yyyy/MM/dd').format(birthDate),
      ];
}

特筆することは無いですが、CSVフォーマット前提のモデルなので下記のような整形メソッドを用意しておくと良いと思います(一部微妙な実装もありますが、本記事の主旨ではないのでスルーします)。

user.dart
List<String> toCsvFormat() => [
        name,
        '$age',
        // Dart2.15以降ではEnum拡張の`name`が使えるようになります
        // `Blood.values.name.toUpperCase()`
        describeEnum(blood).toUpperCase(),
        // DateFormatインスタンスの生成コストが余計なので別で定義した方がベター
        DateFormat('yyyy/MM/dd').format(birthDate),
      ];

ダミーデータ

Perfumeの3人です。綺麗に歳を重ねて感慨深いですね..。

user.dart
// ref. https://www.perfume-web.jp/profile/
final users = [
  User(
    name: 'a-chan',
    age: 32,
    blood: Blood.a,
    birthDate: DateTime(1989, 2, 15),
  ),
  User(
    name: 'NOCCHi',
    age: 33,
    blood: Blood.a,
    birthDate: DateTime(1988, 9, 20),
  ),
  User(
    name: 'KASHIYUKA',
    age: 32,
    blood: Blood.a,
    birthDate: DateTime(1988, 12, 23),
  ),
];

CSVダウンロード処理

最後にメインのCSVダウンロード処理です。AnchorElementでHTML属性のdownloadを指定します。対象のアンカーリンクをクリックすることでCSVがブラウザからダウンロードされる挙動となります。setAttributeに指定する属性は、MDNのドキュメントにHTML属性のリファレンスがあるので参照すると良いと思います。download属性もこの中にあります。

https://developer.mozilla.org/ja/docs/Web/HTML/Attributes
final header = ['name', 'age', 'blood', 'birth'];
final rows = users.map((u) => u.toCsvFormat()).toList();
final csv = const ListToCsvConverter().convert(
    [header, ...rows],
);
AnchorElement(href: 'data:text/plain;charset=utf-8,$csv')
    ..setAttribute('download', 'users.csv')
    ..click();

また、ダウンロードしたCSVをエクセルで開く場合に日本語文字が含まれていると化けてしまうので、必要に応じてUTF-8 BOMを付与するメソッドに拡張しました(下記)。BOMの付与についてはcsvパッケージに該当のイシューがありました。

https://github.com/close2/csv/issues/41#issuecomment-899038353
csv_download.dart
csv_download.dart
void csvDownload({
  required List<String> header,
  required List<List<String>> rows,
  bool utf8BOM = false,
}) {
  AnchorElement anchorElement;
  if (utf8BOM) {
    // Excelで開く用に日本語を含む場合はUTF-8 BOMにする措置
    // ref. https://github.com/close2/csv/issues/41#issuecomment-899038353
    final csv = const ListToCsvConverter(fieldDelimiter: ';').convert(
      [header, ...rows],
    );
    final bomUtf8Csv = [0xEF, 0xBB, 0xBF, ...utf8.encode(csv)];
    final base64CsvBytes = base64Encode(bomUtf8Csv);
    anchorElement = AnchorElement(
      href: 'data:text/plain;charset=utf-8;base64,$base64CsvBytes',
    );
  } else {
    final csv = const ListToCsvConverter().convert(
      [header, ...rows],
    );
    anchorElement = AnchorElement(
      href: 'data:text/plain;charset=utf-8,$csv',
    );
  }
  anchorElement
    ..setAttribute('download', 'users.csv')
    ..click();
}

おまけ

今回は、主にDartのHTMLElementクラスについて紹介した内容となりました。「CSVダウンロードはそんなに大変ではないだろう」と思って調べてみたらdart:ioライブラリが使えず、初めて使うクラスがいくつか出てきたので記事にしました。

ところで、Perfume3名の名前表記が大文字小文字で統一されていないことを不思議に思いませんでしたか?実はこれは誤りではなく公式に発表されています。英語表記は彼女たちがワールド進出する際にファンの間で話題になったのですが、その表記の変遷や背景も一緒に紹介しておきます。

  • あ〜ちゃん: 「a-chan」で全て小文字
    • 海外の人に伸ばして発音してもらえないため-を表記
    • 小文字にすることで一癖ありそう、興味を持ってもらいたいとラジオで公言
    • 「Aa-CHAN」の時期もあったが、AがA,B,C...の見出しと誤解されたため削除
  • のっち: 「NOCCHi」で本人の希望により最後のiのみ小文字(Appleオマージュ?)
  • かしゆか: 「KASHIYUKA」で全て大文字

(参考)Perfume「あ〜ちゃん」の英語表記が「A-CHAN」から「Aa-CHAN」へ変更される
(参考)Perfumeって何? Perfume(a-chan,KASHIYUKA,NOCCHi)

参考

Discussion

ログインするとコメントできます