🐤

ネイティブとWebの両方でfile_pickerを使いたい!

2024/10/17に公開

話題

file_picker、便利ですよね。
最近ネイティブのアプリをWebにも対応させようと画策していたのですが、Webだとdart:ioが使えないそうじゃないですか。
つまり今までネイティブでfile_pickerで選択したファイルをUint8Listにするときに使っていたFileが使えなくなるってことです。

final pickFileResult = await FilePicker.platform.pickFiles();
if(pickFileResult != null){
final fileBytes = File(pickFileResult.files.single.path.toString()).readAsBytesSync();
}else{...

これは大変、ビルドエラーになってしまう!
ということでネイティブとWebとを両立する方法を学んだので一緒に見ていきたいと思います。

対象者

Flutter初学者
file_picker利用者

結論

普通に公式の説明にありました。
https://pub.dev/packages/file_picker
ネイティブは上記の通りFileを使う
Webはfile_pickerの戻り値であるFilePickerResultのプロパティであるbytesを使う

両立させたいときはexportで分岐させればいけそうですね。

本文

Fileか.bytesか

file_pickerの戻り値であるPickFilerResultには様々なプロパティが用意されており、PickeFileResult.files.singleで一つのファイルに関するプロパティにアクセスできるのですが、ネイティブ環境では.bytesnullになってしまうようです。
そのためFlieを使って以下のように実装するのが良いとのことです。

final fileBytes = File(pickFilerResult.files.single.path.toString()).readAsBytesSync();

ちなみにWeb版でFileを使うとちゃんとエラーで教えてくれます。

Error extracting text from PDF:
    On web `path` is unavailable and accessing it causes this exception.
    You should access `bytes` property instead,
    Read more about it [here]
    (https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ)

Web版では以下のように記述しましょう

final pickFilerResult = await FilePicker.platform.pickFiles();
if(pickFilerResult != null){
final fileBytes = pickFilerResult.files.single.bytes;
}else{...
if文について

私はif文をこう書くことが多いんですがどっちがいいんでしょうか?

// 返り値がnull許容型の関数内
final pickFilerResult = await FilePicker.platform.pickFiles();
if(pickFilerResult == null){return;}
final fileBytes = pickFilerResult.files.single.bytes;

exportの書き方

(参考にした記事を載せたかったのですが見つけられなかったので見つけ次第追記します。)

exportは依存させたいパッケージ、つまりimportさせたいパッケージを分岐させるときに有用(というか必須)な手法です。

ネイティブのアプリがある体で解説します。

ネイティブのアプリ
main.dart
import 'dart:io'
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
              onPressed: () => test(), child: const Text('print file bytes')),
        ),
      ),
    );
  }

  Future<void> printFileBytes() async {
    try {
      final filePickerResult = await FilePicker.platform.pickFiles();
      if (filePickerResult == null) {
        return;
      }
      final fileBytes = File(filePickerResult.files.single.path.toString()).readAsByteSync();
      if (kDebugMode) {
        debugPrint(fileBytes.toString());
      }
    } catch (e) {
      if (kDebugMode) {
        debugPrint('Error extracting text from PDF: $e');
      }
    }
  }
}

まずconnectionという名前のフォルダーを作成して、connection.dartnative.dartweb.dartという3つのファイルを作成してください。
それぞれの中身はこんな感じです。

ファイルの中身
connection.dart
export 'web.dart' if (dart.library.io) 'native.dart';
native.dart
import 'dart:io';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';

class BytesReader {
  const BytesReader();

  Uint8List readBytes(FilePickerResult filePickerResult) {
    print('native');
    final file = File(filePickerResult.files.single.path.toString());
    final fileBytes = file.readAsBytesSync();
    return fileBytes;
  }
}
web.dart
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';

class BytesReader {
  const BytesReader();

  Uint8List readBytes(FilePickerResult filePickerResult) {
    print('web');
    final fileBytes = filePickerResult.files.single.bytes!;
    return fileBytes;
  }
}

続いて元あったアプリにconnection.dartをimportし、main.dartを書き換えます。

main.dart
final fileBytes = File(filePickerResult.files.single.path.toString()).readAsByteSync(); // これを
final fileBytes = const BytesReader().readBytes(filePickerResult); // こう!

これで完成です!

何が起こっているのか解説します。
native.dartweb.dartにはBytesReaderという同じ名前のクラスがあり、その中にreadBytes()という同じ名前のメソッドを持っています。メソッドの中身はそれぞれネイティブのアプリ、Webのアプリで実行したい処理が記述されています。

  • ネイティブにはdart:ioを使用してFile()に変換する処理
  • WebにはFilePickerResult.bytesを利用した処理を書きました。

connection.dartではdart:ioをサポートしていない場合、web.dartに、サポートしている場合はnative.dartに繋ぐ処理を行っています。(一昔前の電話センターみたいなイメージです。)
ここでmain.dartconnection.dartをimportするとnative.dart``web.dartのいずれかに依存するので、共通のクラスであるBytesReaderが使用できるということです。

終わりに

file_pickerをネイティブとWebで両立する方法を書いてみました。
間違っていることがあったらコメントなどで教えていただければ嬉しいです!
ここまで読んでいただきありがとうございました!

Discussion