ネイティブと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