Flutter-ファイル名選択ダイアログ

Flutterでファイルを選択で使用するダイアログのパッケージについての調査。
英語圏ではPikcerと呼んでいるので、pub.devでの検索ワードを求めるのが面倒だった。

ここで主眼としているのは保存に関するダイアログ。

  • 2023/04/06
    上書き保存ダイアログが出ない件が修正されたことについて追記。

よく使われるパッケージ

file_picker

file_picker | Flutter package
A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension f...

多分これが一番よく使われているパッケージなのだと思う。
「開く」に関しては、すべてのデバイスで使えるようになっている。

残念なのは、「保存」がプアなこと。

「保存」対応しているデバイスにAndroidとiOSとWEBが外れている。
特にAndroidとiOSが抜けているのがきついかもしれない。

「保存」の問題点

Windows版だけど使っていて以下の問題点が分かった。

  1. すでに同名のファイルがある場合、上書き確認ダイアログが表示されない。
    5.2.7で対応されていた。
  2. 保存ダイアログで既定で表示されているファイル名から変更すると、拡張子が付与されないファイル名が帰ってくる。

1については以下の様にissueが入っているが、残念なことに対応はされていない。
Closeになり5.2.7で対応が入った模様。

https://github.com/miguelpruivo/flutter_file_picker/issues/989

2については、他のデバイスでの拡張子付与の仕組み等を考慮し検討しなければいけないので難しいのかな。

flutter_file_dialog

Android/iOS版の保存ダイアログがあるパッケージ。

flutter_file_dialog | Flutter package
Dialogs for picking and saving files in Android and in iOS.

なお、file_picker側でAndroid/iOSが使えるようにしてくれというissueがあり、その中の一つの提案として、このパッケージを使ったらどうかというのがあった。

Save-file dialog for Android and iOS ? · Issue #882 · miguelpruivo/flutter_file_picker
Hello Miguel Is it possible to have the api saveFile() for Android and iOS ? Currently, it is not supported on Android a...

「保存」の問題点?

file_picker側はファイル名を取得するというAPIになっているのだけど、こちらはデータ(もしくは存在しているファイル)を与えて、指定された場所に保存までしてくれるという機能になっている。

通常はfile_pickerを使用し、Android/iOSの保存のみこちらを使用するという場合には、上位側からのAPIをflutter_file_dialogに合わせる必要が出てくる。
ただ、これに関しては特に問題ない。

本当の問題は、保存までパッケージ内で実施されてしまうため、保存データが大きい場合に進捗状況を表示する機能を付与できないということになる。

小さいデータであれば問題ないのだけど。

filesystem_picker

パッケージ検索してた時に、「保存」ダイアログがあるようにサンプルが付けられていたので目に留まった物。

filesystem_picker | Flutter package
FileSystem file or folder picker dialog. Allows the user to browse the file system and pick a folder or file.

残念なことに「保存」ダイアログはなかった。
よく見たら、フォルダ選択ダイアログに保存なにがしというタイトルがついていたので見間違いをしてしまったようだ。

上2つと違い、ウィジェットとして独自構築している点。

ダイアログではなく、既存ページ内に組み込むことも可能なようだ。

保存時での実際の利用

基本はfile_picker、Android/iOSの保存にはflutter_file_dialogを使う。
また保存用のAPIはflutter_file_dialog側に合わせるようにする。
といった感じだろうか。

file_pickerのWindows版保存に問題もあるので、その対応も入れ込む。
ただしこちらは将来file_pickerで改善される可能性もあるので、そこらあたり考慮した形にする。

Windows版のfile_pickerの対応

先にあげた2点の問題をまず解決したい。
issuesには1点目だけ登録されているが解決のめどは立っておらず、2点目はissuesにあげられていない。(自分があげろよという意見もあるが)

この問題の対応は、以下ファイルの修正で可能。

