📁

【Flutter】Windows, Webでのファイル選択・保存方法 [前編]

2021/06/24に公開

前回Todoアプリまで作成したわけですが、せっかくWindowsデスクトップアプリとWebアプリをターゲットにしているのでワンソースで実現するのは難しそうな、ファイル選択・保存を試してみます。

結論から言ってしまうと、完全なワンソースでは無理でしたが、多少の処理分岐で実現可能でした。

長くなってしまったのでWindowsデスクトップアプリを前編としています。
後編(Webアプリ)は後日。

パッケージ探し

やりたい事が決まったらパッケージ探し。
「flutter file select」やら「flutter file picker」等で検索すると、まずfile_pickerが候補に上がります。
色々情報もあり実装しやすそうなのですが、対象がAndroid, iOS, Webのみ。
デスクトップアプリに対応してないので今回は使えませんでした。

最終的に採用したのがfile_selectorfile_selector_windowsを組み合わせる方法。
ただし、後者はFlutterのデスクトップアプリ同様に正式リリースされているものではないので、今後使い方が変わる可能性があります。

file_selectorインストール

pubspec.yamlに以下を追加します。

pubspec.yaml
dependencies:
  file_selector: ^0.8.2
  file_selector_windows: ^0.0.2

自動で実行されなければpub getを実行。

> flutter pub get

使用するソースでimportすれば完了です。

main.dart
import 'package:file_selector/file_selector.dart';

ファイル保存・ファイル読込ボタン追加

保存・読込のためのボタンをTodoアプリに追加します。
body直下をContainer > Columnで括り、ボタン領域をRow、リスト表示部分をExpandedで表示します。

main.dart
  body: Container(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Padding(
              padding: EdgeInsets.all(10),
              child: ElevatedButton(
                onPressed: () {
                },
                child:
                    Text("ファイル読込", style: TextStyle(color: Colors.white)),
              ),
            ),
            Padding(
              padding: EdgeInsets.all(10),
              child: ElevatedButton(
                onPressed: () {
                },
                style: ElevatedButton.styleFrom(
                  primary: Colors.red,
                ),
                child:
                    Text("ファイル保存", style: TextStyle(color: Colors.white)),
              ),
            ),
          ],
        ),
        Expanded(
          child: ListView.builder(
            itemCount: todoList.length,
            itemBuilder: (context, index) {
              return Card(
                child: ListTile(
                  title: Text(todoList[index]),
                  trailing: ElevatedButton(
                    onPressed: () {
                      setState(() {
                        todoList.removeAt(index);
                      });
                    },
                    child:
                        Text("削除", style: TextStyle(color: Colors.white)),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    ),
  ),

これでボタン表示までは出来ました。(押下してもまだ何も起きない)

ファイル保存

まずTodoリストはあるので保存から。
保存先選択はパッケージページのSaving a fileを参考に実装します。
json形式としているのでmimeTypeを変更します。

main.dart
  child: ElevatedButton(
    onPressed: () async {
      String? path = await getSavePath();
      print(path);
      if (path == null) {
	return;
      } else {
	final data =
	    Uint8List.fromList(jsonEncode(todoList).codeUnits);
	final mimeType = "application/json";
	final file = XFile.fromData(data, mimeType: mimeType);
	await file.saveTo(path);
      }
    },
    style: ElevatedButton.styleFrom(
      primary: Colors.red,
    ),
    child:
	Text("ファイル保存", style: TextStyle(color: Colors.white)),
  ),

getSavePathで表示された名前を付けて保存ダイアログでキャンセルを押下すると、pathnullが入るので、その場でreturnさせます。
pathnullでなければ、todoListjsonEncodeで文字列化、それを指定パスに保存するという流れです。

ここで一つ問題が。

何もオプションを指定せずにgetSavePathを呼び出すと、ファイル名もファイル種類も空のダイアログが表示されていまいます。
ですので、ここにデフォルト値を設定します。

main.dart
String? path = await getSavePath(acceptedTypeGroups: [
                        XTypeGroup(label: 'json', extensions: ['json'])
                      ], suggestedName: "todoList.json");


無事、デフォルトのファイル名とファイル種類が設定できました。
ここで保存を押下すると。

ちゃんとテキストに出力されていますね。

ファイル読込

保存が出来たので次は読み込みです。
こちらもパッケージページのOpen a single fileを参考に実装します。

main.dart
 child: ElevatedButton(
    onPressed: () async {
      final XTypeGroup typeGroup = XTypeGroup(
	label: 'json',
	extensions: ['json'],
      );
      final XFile? file =
	  await openFile(acceptedTypeGroups: [typeGroup]);
      if (file == null) {
	return;
      }
      final String fileContent = await file.readAsString();
      print(fileContent);
      setState(() {
	todoList = jsonDecode(fileContent);
      });
    },
    child:
	Text("ファイル読込", style: TextStyle(color: Colors.white)),
  ),

読み込みも保存と同様にファイルの種類設定を行います。
ファイル選択なのでデフォルトファイル名は指定しません。
ファイル選択ダイアログでキャンセルが押下された場合、戻り値がnullになるのも同様です。
filenullでなければ、file.readAsStringで文字列として読み込み、jsonDecodeでList型に戻してやる算段です。

これで、ファイル読込ボタンから先程出力したファイルを選択すると…

> todoList = jsonDecode(fileContent);

jsonのデコードでエラーが発生しました。
どうやらList<dynamic>型にしかデコード出来ないようなのでtodoListの方を変更します。

main.dart
- List<String> todoList = [];
+ List<dynamic> todoList = [];

これで無事読み込みも成功しました。

読み込み後の表示が出力前と同じなので微妙ですね…

後編に続く

ここまででWindowsデスクトップアプリでのTodoリスト出力、読込ができました。
後編ではこのソースを元にWebアプリでの選択(アップロード)・保存(ダウンロード)を実装していきます。

Discussion