ネイティブとWebの両方でfile_pickerを使いたい!
話題
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利用者
結論
普通に公式の説明にありました。
ネイティブは上記の通りFileを使う。
Webはfile_pickerの戻り値であるFilePickerResultのプロパティであるbytesを使う。
両立させたいときはexportで分岐させればいけそうですね。
本文
Fileか.bytesか
file_pickerの戻り値であるPickFilerResultには様々なプロパティが用意されており、PickeFileResult.files.singleで一つのファイルに関するプロパティにアクセスできるのですが、ネイティブ環境では.bytesはnullになってしまうようです。
そのため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させたいパッケージを分岐させるときに有用(というか必須)な手法です。
ネイティブのアプリがある体で解説します。
ネイティブのアプリ
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.dart、native.dart、web.dartという3つのファイルを作成してください。
それぞれの中身はこんな感じです。
ファイルの中身
export 'web.dart' if (dart.library.io) '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;
}
}
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を書き換えます。
final fileBytes = File(filePickerResult.files.single.path.toString()).readAsByteSync(); // これを
final fileBytes = const BytesReader().readBytes(filePickerResult); // こう!
これで完成です!
何が起こっているのか解説します。
native.dart、web.dartにはBytesReaderという同じ名前のクラスがあり、その中にreadBytes()という同じ名前のメソッドを持っています。メソッドの中身はそれぞれネイティブのアプリ、Webのアプリで実行したい処理が記述されています。
- ネイティブには
dart:ioを使用してFile()に変換する処理 - Webには
FilePickerResultの.bytesを利用した処理を書きました。
connection.dartではdart:ioをサポートしていない場合、web.dartに、サポートしている場合はnative.dartに繋ぐ処理を行っています。(一昔前の電話センターみたいなイメージです。)
ここでmain.dartにconnection.dartをimportするとnative.dart``web.dartのいずれかに依存するので、共通のクラスであるBytesReaderが使用できるということです。
終わりに
file_pickerをネイティブとWebで両立する方法を書いてみました。
間違っていることがあったらコメントなどで教えていただければ嬉しいです!
ここまで読んでいただきありがとうございました!
Discussion