https://github.com/miguelpruivo/flutter_file_picker/blob/master/lib/src/windows/file_picker_windows.dart
  Pointer<OPENFILENAMEW> _instantiateOpenFileNameW({
    bool allowMultiple = false,
    String? dialogTitle,
    String? defaultFileName,
    String? initialDirectory,
    List<String>? allowedExtensions,
    FileType type = FileType.any,
    bool lockParentWindow = false,
    bool isSave = false, // 追加・saveFileからの呼び出しでtrueにする。
  }) {
    final lpstrFileBufferSize = 8192 * maximumPathLength;
    final Pointer<OPENFILENAMEW> openFileNameW = calloc<OPENFILENAMEW>();

    openFileNameW.ref.lStructSize = sizeOf<OPENFILENAMEW>();
    openFileNameW.ref.lpstrTitle =
        (dialogTitle ?? defaultDialogTitle).toNativeUtf16();
    openFileNameW.ref.lpstrFile = calloc.allocate<Utf16>(lpstrFileBufferSize);
    openFileNameW.ref.lpstrFilter =
        fileTypeToFileFilter(type, allowedExtensions).toNativeUtf16();
    openFileNameW.ref.nMaxFile = lpstrFileBufferSize;
    openFileNameW.ref.lpstrInitialDir =
        (initialDirectory ?? '').toNativeUtf16();
    // 以下2設定が変更箇所
    openFileNameW.ref.flags = ofnExplorer |
        ofnFileMustExist |
        ofnHideReadOnly |
        (isSave ? 0x00000002 : 0);
    if (isSave && allowedExtensions != null && allowedExtensions.isNotEmpty) {
      openFileNameW.ref.lpstrDefExt = allowedExtensions[0].toNativeUtf16();
    }

調べてみると、Windows版はWin32APIのGetSaveFileNameを呼んでいるようだったので、そこへ渡すフラグ等の変更でいい感じになった。

上書き確認にはOFN_OVERWRITEPROMPT(0x00000002)を指定し、拡張子の指定がない場合の追加拡張子の指定にはlpstrDefExtにその拡張子名を指定すればいい感じだということになる。

これをおおもとに取り込んでもらえればいいのだけど、まずは自分用として利用するためプロジェクトに取り込む。

保存処理

デバイスによる切り替え、APIの統一などで以下の様な感じの実装になる。

file_picker側は以下の様に独自クラスのものを設定しておけば、利用側の変更は必要ない。

import 'package:file_picker/file_picker.dart';
import 'package:sample4/sub/file_picker_windows.dart' as s;

// runAppの前に以下を呼び出す。
if (!kIsWeb && Platform.isWindows) {
  FilePicker.platform = s.FilePickerWindows();
}
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:path/path.dart' as p;
import 'package:sample4/sub/file_picker_windows.dart' as s;

Future<String?> saveFile(String title, Uint8List data, String fileName) async {
  if (Platform.isAndroid || Platform.isIOS) {
    final params = SaveFileDialogParams(data: data, fileName: fileName);
    return FlutterFileDialog.saveFile(params: params);
  } else {
    // 拡張子を解析
    final extentions = <String>[];
    final ext = p.extension(fileName);
    if (ext.isNotEmpty) {
      exList.add(ext.substring(1)); // sは半角だとエラーになったので全角にしてある
    }
    // 保存ダイアログ
    String? output;
    output = await FilePicker.platform.saveFile(
      dialogTitle: title,
      fileName: fileName,
      type: FileType.custom,
      allowedExtensions: exList.isEmpty ? null : exList,
    );
    if (output != null) {
      await File(output).writeAsBytes(data);
    }

    return output;
  }
}

ひとつ前の部分で、自分用に作成したWindows版保存処理に関しては、importで別名定義してとりあえずクラス名が重複しないようにしておく。

一応これで保存処理で一通りのデバイスに対応できるものになったと思う。

最後に

file_pickerとかけっこう前からあるように思えるのだけど、保存用のダイアログがAndroid/iOSで実装されなかったのはなぜなのだろうか。

コメント

タイトルとURLをコピーしました